@syncular/server-hono 0.0.6-125 → 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.
@@ -4,20 +4,19 @@ import { cors } from 'hono/cors';
4
4
  import type { UpgradeWebSocket } from 'hono/ws';
5
5
  import { describeRoute, resolver, validator as zValidator } from 'hono-openapi';
6
6
  import { z } from 'zod';
7
+ import {
8
+ closeUnauthenticatedSocket,
9
+ parseBearerToken,
10
+ parseWebSocketAuthToken,
11
+ } from './live-auth';
7
12
  import type {
8
13
  ConsoleApiKey,
9
14
  ConsoleApiKeyBulkRevokeResponse,
10
15
  ConsoleApiKeyCreateResponse,
11
- ConsoleClearEventsResult,
12
16
  ConsoleClient,
13
17
  ConsoleCommitListItem,
14
- ConsoleCompactResult,
15
- ConsoleEvictResult,
16
18
  ConsoleOperationEvent,
17
19
  ConsolePaginatedResponse,
18
- ConsolePruneEventsResult,
19
- ConsolePrunePreview,
20
- ConsolePruneResult,
21
20
  ConsoleRequestEvent,
22
21
  ConsoleTimelineItem,
23
22
  LatencyPercentiles,
@@ -155,6 +154,11 @@ const GatewaySingleInstanceQuerySchema = GatewayInstanceFilterSchema;
155
154
  const GatewaySingleInstancePartitionQuerySchema =
156
155
  ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape);
157
156
 
157
+ type GatewayInstanceFilterQuery = {
158
+ instanceId?: string;
159
+ instanceIds?: string;
160
+ };
161
+
158
162
  const GatewayApiKeyStatusSchema = z.enum(['active', 'revoked', 'expiring']);
159
163
 
160
164
  const GatewayApiKeysQuerySchema = ConsolePaginationQuerySchema.extend({
@@ -416,84 +420,58 @@ function parseLocalNumericId(value: string): number | null {
416
420
  return parsed;
417
421
  }
418
422
 
419
- function resolveEventTarget(args: {
420
- id: string;
423
+ function noInstancesSelectedResponse(): {
424
+ ok: false;
425
+ status: 400;
426
+ error: 'NO_INSTANCES_SELECTED';
427
+ message: string;
428
+ } {
429
+ return {
430
+ ok: false,
431
+ status: 400,
432
+ error: 'NO_INSTANCES_SELECTED',
433
+ message: 'No enabled instances matched the provided instance filter.',
434
+ };
435
+ }
436
+
437
+ function resolveSingleSelectedInstance(args: {
421
438
  instances: ConsoleGatewayInstance[];
422
- query: { instanceId?: string; instanceIds?: string };
439
+ query: GatewayInstanceFilterQuery;
440
+ onMultiple: { error: string; message: string };
423
441
  }):
424
- | { ok: true; instance: ConsoleGatewayInstance; localEventId: number }
425
- | { ok: false; status: 400 | 404; error: string; message?: string } {
426
- const federated = parseFederatedNumericId(args.id);
427
- if (federated) {
428
- const instance = findInstanceById({
429
- instances: args.instances,
430
- instanceId: federated.instanceId,
431
- });
432
- if (!instance) {
433
- return {
434
- ok: false,
435
- status: 404,
436
- error: 'NOT_FOUND',
437
- message: 'Instance not found',
438
- };
439
- }
440
- return { ok: true, instance, localEventId: federated.localId };
441
- }
442
-
443
- const localEventId = parseLocalNumericId(args.id);
444
- if (localEventId === null) {
445
- return {
446
- ok: false,
447
- status: 400,
448
- error: 'INVALID_FEDERATED_ID',
449
- message:
450
- 'Expected either "<instanceId>:<eventId>" or "<eventId>" with an explicit instance filter.',
451
- };
452
- }
453
-
454
- const selectedInstances = selectInstances({
455
- instances: args.instances,
456
- query: args.query,
457
- });
442
+ | { ok: true; instance: ConsoleGatewayInstance }
443
+ | { ok: false; status: 400; error: string; message: string } {
444
+ const selectedInstances = selectInstances(args);
458
445
  if (selectedInstances.length === 0) {
459
- return {
460
- ok: false,
461
- status: 400,
462
- error: 'NO_INSTANCES_SELECTED',
463
- message: 'No enabled instances matched the provided instance filter.',
464
- };
446
+ return noInstancesSelectedResponse();
465
447
  }
466
448
  if (selectedInstances.length > 1) {
467
449
  return {
468
450
  ok: false,
469
451
  status: 400,
470
- error: 'AMBIGUOUS_EVENT_ID',
471
- message:
472
- 'Local event IDs are ambiguous across multiple instances. Use "<instanceId>:<eventId>" or select one instance.',
452
+ error: args.onMultiple.error,
453
+ message: args.onMultiple.message,
473
454
  };
474
455
  }
475
456
 
476
457
  const instance = selectedInstances[0];
477
458
  if (!instance) {
478
- return {
479
- ok: false,
480
- status: 400,
481
- error: 'NO_INSTANCES_SELECTED',
482
- message: 'No enabled instances matched the provided instance filter.',
483
- };
459
+ return noInstancesSelectedResponse();
484
460
  }
485
-
486
- return { ok: true, instance, localEventId };
461
+ return { ok: true, instance };
487
462
  }
488
463
 
489
- function resolveCommitTarget(args: {
490
- seq: string;
464
+ function resolveFederatedOrLocalNumericTarget(args: {
465
+ id: string;
491
466
  instances: ConsoleGatewayInstance[];
492
- query: { instanceId?: string; instanceIds?: string };
467
+ query: GatewayInstanceFilterQuery;
468
+ invalidMessage: string;
469
+ ambiguousError: string;
470
+ ambiguousMessage: string;
493
471
  }):
494
- | { ok: true; instance: ConsoleGatewayInstance; localCommitSeq: number }
472
+ | { ok: true; instance: ConsoleGatewayInstance; localId: number }
495
473
  | { ok: false; status: 400 | 404; error: string; message?: string } {
496
- const federated = parseFederatedNumericId(args.seq);
474
+ const federated = parseFederatedNumericId(args.id);
497
475
  if (federated) {
498
476
  const instance = findInstanceById({
499
477
  instances: args.instances,
@@ -507,92 +485,96 @@ function resolveCommitTarget(args: {
507
485
  message: 'Instance not found',
508
486
  };
509
487
  }
510
- return { ok: true, instance, localCommitSeq: federated.localId };
488
+ return { ok: true, instance, localId: federated.localId };
511
489
  }
512
490
 
513
- const localCommitSeq = parseLocalNumericId(args.seq);
514
- if (localCommitSeq === null) {
491
+ const localId = parseLocalNumericId(args.id);
492
+ if (localId === null) {
515
493
  return {
516
494
  ok: false,
517
495
  status: 400,
518
496
  error: 'INVALID_FEDERATED_ID',
519
- message:
520
- 'Expected either "<instanceId>:<commitSeq>" or "<commitSeq>" with an explicit instance filter.',
497
+ message: args.invalidMessage,
521
498
  };
522
499
  }
523
500
 
524
- const selectedInstances = selectInstances({
501
+ const selection = resolveSingleSelectedInstance({
525
502
  instances: args.instances,
526
503
  query: args.query,
504
+ onMultiple: {
505
+ error: args.ambiguousError,
506
+ message: args.ambiguousMessage,
507
+ },
527
508
  });
528
- if (selectedInstances.length === 0) {
529
- return {
530
- ok: false,
531
- status: 400,
532
- error: 'NO_INSTANCES_SELECTED',
533
- message: 'No enabled instances matched the provided instance filter.',
534
- };
535
- }
536
- if (selectedInstances.length > 1) {
537
- return {
538
- ok: false,
539
- status: 400,
540
- error: 'AMBIGUOUS_COMMIT_ID',
541
- message:
542
- 'Local commit IDs are ambiguous across multiple instances. Use "<instanceId>:<commitSeq>" or select one instance.',
543
- };
544
- }
509
+ if (!selection.ok) return selection;
545
510
 
546
- const instance = selectedInstances[0];
547
- if (!instance) {
548
- return {
549
- ok: false,
550
- status: 400,
551
- error: 'NO_INSTANCES_SELECTED',
552
- message: 'No enabled instances matched the provided instance filter.',
553
- };
554
- }
511
+ return { ok: true, instance: selection.instance, localId };
512
+ }
513
+
514
+ function resolveEventTarget(args: {
515
+ id: string;
516
+ instances: ConsoleGatewayInstance[];
517
+ query: GatewayInstanceFilterQuery;
518
+ }):
519
+ | { ok: true; instance: ConsoleGatewayInstance; localEventId: number }
520
+ | { ok: false; status: 400 | 404; error: string; message?: string } {
521
+ const resolved = resolveFederatedOrLocalNumericTarget({
522
+ id: args.id,
523
+ instances: args.instances,
524
+ query: args.query,
525
+ invalidMessage:
526
+ 'Expected either "<instanceId>:<eventId>" or "<eventId>" with an explicit instance filter.',
527
+ ambiguousError: 'AMBIGUOUS_EVENT_ID',
528
+ ambiguousMessage:
529
+ 'Local event IDs are ambiguous across multiple instances. Use "<instanceId>:<eventId>" or select one instance.',
530
+ });
531
+ if (!resolved.ok) return resolved;
532
+ return {
533
+ ok: true,
534
+ instance: resolved.instance,
535
+ localEventId: resolved.localId,
536
+ };
537
+ }
555
538
 
556
- return { ok: true, instance, localCommitSeq };
539
+ function resolveCommitTarget(args: {
540
+ seq: string;
541
+ instances: ConsoleGatewayInstance[];
542
+ query: GatewayInstanceFilterQuery;
543
+ }):
544
+ | { ok: true; instance: ConsoleGatewayInstance; localCommitSeq: number }
545
+ | { ok: false; status: 400 | 404; error: string; message?: string } {
546
+ const resolved = resolveFederatedOrLocalNumericTarget({
547
+ id: args.seq,
548
+ instances: args.instances,
549
+ query: args.query,
550
+ invalidMessage:
551
+ 'Expected either "<instanceId>:<commitSeq>" or "<commitSeq>" with an explicit instance filter.',
552
+ ambiguousError: 'AMBIGUOUS_COMMIT_ID',
553
+ ambiguousMessage:
554
+ 'Local commit IDs are ambiguous across multiple instances. Use "<instanceId>:<commitSeq>" or select one instance.',
555
+ });
556
+ if (!resolved.ok) return resolved;
557
+ return {
558
+ ok: true,
559
+ instance: resolved.instance,
560
+ localCommitSeq: resolved.localId,
561
+ };
557
562
  }
558
563
 
559
564
  function resolveSingleInstanceTarget(args: {
560
565
  instances: ConsoleGatewayInstance[];
561
- query: { instanceId?: string; instanceIds?: string };
566
+ query: GatewayInstanceFilterQuery;
562
567
  }):
563
568
  | { ok: true; instance: ConsoleGatewayInstance }
564
569
  | { ok: false; status: 400; error: string; message: string } {
565
- const selectedInstances = selectInstances(args);
566
- if (selectedInstances.length === 0) {
567
- return {
568
- ok: false,
569
- status: 400,
570
- error: 'NO_INSTANCES_SELECTED',
571
- message: 'No enabled instances matched the provided instance filter.',
572
- };
573
- }
574
-
575
- if (selectedInstances.length > 1) {
576
- return {
577
- ok: false,
578
- status: 400,
570
+ return resolveSingleSelectedInstance({
571
+ ...args,
572
+ onMultiple: {
579
573
  error: 'INSTANCE_REQUIRED',
580
574
  message:
581
575
  'This endpoint requires exactly one target instance. Provide `instanceId` or a single-value `instanceIds` filter.',
582
- };
583
- }
584
-
585
- const instance = selectedInstances[0];
586
- if (!instance) {
587
- return {
588
- ok: false,
589
- status: 400,
590
- error: 'NO_INSTANCES_SELECTED',
591
- message: 'No enabled instances matched the provided instance filter.',
592
- };
593
- }
594
-
595
- return { ok: true, instance };
576
+ },
577
+ });
596
578
  }
597
579
 
598
580
  function minNullable(values: Array<number | null>): number | null {
@@ -728,17 +710,6 @@ function resolveForwardAuthorization(args: {
728
710
  return null;
729
711
  }
730
712
 
731
- function parseBearerToken(
732
- authHeader: string | null | undefined
733
- ): string | null {
734
- const value = authHeader?.trim();
735
- if (!value?.startsWith('Bearer ')) {
736
- return null;
737
- }
738
- const token = value.slice(7).trim();
739
- return token.length > 0 ? token : null;
740
- }
741
-
742
713
  async function fetchDownstreamJson<T>(args: {
743
714
  c: Context;
744
715
  instance: ConsoleGatewayInstance;
@@ -1079,6 +1050,222 @@ export function createConsoleGatewayRoutes(
1079
1050
  })
1080
1051
  );
1081
1052
 
1053
+ const withGatewayAuth = async (
1054
+ c: Context,
1055
+ callback: () => Promise<Response>
1056
+ ): Promise<Response> => {
1057
+ const auth = await options.authenticate(c);
1058
+ if (!auth) {
1059
+ return unauthorizedResponse(c);
1060
+ }
1061
+ return callback();
1062
+ };
1063
+
1064
+ const proxySingleInstanceJsonRequest = async <T>(args: {
1065
+ c: Context;
1066
+ query: { instanceId?: string; instanceIds?: string };
1067
+ method: 'GET' | 'POST' | 'DELETE';
1068
+ path: string;
1069
+ responseSchema: z.ZodType<T>;
1070
+ body?: unknown;
1071
+ }): Promise<Response> => {
1072
+ const target = resolveSingleInstanceTarget({
1073
+ instances,
1074
+ query: args.query,
1075
+ });
1076
+ if (!target.ok) {
1077
+ return args.c.json(
1078
+ {
1079
+ error: target.error,
1080
+ message: target.message,
1081
+ },
1082
+ target.status
1083
+ );
1084
+ }
1085
+
1086
+ const forwardQuery = sanitizeForwardQueryParams(
1087
+ new URL(args.c.req.url).searchParams
1088
+ );
1089
+ const result = await forwardDownstreamJsonRequest<T>({
1090
+ c: args.c,
1091
+ instance: target.instance,
1092
+ method: args.method,
1093
+ path: args.path,
1094
+ query: forwardQuery,
1095
+ ...(args.body === undefined ? {} : { body: args.body }),
1096
+ responseSchema: args.responseSchema,
1097
+ fetchImpl,
1098
+ });
1099
+
1100
+ if (!result.ok) {
1101
+ return jsonResponse(result.body, result.status);
1102
+ }
1103
+
1104
+ return jsonResponse(result.data, result.status);
1105
+ };
1106
+
1107
+ const selectTargetInstances = (
1108
+ c: Context,
1109
+ query: GatewayInstanceFilterQuery
1110
+ ):
1111
+ | { ok: true; selectedInstances: ConsoleGatewayInstance[] }
1112
+ | { ok: false; response: Response } => {
1113
+ const selectedInstances = selectInstances({ instances, query });
1114
+ if (selectedInstances.length > 0) {
1115
+ return { ok: true, selectedInstances };
1116
+ }
1117
+
1118
+ const noInstanceError = noInstancesSelectedResponse();
1119
+ return {
1120
+ ok: false,
1121
+ response: c.json(
1122
+ {
1123
+ error: noInstanceError.error,
1124
+ message: noInstanceError.message,
1125
+ },
1126
+ noInstanceError.status
1127
+ ),
1128
+ };
1129
+ };
1130
+
1131
+ const fetchFromSelectedInstances = async <T>(args: {
1132
+ c: Context;
1133
+ selectedInstances: ConsoleGatewayInstance[];
1134
+ path: string;
1135
+ query: URLSearchParams;
1136
+ schema: z.ZodType<T>;
1137
+ }): Promise<
1138
+ | {
1139
+ ok: true;
1140
+ successfulResults: Array<{
1141
+ instance: ConsoleGatewayInstance;
1142
+ data: T;
1143
+ }>;
1144
+ failedInstances: GatewayFailure[];
1145
+ }
1146
+ | { ok: false; response: Response }
1147
+ > => {
1148
+ const results = await Promise.all(
1149
+ args.selectedInstances.map((instance) =>
1150
+ fetchDownstreamJson({
1151
+ c: args.c,
1152
+ instance,
1153
+ path: args.path,
1154
+ query: args.query,
1155
+ schema: args.schema,
1156
+ fetchImpl,
1157
+ })
1158
+ )
1159
+ );
1160
+
1161
+ const failedInstances = results
1162
+ .filter(
1163
+ (result): result is { ok: false; failure: GatewayFailure } => !result.ok
1164
+ )
1165
+ .map((result) => result.failure);
1166
+ const successfulResults = results
1167
+ .map((result, index) => ({
1168
+ result,
1169
+ instance: args.selectedInstances[index],
1170
+ }))
1171
+ .filter(
1172
+ (
1173
+ entry
1174
+ ): entry is {
1175
+ result: { ok: true; data: T };
1176
+ instance: ConsoleGatewayInstance;
1177
+ } => Boolean(entry.instance) && entry.result.ok
1178
+ )
1179
+ .map((entry) => ({
1180
+ instance: entry.instance,
1181
+ data: entry.result.data,
1182
+ }));
1183
+
1184
+ if (successfulResults.length === 0) {
1185
+ return {
1186
+ ok: false,
1187
+ response: allInstancesFailedResponse(args.c, failedInstances),
1188
+ };
1189
+ }
1190
+
1191
+ return {
1192
+ ok: true,
1193
+ successfulResults,
1194
+ failedInstances,
1195
+ };
1196
+ };
1197
+
1198
+ const fetchPagedFromSelectedInstances = async <T>(args: {
1199
+ c: Context;
1200
+ selectedInstances: ConsoleGatewayInstance[];
1201
+ path: string;
1202
+ query: URLSearchParams;
1203
+ targetCount: number;
1204
+ schema: z.ZodType<ConsolePaginatedResponse<T>>;
1205
+ }): Promise<
1206
+ | {
1207
+ ok: true;
1208
+ successfulResults: Array<{
1209
+ instance: ConsoleGatewayInstance;
1210
+ items: T[];
1211
+ total: number;
1212
+ }>;
1213
+ failedInstances: GatewayFailure[];
1214
+ }
1215
+ | { ok: false; response: Response }
1216
+ > => {
1217
+ const results = await Promise.all(
1218
+ args.selectedInstances.map((instance) =>
1219
+ fetchDownstreamPaged({
1220
+ c: args.c,
1221
+ instance,
1222
+ path: args.path,
1223
+ query: args.query,
1224
+ targetCount: args.targetCount,
1225
+ schema: args.schema,
1226
+ fetchImpl,
1227
+ })
1228
+ )
1229
+ );
1230
+
1231
+ const failedInstances = results
1232
+ .filter(
1233
+ (result): result is { ok: false; failure: GatewayFailure } => !result.ok
1234
+ )
1235
+ .map((result) => result.failure);
1236
+ const successfulResults = results
1237
+ .map((result, index) => ({
1238
+ result,
1239
+ instance: args.selectedInstances[index],
1240
+ }))
1241
+ .filter(
1242
+ (
1243
+ entry
1244
+ ): entry is {
1245
+ result: { ok: true; items: T[]; total: number };
1246
+ instance: ConsoleGatewayInstance;
1247
+ } => Boolean(entry.instance) && entry.result.ok
1248
+ )
1249
+ .map((entry) => ({
1250
+ instance: entry.instance,
1251
+ items: entry.result.items,
1252
+ total: entry.result.total,
1253
+ }));
1254
+
1255
+ if (successfulResults.length === 0) {
1256
+ return {
1257
+ ok: false,
1258
+ response: allInstancesFailedResponse(args.c, failedInstances),
1259
+ };
1260
+ }
1261
+
1262
+ return {
1263
+ ok: true,
1264
+ successfulResults,
1265
+ failedInstances,
1266
+ };
1267
+ };
1268
+
1082
1269
  routes.get(
1083
1270
  '/instances',
1084
1271
  describeRoute({
@@ -1104,18 +1291,15 @@ export function createConsoleGatewayRoutes(
1104
1291
  },
1105
1292
  }),
1106
1293
  async (c) => {
1107
- const auth = await options.authenticate(c);
1108
- if (!auth) {
1109
- return unauthorizedResponse(c);
1110
- }
1111
-
1112
- return c.json({
1113
- items: instances.map((instance) => ({
1114
- instanceId: instance.instanceId,
1115
- label: instance.label ?? instance.instanceId,
1116
- baseUrl: instance.baseUrl,
1117
- enabled: instance.enabled ?? true,
1118
- })),
1294
+ return withGatewayAuth(c, async () => {
1295
+ return c.json({
1296
+ items: instances.map((instance) => ({
1297
+ instanceId: instance.instanceId,
1298
+ label: instance.label ?? instance.instanceId,
1299
+ baseUrl: instance.baseUrl,
1300
+ enabled: instance.enabled ?? true,
1301
+ })),
1302
+ });
1119
1303
  });
1120
1304
  }
1121
1305
  );
@@ -1146,46 +1330,36 @@ export function createConsoleGatewayRoutes(
1146
1330
  }),
1147
1331
  zValidator('query', GatewayInstanceFilterSchema),
1148
1332
  async (c) => {
1149
- const auth = await options.authenticate(c);
1150
- if (!auth) {
1151
- return unauthorizedResponse(c);
1152
- }
1333
+ return withGatewayAuth(c, async () => {
1334
+ const query = c.req.valid('query');
1335
+ const selection = selectTargetInstances(c, query);
1336
+ if (!selection.ok) {
1337
+ return selection.response;
1338
+ }
1153
1339
 
1154
- const query = c.req.valid('query');
1155
- const selectedInstances = selectInstances({ instances, query });
1156
- if (selectedInstances.length === 0) {
1157
- return c.json(
1158
- {
1159
- error: 'NO_INSTANCES_SELECTED',
1160
- message:
1161
- 'No enabled instances matched the provided instance filter.',
1162
- },
1163
- 400
1340
+ const items = await Promise.all(
1341
+ selection.selectedInstances.map((instance) =>
1342
+ checkDownstreamInstanceHealth({
1343
+ c,
1344
+ instance,
1345
+ fetchImpl,
1346
+ })
1347
+ )
1164
1348
  );
1165
- }
1166
-
1167
- const items = await Promise.all(
1168
- selectedInstances.map((instance) =>
1169
- checkDownstreamInstanceHealth({
1170
- c,
1171
- instance,
1172
- fetchImpl,
1173
- })
1174
- )
1175
- );
1176
1349
 
1177
- const failedInstances = items
1178
- .filter((item) => !item.healthy)
1179
- .map((item) => ({
1180
- instanceId: item.instanceId,
1181
- reason: item.reason ?? 'Health probe failed',
1182
- ...(item.status !== undefined ? { status: item.status } : {}),
1183
- }));
1184
-
1185
- return c.json({
1186
- items,
1187
- partial: failedInstances.length > 0,
1188
- failedInstances,
1350
+ const failedInstances = items
1351
+ .filter((item) => !item.healthy)
1352
+ .map((item) => ({
1353
+ instanceId: item.instanceId,
1354
+ reason: item.reason ?? 'Health probe failed',
1355
+ ...(item.status !== undefined ? { status: item.status } : {}),
1356
+ }));
1357
+
1358
+ return c.json({
1359
+ items,
1360
+ partial: failedInstances.length > 0,
1361
+ failedInstances,
1362
+ });
1189
1363
  });
1190
1364
  }
1191
1365
  );
@@ -1209,41 +1383,16 @@ export function createConsoleGatewayRoutes(
1209
1383
  }),
1210
1384
  zValidator('query', GatewaySingleInstanceQuerySchema),
1211
1385
  async (c) => {
1212
- const auth = await options.authenticate(c);
1213
- if (!auth) {
1214
- return unauthorizedResponse(c);
1215
- }
1216
-
1217
- const query = c.req.valid('query');
1218
- const target = resolveSingleInstanceTarget({ instances, query });
1219
- if (!target.ok) {
1220
- return c.json(
1221
- {
1222
- error: target.error,
1223
- message: target.message,
1224
- },
1225
- target.status
1226
- );
1227
- }
1228
-
1229
- const forwardQuery = sanitizeForwardQueryParams(
1230
- new URL(c.req.url).searchParams
1231
- );
1232
- const result = await forwardDownstreamJsonRequest({
1233
- c,
1234
- instance: target.instance,
1235
- method: 'GET',
1236
- path: '/handlers',
1237
- query: forwardQuery,
1238
- responseSchema: GatewayHandlersResponseSchema,
1239
- fetchImpl,
1386
+ return withGatewayAuth(c, async () => {
1387
+ const query = c.req.valid('query');
1388
+ return proxySingleInstanceJsonRequest({
1389
+ c,
1390
+ query,
1391
+ method: 'GET',
1392
+ path: '/handlers',
1393
+ responseSchema: GatewayHandlersResponseSchema,
1394
+ });
1240
1395
  });
1241
-
1242
- if (!result.ok) {
1243
- return jsonResponse(result.body, result.status);
1244
- }
1245
-
1246
- return jsonResponse(result.data, result.status);
1247
1396
  }
1248
1397
  );
1249
1398
 
@@ -1266,41 +1415,16 @@ export function createConsoleGatewayRoutes(
1266
1415
  }),
1267
1416
  zValidator('query', GatewaySingleInstanceQuerySchema),
1268
1417
  async (c) => {
1269
- const auth = await options.authenticate(c);
1270
- if (!auth) {
1271
- return unauthorizedResponse(c);
1272
- }
1273
-
1274
- const query = c.req.valid('query');
1275
- const target = resolveSingleInstanceTarget({ instances, query });
1276
- if (!target.ok) {
1277
- return c.json(
1278
- {
1279
- error: target.error,
1280
- message: target.message,
1281
- },
1282
- target.status
1283
- );
1284
- }
1285
-
1286
- const forwardQuery = sanitizeForwardQueryParams(
1287
- new URL(c.req.url).searchParams
1288
- );
1289
- const result = await forwardDownstreamJsonRequest<ConsolePrunePreview>({
1290
- c,
1291
- instance: target.instance,
1292
- method: 'POST',
1293
- path: '/prune/preview',
1294
- query: forwardQuery,
1295
- responseSchema: ConsolePrunePreviewSchema,
1296
- fetchImpl,
1418
+ return withGatewayAuth(c, async () => {
1419
+ const query = c.req.valid('query');
1420
+ return proxySingleInstanceJsonRequest({
1421
+ c,
1422
+ query,
1423
+ method: 'POST',
1424
+ path: '/prune/preview',
1425
+ responseSchema: ConsolePrunePreviewSchema,
1426
+ });
1297
1427
  });
1298
-
1299
- if (!result.ok) {
1300
- return jsonResponse(result.body, result.status);
1301
- }
1302
-
1303
- return jsonResponse(result.data, result.status);
1304
1428
  }
1305
1429
  );
1306
1430
 
@@ -1323,41 +1447,16 @@ export function createConsoleGatewayRoutes(
1323
1447
  }),
1324
1448
  zValidator('query', GatewaySingleInstanceQuerySchema),
1325
1449
  async (c) => {
1326
- const auth = await options.authenticate(c);
1327
- if (!auth) {
1328
- return unauthorizedResponse(c);
1329
- }
1330
-
1331
- const query = c.req.valid('query');
1332
- const target = resolveSingleInstanceTarget({ instances, query });
1333
- if (!target.ok) {
1334
- return c.json(
1335
- {
1336
- error: target.error,
1337
- message: target.message,
1338
- },
1339
- target.status
1340
- );
1341
- }
1342
-
1343
- const forwardQuery = sanitizeForwardQueryParams(
1344
- new URL(c.req.url).searchParams
1345
- );
1346
- const result = await forwardDownstreamJsonRequest<ConsolePruneResult>({
1347
- c,
1348
- instance: target.instance,
1349
- method: 'POST',
1350
- path: '/prune',
1351
- query: forwardQuery,
1352
- responseSchema: ConsolePruneResultSchema,
1353
- fetchImpl,
1450
+ return withGatewayAuth(c, async () => {
1451
+ const query = c.req.valid('query');
1452
+ return proxySingleInstanceJsonRequest({
1453
+ c,
1454
+ query,
1455
+ method: 'POST',
1456
+ path: '/prune',
1457
+ responseSchema: ConsolePruneResultSchema,
1458
+ });
1354
1459
  });
1355
-
1356
- if (!result.ok) {
1357
- return jsonResponse(result.body, result.status);
1358
- }
1359
-
1360
- return jsonResponse(result.data, result.status);
1361
1460
  }
1362
1461
  );
1363
1462
 
@@ -1380,41 +1479,16 @@ export function createConsoleGatewayRoutes(
1380
1479
  }),
1381
1480
  zValidator('query', GatewaySingleInstanceQuerySchema),
1382
1481
  async (c) => {
1383
- const auth = await options.authenticate(c);
1384
- if (!auth) {
1385
- return unauthorizedResponse(c);
1386
- }
1387
-
1388
- const query = c.req.valid('query');
1389
- const target = resolveSingleInstanceTarget({ instances, query });
1390
- if (!target.ok) {
1391
- return c.json(
1392
- {
1393
- error: target.error,
1394
- message: target.message,
1395
- },
1396
- target.status
1397
- );
1398
- }
1399
-
1400
- const forwardQuery = sanitizeForwardQueryParams(
1401
- new URL(c.req.url).searchParams
1402
- );
1403
- const result = await forwardDownstreamJsonRequest<ConsoleCompactResult>({
1404
- c,
1405
- instance: target.instance,
1406
- method: 'POST',
1407
- path: '/compact',
1408
- query: forwardQuery,
1409
- responseSchema: ConsoleCompactResultSchema,
1410
- fetchImpl,
1482
+ return withGatewayAuth(c, async () => {
1483
+ const query = c.req.valid('query');
1484
+ return proxySingleInstanceJsonRequest({
1485
+ c,
1486
+ query,
1487
+ method: 'POST',
1488
+ path: '/compact',
1489
+ responseSchema: ConsoleCompactResultSchema,
1490
+ });
1411
1491
  });
1412
-
1413
- if (!result.ok) {
1414
- return jsonResponse(result.body, result.status);
1415
- }
1416
-
1417
- return jsonResponse(result.data, result.status);
1418
1492
  }
1419
1493
  );
1420
1494
 
@@ -1438,43 +1512,18 @@ export function createConsoleGatewayRoutes(
1438
1512
  zValidator('query', GatewaySingleInstanceQuerySchema),
1439
1513
  zValidator('json', GatewayNotifyDataChangeRequestSchema),
1440
1514
  async (c) => {
1441
- const auth = await options.authenticate(c);
1442
- if (!auth) {
1443
- return unauthorizedResponse(c);
1444
- }
1445
-
1446
- const query = c.req.valid('query');
1447
- const body = c.req.valid('json');
1448
- const target = resolveSingleInstanceTarget({ instances, query });
1449
- if (!target.ok) {
1450
- return c.json(
1451
- {
1452
- error: target.error,
1453
- message: target.message,
1454
- },
1455
- target.status
1456
- );
1457
- }
1458
-
1459
- const forwardQuery = sanitizeForwardQueryParams(
1460
- new URL(c.req.url).searchParams
1461
- );
1462
- const result = await forwardDownstreamJsonRequest({
1463
- c,
1464
- instance: target.instance,
1465
- method: 'POST',
1466
- path: '/notify-data-change',
1467
- query: forwardQuery,
1468
- body,
1469
- responseSchema: GatewayNotifyDataChangeResponseSchema,
1470
- fetchImpl,
1515
+ return withGatewayAuth(c, async () => {
1516
+ const query = c.req.valid('query');
1517
+ const body = c.req.valid('json');
1518
+ return proxySingleInstanceJsonRequest({
1519
+ c,
1520
+ query,
1521
+ method: 'POST',
1522
+ path: '/notify-data-change',
1523
+ body,
1524
+ responseSchema: GatewayNotifyDataChangeResponseSchema,
1525
+ });
1471
1526
  });
1472
-
1473
- if (!result.ok) {
1474
- return jsonResponse(result.body, result.status);
1475
- }
1476
-
1477
- return jsonResponse(result.data, result.status);
1478
1527
  }
1479
1528
  );
1480
1529
 
@@ -1498,42 +1547,17 @@ export function createConsoleGatewayRoutes(
1498
1547
  zValidator('param', GatewayClientPathParamSchema),
1499
1548
  zValidator('query', GatewaySingleInstancePartitionQuerySchema),
1500
1549
  async (c) => {
1501
- const auth = await options.authenticate(c);
1502
- if (!auth) {
1503
- return unauthorizedResponse(c);
1504
- }
1505
-
1506
- const { id } = c.req.valid('param');
1507
- const query = c.req.valid('query');
1508
- const target = resolveSingleInstanceTarget({ instances, query });
1509
- if (!target.ok) {
1510
- return c.json(
1511
- {
1512
- error: target.error,
1513
- message: target.message,
1514
- },
1515
- target.status
1516
- );
1517
- }
1518
-
1519
- const forwardQuery = sanitizeForwardQueryParams(
1520
- new URL(c.req.url).searchParams
1521
- );
1522
- const result = await forwardDownstreamJsonRequest<ConsoleEvictResult>({
1523
- c,
1524
- instance: target.instance,
1525
- method: 'DELETE',
1526
- path: `/clients/${encodeURIComponent(id)}`,
1527
- query: forwardQuery,
1528
- responseSchema: ConsoleEvictResultSchema,
1529
- fetchImpl,
1550
+ return withGatewayAuth(c, async () => {
1551
+ const { id } = c.req.valid('param');
1552
+ const query = c.req.valid('query');
1553
+ return proxySingleInstanceJsonRequest({
1554
+ c,
1555
+ query,
1556
+ method: 'DELETE',
1557
+ path: `/clients/${encodeURIComponent(id)}`,
1558
+ responseSchema: ConsoleEvictResultSchema,
1559
+ });
1530
1560
  });
1531
-
1532
- if (!result.ok) {
1533
- return jsonResponse(result.body, result.status);
1534
- }
1535
-
1536
- return jsonResponse(result.data, result.status);
1537
1561
  }
1538
1562
  );
1539
1563
 
@@ -1556,42 +1580,16 @@ export function createConsoleGatewayRoutes(
1556
1580
  }),
1557
1581
  zValidator('query', GatewaySingleInstanceQuerySchema),
1558
1582
  async (c) => {
1559
- const auth = await options.authenticate(c);
1560
- if (!auth) {
1561
- return unauthorizedResponse(c);
1562
- }
1563
-
1564
- const query = c.req.valid('query');
1565
- const target = resolveSingleInstanceTarget({ instances, query });
1566
- if (!target.ok) {
1567
- return c.json(
1568
- {
1569
- error: target.error,
1570
- message: target.message,
1571
- },
1572
- target.status
1573
- );
1574
- }
1575
-
1576
- const forwardQuery = sanitizeForwardQueryParams(
1577
- new URL(c.req.url).searchParams
1578
- );
1579
- const result =
1580
- await forwardDownstreamJsonRequest<ConsoleClearEventsResult>({
1583
+ return withGatewayAuth(c, async () => {
1584
+ const query = c.req.valid('query');
1585
+ return proxySingleInstanceJsonRequest({
1581
1586
  c,
1582
- instance: target.instance,
1587
+ query,
1583
1588
  method: 'DELETE',
1584
1589
  path: '/events',
1585
- query: forwardQuery,
1586
1590
  responseSchema: ConsoleClearEventsResultSchema,
1587
- fetchImpl,
1588
1591
  });
1589
-
1590
- if (!result.ok) {
1591
- return jsonResponse(result.body, result.status);
1592
- }
1593
-
1594
- return jsonResponse(result.data, result.status);
1592
+ });
1595
1593
  }
1596
1594
  );
1597
1595
 
@@ -1614,42 +1612,16 @@ export function createConsoleGatewayRoutes(
1614
1612
  }),
1615
1613
  zValidator('query', GatewaySingleInstanceQuerySchema),
1616
1614
  async (c) => {
1617
- const auth = await options.authenticate(c);
1618
- if (!auth) {
1619
- return unauthorizedResponse(c);
1620
- }
1621
-
1622
- const query = c.req.valid('query');
1623
- const target = resolveSingleInstanceTarget({ instances, query });
1624
- if (!target.ok) {
1625
- return c.json(
1626
- {
1627
- error: target.error,
1628
- message: target.message,
1629
- },
1630
- target.status
1631
- );
1632
- }
1633
-
1634
- const forwardQuery = sanitizeForwardQueryParams(
1635
- new URL(c.req.url).searchParams
1636
- );
1637
- const result =
1638
- await forwardDownstreamJsonRequest<ConsolePruneEventsResult>({
1615
+ return withGatewayAuth(c, async () => {
1616
+ const query = c.req.valid('query');
1617
+ return proxySingleInstanceJsonRequest({
1639
1618
  c,
1640
- instance: target.instance,
1619
+ query,
1641
1620
  method: 'POST',
1642
1621
  path: '/events/prune',
1643
- query: forwardQuery,
1644
1622
  responseSchema: ConsolePruneEventsResultSchema,
1645
- fetchImpl,
1646
1623
  });
1647
-
1648
- if (!result.ok) {
1649
- return jsonResponse(result.body, result.status);
1650
- }
1651
-
1652
- return jsonResponse(result.data, result.status);
1624
+ });
1653
1625
  }
1654
1626
  );
1655
1627
 
@@ -1674,43 +1646,18 @@ export function createConsoleGatewayRoutes(
1674
1646
  }),
1675
1647
  zValidator('query', GatewayApiKeysQuerySchema),
1676
1648
  async (c) => {
1677
- const auth = await options.authenticate(c);
1678
- if (!auth) {
1679
- return unauthorizedResponse(c);
1680
- }
1681
-
1682
- const query = c.req.valid('query');
1683
- const target = resolveSingleInstanceTarget({ instances, query });
1684
- if (!target.ok) {
1685
- return c.json(
1686
- {
1687
- error: target.error,
1688
- message: target.message,
1689
- },
1690
- target.status
1691
- );
1692
- }
1693
-
1694
- const forwardQuery = sanitizeForwardQueryParams(
1695
- new URL(c.req.url).searchParams
1696
- );
1697
- const result = await forwardDownstreamJsonRequest<
1698
- ConsolePaginatedResponse<ConsoleApiKey>
1699
- >({
1700
- c,
1701
- instance: target.instance,
1702
- method: 'GET',
1703
- path: '/api-keys',
1704
- query: forwardQuery,
1705
- responseSchema: ConsolePaginatedResponseSchema(ConsoleApiKeySchema),
1706
- fetchImpl,
1649
+ return withGatewayAuth(c, async () => {
1650
+ const query = c.req.valid('query');
1651
+ return proxySingleInstanceJsonRequest<
1652
+ ConsolePaginatedResponse<ConsoleApiKey>
1653
+ >({
1654
+ c,
1655
+ query,
1656
+ method: 'GET',
1657
+ path: '/api-keys',
1658
+ responseSchema: ConsolePaginatedResponseSchema(ConsoleApiKeySchema),
1659
+ });
1707
1660
  });
1708
-
1709
- if (!result.ok) {
1710
- return jsonResponse(result.body, result.status);
1711
- }
1712
-
1713
- return jsonResponse(result.data, result.status);
1714
1661
  }
1715
1662
  );
1716
1663
 
@@ -1734,44 +1681,18 @@ export function createConsoleGatewayRoutes(
1734
1681
  zValidator('query', GatewaySingleInstanceQuerySchema),
1735
1682
  zValidator('json', ConsoleApiKeyCreateRequestSchema),
1736
1683
  async (c) => {
1737
- const auth = await options.authenticate(c);
1738
- if (!auth) {
1739
- return unauthorizedResponse(c);
1740
- }
1741
-
1742
- const query = c.req.valid('query');
1743
- const body = c.req.valid('json');
1744
- const target = resolveSingleInstanceTarget({ instances, query });
1745
- if (!target.ok) {
1746
- return c.json(
1747
- {
1748
- error: target.error,
1749
- message: target.message,
1750
- },
1751
- target.status
1752
- );
1753
- }
1754
-
1755
- const forwardQuery = sanitizeForwardQueryParams(
1756
- new URL(c.req.url).searchParams
1757
- );
1758
- const result =
1759
- await forwardDownstreamJsonRequest<ConsoleApiKeyCreateResponse>({
1684
+ return withGatewayAuth(c, async () => {
1685
+ const query = c.req.valid('query');
1686
+ const body = c.req.valid('json');
1687
+ return proxySingleInstanceJsonRequest<ConsoleApiKeyCreateResponse>({
1760
1688
  c,
1761
- instance: target.instance,
1689
+ query,
1762
1690
  method: 'POST',
1763
1691
  path: '/api-keys',
1764
- query: forwardQuery,
1765
1692
  body,
1766
1693
  responseSchema: ConsoleApiKeyCreateResponseSchema,
1767
- fetchImpl,
1768
1694
  });
1769
-
1770
- if (!result.ok) {
1771
- return jsonResponse(result.body, result.status);
1772
- }
1773
-
1774
- return jsonResponse(result.data, result.status);
1695
+ });
1775
1696
  }
1776
1697
  );
1777
1698
 
@@ -1795,42 +1716,17 @@ export function createConsoleGatewayRoutes(
1795
1716
  zValidator('param', GatewayApiKeyPathParamSchema),
1796
1717
  zValidator('query', GatewaySingleInstanceQuerySchema),
1797
1718
  async (c) => {
1798
- const auth = await options.authenticate(c);
1799
- if (!auth) {
1800
- return unauthorizedResponse(c);
1801
- }
1802
-
1803
- const { id } = c.req.valid('param');
1804
- const query = c.req.valid('query');
1805
- const target = resolveSingleInstanceTarget({ instances, query });
1806
- if (!target.ok) {
1807
- return c.json(
1808
- {
1809
- error: target.error,
1810
- message: target.message,
1811
- },
1812
- target.status
1813
- );
1814
- }
1815
-
1816
- const forwardQuery = sanitizeForwardQueryParams(
1817
- new URL(c.req.url).searchParams
1818
- );
1819
- const result = await forwardDownstreamJsonRequest<ConsoleApiKey>({
1820
- c,
1821
- instance: target.instance,
1822
- method: 'GET',
1823
- path: `/api-keys/${encodeURIComponent(id)}`,
1824
- query: forwardQuery,
1825
- responseSchema: ConsoleApiKeySchema,
1826
- fetchImpl,
1719
+ return withGatewayAuth(c, async () => {
1720
+ const { id } = c.req.valid('param');
1721
+ const query = c.req.valid('query');
1722
+ return proxySingleInstanceJsonRequest<ConsoleApiKey>({
1723
+ c,
1724
+ query,
1725
+ method: 'GET',
1726
+ path: `/api-keys/${encodeURIComponent(id)}`,
1727
+ responseSchema: ConsoleApiKeySchema,
1728
+ });
1827
1729
  });
1828
-
1829
- if (!result.ok) {
1830
- return jsonResponse(result.body, result.status);
1831
- }
1832
-
1833
- return jsonResponse(result.data, result.status);
1834
1730
  }
1835
1731
  );
1836
1732
 
@@ -1854,42 +1750,17 @@ export function createConsoleGatewayRoutes(
1854
1750
  zValidator('param', GatewayApiKeyPathParamSchema),
1855
1751
  zValidator('query', GatewaySingleInstanceQuerySchema),
1856
1752
  async (c) => {
1857
- const auth = await options.authenticate(c);
1858
- if (!auth) {
1859
- return unauthorizedResponse(c);
1860
- }
1861
-
1862
- const { id } = c.req.valid('param');
1863
- const query = c.req.valid('query');
1864
- const target = resolveSingleInstanceTarget({ instances, query });
1865
- if (!target.ok) {
1866
- return c.json(
1867
- {
1868
- error: target.error,
1869
- message: target.message,
1870
- },
1871
- target.status
1872
- );
1873
- }
1874
-
1875
- const forwardQuery = sanitizeForwardQueryParams(
1876
- new URL(c.req.url).searchParams
1877
- );
1878
- const result = await forwardDownstreamJsonRequest<{ revoked: boolean }>({
1879
- c,
1880
- instance: target.instance,
1881
- method: 'DELETE',
1882
- path: `/api-keys/${encodeURIComponent(id)}`,
1883
- query: forwardQuery,
1884
- responseSchema: ConsoleApiKeyRevokeResponseSchema,
1885
- fetchImpl,
1753
+ return withGatewayAuth(c, async () => {
1754
+ const { id } = c.req.valid('param');
1755
+ const query = c.req.valid('query');
1756
+ return proxySingleInstanceJsonRequest<{ revoked: boolean }>({
1757
+ c,
1758
+ query,
1759
+ method: 'DELETE',
1760
+ path: `/api-keys/${encodeURIComponent(id)}`,
1761
+ responseSchema: ConsoleApiKeyRevokeResponseSchema,
1762
+ });
1886
1763
  });
1887
-
1888
- if (!result.ok) {
1889
- return jsonResponse(result.body, result.status);
1890
- }
1891
-
1892
- return jsonResponse(result.data, result.status);
1893
1764
  }
1894
1765
  );
1895
1766
 
@@ -1913,44 +1784,18 @@ export function createConsoleGatewayRoutes(
1913
1784
  zValidator('query', GatewaySingleInstanceQuerySchema),
1914
1785
  zValidator('json', ConsoleApiKeyBulkRevokeRequestSchema),
1915
1786
  async (c) => {
1916
- const auth = await options.authenticate(c);
1917
- if (!auth) {
1918
- return unauthorizedResponse(c);
1919
- }
1920
-
1921
- const query = c.req.valid('query');
1922
- const body = c.req.valid('json');
1923
- const target = resolveSingleInstanceTarget({ instances, query });
1924
- if (!target.ok) {
1925
- return c.json(
1926
- {
1927
- error: target.error,
1928
- message: target.message,
1929
- },
1930
- target.status
1931
- );
1932
- }
1933
-
1934
- const forwardQuery = sanitizeForwardQueryParams(
1935
- new URL(c.req.url).searchParams
1936
- );
1937
- const result =
1938
- await forwardDownstreamJsonRequest<ConsoleApiKeyBulkRevokeResponse>({
1787
+ return withGatewayAuth(c, async () => {
1788
+ const query = c.req.valid('query');
1789
+ const body = c.req.valid('json');
1790
+ return proxySingleInstanceJsonRequest<ConsoleApiKeyBulkRevokeResponse>({
1939
1791
  c,
1940
- instance: target.instance,
1792
+ query,
1941
1793
  method: 'POST',
1942
1794
  path: '/api-keys/bulk-revoke',
1943
- query: forwardQuery,
1944
1795
  body,
1945
1796
  responseSchema: ConsoleApiKeyBulkRevokeResponseSchema,
1946
- fetchImpl,
1947
1797
  });
1948
-
1949
- if (!result.ok) {
1950
- return jsonResponse(result.body, result.status);
1951
- }
1952
-
1953
- return jsonResponse(result.data, result.status);
1798
+ });
1954
1799
  }
1955
1800
  );
1956
1801
 
@@ -1974,43 +1819,17 @@ export function createConsoleGatewayRoutes(
1974
1819
  zValidator('param', GatewayApiKeyPathParamSchema),
1975
1820
  zValidator('query', GatewaySingleInstanceQuerySchema),
1976
1821
  async (c) => {
1977
- const auth = await options.authenticate(c);
1978
- if (!auth) {
1979
- return unauthorizedResponse(c);
1980
- }
1981
-
1982
- const { id } = c.req.valid('param');
1983
- const query = c.req.valid('query');
1984
- const target = resolveSingleInstanceTarget({ instances, query });
1985
- if (!target.ok) {
1986
- return c.json(
1987
- {
1988
- error: target.error,
1989
- message: target.message,
1990
- },
1991
- target.status
1992
- );
1993
- }
1994
-
1995
- const forwardQuery = sanitizeForwardQueryParams(
1996
- new URL(c.req.url).searchParams
1997
- );
1998
- const result =
1999
- await forwardDownstreamJsonRequest<ConsoleApiKeyCreateResponse>({
1822
+ return withGatewayAuth(c, async () => {
1823
+ const { id } = c.req.valid('param');
1824
+ const query = c.req.valid('query');
1825
+ return proxySingleInstanceJsonRequest<ConsoleApiKeyCreateResponse>({
2000
1826
  c,
2001
- instance: target.instance,
1827
+ query,
2002
1828
  method: 'POST',
2003
1829
  path: `/api-keys/${encodeURIComponent(id)}/rotate/stage`,
2004
- query: forwardQuery,
2005
1830
  responseSchema: ConsoleApiKeyCreateResponseSchema,
2006
- fetchImpl,
2007
1831
  });
2008
-
2009
- if (!result.ok) {
2010
- return jsonResponse(result.body, result.status);
2011
- }
2012
-
2013
- return jsonResponse(result.data, result.status);
1832
+ });
2014
1833
  }
2015
1834
  );
2016
1835
 
@@ -2034,43 +1853,17 @@ export function createConsoleGatewayRoutes(
2034
1853
  zValidator('param', GatewayApiKeyPathParamSchema),
2035
1854
  zValidator('query', GatewaySingleInstanceQuerySchema),
2036
1855
  async (c) => {
2037
- const auth = await options.authenticate(c);
2038
- if (!auth) {
2039
- return unauthorizedResponse(c);
2040
- }
2041
-
2042
- const { id } = c.req.valid('param');
2043
- const query = c.req.valid('query');
2044
- const target = resolveSingleInstanceTarget({ instances, query });
2045
- if (!target.ok) {
2046
- return c.json(
2047
- {
2048
- error: target.error,
2049
- message: target.message,
2050
- },
2051
- target.status
2052
- );
2053
- }
2054
-
2055
- const forwardQuery = sanitizeForwardQueryParams(
2056
- new URL(c.req.url).searchParams
2057
- );
2058
- const result =
2059
- await forwardDownstreamJsonRequest<ConsoleApiKeyCreateResponse>({
1856
+ return withGatewayAuth(c, async () => {
1857
+ const { id } = c.req.valid('param');
1858
+ const query = c.req.valid('query');
1859
+ return proxySingleInstanceJsonRequest<ConsoleApiKeyCreateResponse>({
2060
1860
  c,
2061
- instance: target.instance,
1861
+ query,
2062
1862
  method: 'POST',
2063
1863
  path: `/api-keys/${encodeURIComponent(id)}/rotate`,
2064
- query: forwardQuery,
2065
1864
  responseSchema: ConsoleApiKeyCreateResponseSchema,
2066
- fetchImpl,
2067
1865
  });
2068
-
2069
- if (!result.ok) {
2070
- return jsonResponse(result.body, result.status);
2071
- }
2072
-
2073
- return jsonResponse(result.data, result.status);
1866
+ });
2074
1867
  }
2075
1868
  );
2076
1869
 
@@ -2092,96 +1885,65 @@ export function createConsoleGatewayRoutes(
2092
1885
  }),
2093
1886
  zValidator('query', GatewayStatsQuerySchema),
2094
1887
  async (c) => {
2095
- const auth = await options.authenticate(c);
2096
- if (!auth) {
2097
- return unauthorizedResponse(c);
2098
- }
1888
+ return withGatewayAuth(c, async () => {
1889
+ const query = c.req.valid('query');
1890
+ const selection = selectTargetInstances(c, query);
1891
+ if (!selection.ok) {
1892
+ return selection.response;
1893
+ }
2099
1894
 
2100
- const query = c.req.valid('query');
2101
- const selectedInstances = selectInstances({ instances, query });
2102
- if (selectedInstances.length === 0) {
2103
- return c.json(
2104
- {
2105
- error: 'NO_INSTANCES_SELECTED',
2106
- message:
2107
- 'No enabled instances matched the provided instance filter.',
2108
- },
2109
- 400
1895
+ const forwardQuery = sanitizeForwardQueryParams(
1896
+ new URL(c.req.url).searchParams
2110
1897
  );
2111
- }
2112
-
2113
- const forwardQuery = sanitizeForwardQueryParams(
2114
- new URL(c.req.url).searchParams
2115
- );
2116
-
2117
- const results = await Promise.all(
2118
- selectedInstances.map((instance) =>
2119
- fetchDownstreamJson({
2120
- c,
2121
- instance,
2122
- path: '/stats',
2123
- query: forwardQuery,
2124
- schema: SyncStatsSchema,
2125
- fetchImpl,
2126
- })
2127
- )
2128
- );
2129
-
2130
- const failedInstances = results
2131
- .filter(
2132
- (result): result is { ok: false; failure: GatewayFailure } =>
2133
- !result.ok
2134
- )
2135
- .map((result) => result.failure);
2136
- const successfulResults = results.filter(
2137
- (result): result is { ok: true; data: SyncStats } => result.ok
2138
- );
2139
-
2140
- if (successfulResults.length === 0) {
2141
- return allInstancesFailedResponse(c, failedInstances);
2142
- }
1898
+ const fetched = await fetchFromSelectedInstances<SyncStats>({
1899
+ c,
1900
+ selectedInstances: selection.selectedInstances,
1901
+ path: '/stats',
1902
+ query: forwardQuery,
1903
+ schema: SyncStatsSchema,
1904
+ });
1905
+ if (!fetched.ok) {
1906
+ return fetched.response;
1907
+ }
2143
1908
 
2144
- const statsByInstance = new Map<string, SyncStats>();
2145
- for (let i = 0; i < selectedInstances.length; i++) {
2146
- const result = results[i];
2147
- if (!result || !result.ok) continue;
2148
- const instance = selectedInstances[i];
2149
- if (!instance) continue;
2150
- statsByInstance.set(instance.instanceId, result.data);
2151
- }
1909
+ const statsByInstance = new Map<string, SyncStats>();
1910
+ for (const result of fetched.successfulResults) {
1911
+ statsByInstance.set(result.instance.instanceId, result.data);
1912
+ }
2152
1913
 
2153
- const statsValues = Array.from(statsByInstance.values());
2154
- const sum = (selector: (stats: SyncStats) => number): number =>
2155
- statsValues.reduce((acc, stats) => acc + selector(stats), 0);
1914
+ const statsValues = Array.from(statsByInstance.values());
1915
+ const sum = (selector: (stats: SyncStats) => number): number =>
1916
+ statsValues.reduce((acc, stats) => acc + selector(stats), 0);
2156
1917
 
2157
- const minCommitSeqByInstance: Record<string, number> = {};
2158
- const maxCommitSeqByInstance: Record<string, number> = {};
2159
- for (const [instanceId, stats] of statsByInstance.entries()) {
2160
- minCommitSeqByInstance[instanceId] = stats.minCommitSeq;
2161
- maxCommitSeqByInstance[instanceId] = stats.maxCommitSeq;
2162
- }
1918
+ const minCommitSeqByInstance: Record<string, number> = {};
1919
+ const maxCommitSeqByInstance: Record<string, number> = {};
1920
+ for (const [instanceId, stats] of statsByInstance.entries()) {
1921
+ minCommitSeqByInstance[instanceId] = stats.minCommitSeq;
1922
+ maxCommitSeqByInstance[instanceId] = stats.maxCommitSeq;
1923
+ }
2163
1924
 
2164
- return c.json({
2165
- commitCount: sum((stats) => stats.commitCount),
2166
- changeCount: sum((stats) => stats.changeCount),
2167
- minCommitSeq: Math.min(
2168
- ...statsValues.map((stats) => stats.minCommitSeq)
2169
- ),
2170
- maxCommitSeq: Math.max(
2171
- ...statsValues.map((stats) => stats.maxCommitSeq)
2172
- ),
2173
- clientCount: sum((stats) => stats.clientCount),
2174
- activeClientCount: sum((stats) => stats.activeClientCount),
2175
- minActiveClientCursor: minNullable(
2176
- statsValues.map((stats) => stats.minActiveClientCursor)
2177
- ),
2178
- maxActiveClientCursor: maxNullable(
2179
- statsValues.map((stats) => stats.maxActiveClientCursor)
2180
- ),
2181
- minCommitSeqByInstance,
2182
- maxCommitSeqByInstance,
2183
- partial: failedInstances.length > 0,
2184
- failedInstances,
1925
+ return c.json({
1926
+ commitCount: sum((stats) => stats.commitCount),
1927
+ changeCount: sum((stats) => stats.changeCount),
1928
+ minCommitSeq: Math.min(
1929
+ ...statsValues.map((stats) => stats.minCommitSeq)
1930
+ ),
1931
+ maxCommitSeq: Math.max(
1932
+ ...statsValues.map((stats) => stats.maxCommitSeq)
1933
+ ),
1934
+ clientCount: sum((stats) => stats.clientCount),
1935
+ activeClientCount: sum((stats) => stats.activeClientCount),
1936
+ minActiveClientCursor: minNullable(
1937
+ statsValues.map((stats) => stats.minActiveClientCursor)
1938
+ ),
1939
+ maxActiveClientCursor: maxNullable(
1940
+ statsValues.map((stats) => stats.maxActiveClientCursor)
1941
+ ),
1942
+ minCommitSeqByInstance,
1943
+ maxCommitSeqByInstance,
1944
+ partial: fetched.failedInstances.length > 0,
1945
+ failedInstances: fetched.failedInstances,
1946
+ });
2185
1947
  });
2186
1948
  }
2187
1949
  );
@@ -2204,64 +1966,37 @@ export function createConsoleGatewayRoutes(
2204
1966
  }),
2205
1967
  zValidator('query', GatewayTimeseriesQuerySchema),
2206
1968
  async (c) => {
2207
- const auth = await options.authenticate(c);
2208
- if (!auth) {
2209
- return unauthorizedResponse(c);
2210
- }
1969
+ return withGatewayAuth(c, async () => {
1970
+ const query = c.req.valid('query');
1971
+ const selection = selectTargetInstances(c, query);
1972
+ if (!selection.ok) {
1973
+ return selection.response;
1974
+ }
2211
1975
 
2212
- const query = c.req.valid('query');
2213
- const selectedInstances = selectInstances({ instances, query });
2214
- if (selectedInstances.length === 0) {
2215
- return c.json(
2216
- {
2217
- error: 'NO_INSTANCES_SELECTED',
2218
- message:
2219
- 'No enabled instances matched the provided instance filter.',
2220
- },
2221
- 400
1976
+ const forwardQuery = sanitizeForwardQueryParams(
1977
+ new URL(c.req.url).searchParams
2222
1978
  );
2223
- }
2224
-
2225
- const forwardQuery = sanitizeForwardQueryParams(
2226
- new URL(c.req.url).searchParams
2227
- );
2228
-
2229
- const results = await Promise.all(
2230
- selectedInstances.map((instance) =>
2231
- fetchDownstreamJson({
1979
+ const fetched =
1980
+ await fetchFromSelectedInstances<TimeseriesStatsResponse>({
2232
1981
  c,
2233
- instance,
1982
+ selectedInstances: selection.selectedInstances,
2234
1983
  path: '/stats/timeseries',
2235
1984
  query: forwardQuery,
2236
1985
  schema: TimeseriesStatsResponseSchema,
2237
- fetchImpl,
2238
- })
2239
- )
2240
- );
2241
-
2242
- const failedInstances = results
2243
- .filter(
2244
- (result): result is { ok: false; failure: GatewayFailure } =>
2245
- !result.ok
2246
- )
2247
- .map((result) => result.failure);
2248
- const successfulResults = results.filter(
2249
- (result): result is { ok: true; data: TimeseriesStatsResponse } =>
2250
- result.ok
2251
- );
2252
-
2253
- if (successfulResults.length === 0) {
2254
- return allInstancesFailedResponse(c, failedInstances);
2255
- }
1986
+ });
1987
+ if (!fetched.ok) {
1988
+ return fetched.response;
1989
+ }
2256
1990
 
2257
- return c.json({
2258
- buckets: mergeTimeseriesBuckets(
2259
- successfulResults.map((result) => result.data)
2260
- ),
2261
- interval: query.interval,
2262
- range: query.range,
2263
- partial: failedInstances.length > 0,
2264
- failedInstances,
1991
+ return c.json({
1992
+ buckets: mergeTimeseriesBuckets(
1993
+ fetched.successfulResults.map((result) => result.data)
1994
+ ),
1995
+ interval: query.interval,
1996
+ range: query.range,
1997
+ partial: fetched.failedInstances.length > 0,
1998
+ failedInstances: fetched.failedInstances,
1999
+ });
2265
2000
  });
2266
2001
  }
2267
2002
  );
@@ -2284,66 +2019,38 @@ export function createConsoleGatewayRoutes(
2284
2019
  }),
2285
2020
  zValidator('query', GatewayLatencyQuerySchema),
2286
2021
  async (c) => {
2287
- const auth = await options.authenticate(c);
2288
- if (!auth) {
2289
- return unauthorizedResponse(c);
2290
- }
2022
+ return withGatewayAuth(c, async () => {
2023
+ const query = c.req.valid('query');
2024
+ const selection = selectTargetInstances(c, query);
2025
+ if (!selection.ok) {
2026
+ return selection.response;
2027
+ }
2291
2028
 
2292
- const query = c.req.valid('query');
2293
- const selectedInstances = selectInstances({ instances, query });
2294
- if (selectedInstances.length === 0) {
2295
- return c.json(
2296
- {
2297
- error: 'NO_INSTANCES_SELECTED',
2298
- message:
2299
- 'No enabled instances matched the provided instance filter.',
2300
- },
2301
- 400
2029
+ const forwardQuery = sanitizeForwardQueryParams(
2030
+ new URL(c.req.url).searchParams
2302
2031
  );
2303
- }
2304
-
2305
- const forwardQuery = sanitizeForwardQueryParams(
2306
- new URL(c.req.url).searchParams
2307
- );
2308
-
2309
- const results = await Promise.all(
2310
- selectedInstances.map((instance) =>
2311
- fetchDownstreamJson({
2312
- c,
2313
- instance,
2314
- path: '/stats/latency',
2315
- query: forwardQuery,
2316
- schema: LatencyStatsResponseSchema,
2317
- fetchImpl,
2318
- })
2319
- )
2320
- );
2321
-
2322
- const failedInstances = results
2323
- .filter(
2324
- (result): result is { ok: false; failure: GatewayFailure } =>
2325
- !result.ok
2326
- )
2327
- .map((result) => result.failure);
2328
- const successfulResults = results.filter(
2329
- (result): result is { ok: true; data: LatencyStatsResponse } =>
2330
- result.ok
2331
- );
2332
-
2333
- if (successfulResults.length === 0) {
2334
- return allInstancesFailedResponse(c, failedInstances);
2335
- }
2032
+ const fetched = await fetchFromSelectedInstances<LatencyStatsResponse>({
2033
+ c,
2034
+ selectedInstances: selection.selectedInstances,
2035
+ path: '/stats/latency',
2036
+ query: forwardQuery,
2037
+ schema: LatencyStatsResponseSchema,
2038
+ });
2039
+ if (!fetched.ok) {
2040
+ return fetched.response;
2041
+ }
2336
2042
 
2337
- return c.json({
2338
- push: averagePercentiles(
2339
- successfulResults.map((result) => result.data.push)
2340
- ),
2341
- pull: averagePercentiles(
2342
- successfulResults.map((result) => result.data.pull)
2343
- ),
2344
- range: query.range,
2345
- partial: failedInstances.length > 0,
2346
- failedInstances,
2043
+ return c.json({
2044
+ push: averagePercentiles(
2045
+ fetched.successfulResults.map((result) => result.data.push)
2046
+ ),
2047
+ pull: averagePercentiles(
2048
+ fetched.successfulResults.map((result) => result.data.pull)
2049
+ ),
2050
+ range: query.range,
2051
+ partial: fetched.failedInstances.length > 0,
2052
+ failedInstances: fetched.failedInstances,
2053
+ });
2347
2054
  });
2348
2055
  }
2349
2056
  );
@@ -2368,95 +2075,62 @@ export function createConsoleGatewayRoutes(
2368
2075
  }),
2369
2076
  zValidator('query', GatewayPaginatedQuerySchema),
2370
2077
  async (c) => {
2371
- const auth = await options.authenticate(c);
2372
- if (!auth) {
2373
- return unauthorizedResponse(c);
2374
- }
2078
+ return withGatewayAuth(c, async () => {
2079
+ const query = c.req.valid('query');
2080
+ const selection = selectTargetInstances(c, query);
2081
+ if (!selection.ok) {
2082
+ return selection.response;
2083
+ }
2375
2084
 
2376
- const query = c.req.valid('query');
2377
- const selectedInstances = selectInstances({ instances, query });
2378
- if (selectedInstances.length === 0) {
2379
- return c.json(
2380
- {
2381
- error: 'NO_INSTANCES_SELECTED',
2382
- message:
2383
- 'No enabled instances matched the provided instance filter.',
2384
- },
2385
- 400
2085
+ const targetCount = query.offset + query.limit;
2086
+ const forwardQuery = sanitizeForwardQueryParams(
2087
+ new URL(c.req.url).searchParams
2386
2088
  );
2387
- }
2388
-
2389
- const targetCount = query.offset + query.limit;
2390
- const forwardQuery = sanitizeForwardQueryParams(
2391
- new URL(c.req.url).searchParams
2392
- );
2393
- forwardQuery.delete('limit');
2394
- forwardQuery.delete('offset');
2395
- const pageSchema = ConsolePaginatedResponseSchema(
2396
- ConsoleCommitListItemSchema
2397
- );
2398
-
2399
- const results = await Promise.all(
2400
- selectedInstances.map((instance) =>
2401
- fetchDownstreamPaged({
2089
+ forwardQuery.delete('limit');
2090
+ forwardQuery.delete('offset');
2091
+ const pageSchema = ConsolePaginatedResponseSchema(
2092
+ ConsoleCommitListItemSchema
2093
+ );
2094
+ const fetched =
2095
+ await fetchPagedFromSelectedInstances<ConsoleCommitListItem>({
2402
2096
  c,
2403
- instance,
2097
+ selectedInstances: selection.selectedInstances,
2404
2098
  path: '/commits',
2405
2099
  query: forwardQuery,
2406
2100
  targetCount,
2407
2101
  schema: pageSchema,
2408
- fetchImpl,
2409
- })
2410
- )
2411
- );
2412
-
2413
- const failedInstances = results
2414
- .filter(
2415
- (result): result is { ok: false; failure: GatewayFailure } =>
2416
- !result.ok
2417
- )
2418
- .map((result) => result.failure);
2419
- const successful = results
2420
- .map((result, index) => ({
2421
- result,
2422
- instance: selectedInstances[index],
2423
- }))
2424
- .filter(
2425
- (
2426
- entry
2427
- ): entry is {
2428
- result: { ok: true; items: ConsoleCommitListItem[]; total: number };
2429
- instance: ConsoleGatewayInstance;
2430
- } => Boolean(entry.instance) && entry.result.ok
2431
- );
2432
-
2433
- if (successful.length === 0) {
2434
- return allInstancesFailedResponse(c, failedInstances);
2435
- }
2102
+ });
2103
+ if (!fetched.ok) {
2104
+ return fetched.response;
2105
+ }
2436
2106
 
2437
- const merged = successful
2438
- .flatMap(({ result, instance }) =>
2439
- result.items.map((commit) => ({
2440
- ...commit,
2441
- instanceId: instance.instanceId,
2442
- federatedCommitId: `${instance.instanceId}:${commit.commitSeq}`,
2443
- }))
2444
- )
2445
- .sort((a, b) => {
2446
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2447
- if (byTime !== 0) return byTime;
2448
- const byInstance = a.instanceId.localeCompare(b.instanceId);
2449
- if (byInstance !== 0) return byInstance;
2450
- return b.commitSeq - a.commitSeq;
2107
+ const merged = fetched.successfulResults
2108
+ .flatMap(({ items, instance }) =>
2109
+ items.map((commit) => ({
2110
+ ...commit,
2111
+ instanceId: instance.instanceId,
2112
+ federatedCommitId: `${instance.instanceId}:${commit.commitSeq}`,
2113
+ }))
2114
+ )
2115
+ .sort((a, b) => {
2116
+ const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2117
+ if (byTime !== 0) return byTime;
2118
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
2119
+ if (byInstance !== 0) return byInstance;
2120
+ return b.commitSeq - a.commitSeq;
2121
+ });
2122
+
2123
+ return c.json({
2124
+ items: merged.slice(query.offset, query.offset + query.limit),
2125
+ total: fetched.successfulResults.reduce(
2126
+ (acc, entry) => acc + entry.total,
2127
+ 0
2128
+ ),
2129
+ offset: query.offset,
2130
+ limit: query.limit,
2131
+ partial: fetched.failedInstances.length > 0,
2132
+ failedInstances: fetched.failedInstances,
2451
2133
  });
2452
-
2453
- return c.json({
2454
- items: merged.slice(query.offset, query.offset + query.limit),
2455
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
2456
- offset: query.offset,
2457
- limit: query.limit,
2458
- partial: failedInstances.length > 0,
2459
- failedInstances,
2460
2134
  });
2461
2135
  }
2462
2136
  );
@@ -2483,54 +2157,51 @@ export function createConsoleGatewayRoutes(
2483
2157
  ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)
2484
2158
  ),
2485
2159
  async (c) => {
2486
- const auth = await options.authenticate(c);
2487
- if (!auth) {
2488
- return unauthorizedResponse(c);
2489
- }
2160
+ return withGatewayAuth(c, async () => {
2161
+ const { seq } = c.req.valid('param');
2162
+ const query = c.req.valid('query');
2163
+ const target = resolveCommitTarget({ seq, instances, query });
2164
+ if (!target.ok) {
2165
+ return c.json(
2166
+ {
2167
+ error: target.error,
2168
+ ...(target.message ? { message: target.message } : {}),
2169
+ },
2170
+ target.status
2171
+ );
2172
+ }
2490
2173
 
2491
- const { seq } = c.req.valid('param');
2492
- const query = c.req.valid('query');
2493
- const target = resolveCommitTarget({ seq, instances, query });
2494
- if (!target.ok) {
2495
- return c.json(
2496
- {
2497
- error: target.error,
2498
- ...(target.message ? { message: target.message } : {}),
2499
- },
2500
- target.status
2174
+ const forwardQuery = sanitizeForwardQueryParams(
2175
+ new URL(c.req.url).searchParams
2501
2176
  );
2502
- }
2503
-
2504
- const forwardQuery = sanitizeForwardQueryParams(
2505
- new URL(c.req.url).searchParams
2506
- );
2507
- const result = await fetchDownstreamJson({
2508
- c,
2509
- instance: target.instance,
2510
- path: `/commits/${target.localCommitSeq}`,
2511
- query: forwardQuery,
2512
- schema: ConsoleCommitDetailSchema,
2513
- fetchImpl,
2514
- });
2177
+ const result = await fetchDownstreamJson({
2178
+ c,
2179
+ instance: target.instance,
2180
+ path: `/commits/${target.localCommitSeq}`,
2181
+ query: forwardQuery,
2182
+ schema: ConsoleCommitDetailSchema,
2183
+ fetchImpl,
2184
+ });
2515
2185
 
2516
- if (!result.ok) {
2517
- if (result.failure.status === 404) {
2518
- return c.json({ error: 'NOT_FOUND' }, 404);
2186
+ if (!result.ok) {
2187
+ if (result.failure.status === 404) {
2188
+ return c.json({ error: 'NOT_FOUND' }, 404);
2189
+ }
2190
+ return c.json(
2191
+ {
2192
+ error: 'DOWNSTREAM_UNAVAILABLE',
2193
+ failedInstances: [result.failure],
2194
+ },
2195
+ 502
2196
+ );
2519
2197
  }
2520
- return c.json(
2521
- {
2522
- error: 'DOWNSTREAM_UNAVAILABLE',
2523
- failedInstances: [result.failure],
2524
- },
2525
- 502
2526
- );
2527
- }
2528
2198
 
2529
- return c.json({
2530
- ...result.data,
2531
- instanceId: target.instance.instanceId,
2532
- federatedCommitId: `${target.instance.instanceId}:${result.data.commitSeq}`,
2533
- localCommitSeq: result.data.commitSeq,
2199
+ return c.json({
2200
+ ...result.data,
2201
+ instanceId: target.instance.instanceId,
2202
+ federatedCommitId: `${target.instance.instanceId}:${result.data.commitSeq}`,
2203
+ localCommitSeq: result.data.commitSeq,
2204
+ });
2534
2205
  });
2535
2206
  }
2536
2207
  );
@@ -2555,93 +2226,59 @@ export function createConsoleGatewayRoutes(
2555
2226
  }),
2556
2227
  zValidator('query', GatewayPaginatedQuerySchema),
2557
2228
  async (c) => {
2558
- const auth = await options.authenticate(c);
2559
- if (!auth) {
2560
- return unauthorizedResponse(c);
2561
- }
2562
-
2563
- const query = c.req.valid('query');
2564
- const selectedInstances = selectInstances({ instances, query });
2565
- if (selectedInstances.length === 0) {
2566
- return c.json(
2567
- {
2568
- error: 'NO_INSTANCES_SELECTED',
2569
- message:
2570
- 'No enabled instances matched the provided instance filter.',
2571
- },
2572
- 400
2573
- );
2574
- }
2575
-
2576
- const targetCount = query.offset + query.limit;
2577
- const forwardQuery = sanitizeForwardQueryParams(
2578
- new URL(c.req.url).searchParams
2579
- );
2580
- forwardQuery.delete('limit');
2581
- forwardQuery.delete('offset');
2582
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleClientSchema);
2583
-
2584
- const results = await Promise.all(
2585
- selectedInstances.map((instance) =>
2586
- fetchDownstreamPaged({
2587
- c,
2588
- instance,
2589
- path: '/clients',
2590
- query: forwardQuery,
2591
- targetCount,
2592
- schema: pageSchema,
2593
- fetchImpl,
2594
- })
2595
- )
2596
- );
2229
+ return withGatewayAuth(c, async () => {
2230
+ const query = c.req.valid('query');
2231
+ const selection = selectTargetInstances(c, query);
2232
+ if (!selection.ok) {
2233
+ return selection.response;
2234
+ }
2597
2235
 
2598
- const failedInstances = results
2599
- .filter(
2600
- (result): result is { ok: false; failure: GatewayFailure } =>
2601
- !result.ok
2602
- )
2603
- .map((result) => result.failure);
2604
- const successful = results
2605
- .map((result, index) => ({
2606
- result,
2607
- instance: selectedInstances[index],
2608
- }))
2609
- .filter(
2610
- (
2611
- entry
2612
- ): entry is {
2613
- result: { ok: true; items: ConsoleClient[]; total: number };
2614
- instance: ConsoleGatewayInstance;
2615
- } => Boolean(entry.instance) && entry.result.ok
2236
+ const targetCount = query.offset + query.limit;
2237
+ const forwardQuery = sanitizeForwardQueryParams(
2238
+ new URL(c.req.url).searchParams
2616
2239
  );
2617
-
2618
- if (successful.length === 0) {
2619
- return allInstancesFailedResponse(c, failedInstances);
2620
- }
2621
-
2622
- const merged = successful
2623
- .flatMap(({ result, instance }) =>
2624
- result.items.map((client) => ({
2625
- ...client,
2626
- instanceId: instance.instanceId,
2627
- federatedClientId: `${instance.instanceId}:${client.clientId}`,
2628
- }))
2629
- )
2630
- .sort((a, b) => {
2631
- const byTime = compareIsoDesc(a.updatedAt, b.updatedAt);
2632
- if (byTime !== 0) return byTime;
2633
- const byInstance = a.instanceId.localeCompare(b.instanceId);
2634
- if (byInstance !== 0) return byInstance;
2635
- return a.clientId.localeCompare(b.clientId);
2240
+ forwardQuery.delete('limit');
2241
+ forwardQuery.delete('offset');
2242
+ const pageSchema = ConsolePaginatedResponseSchema(ConsoleClientSchema);
2243
+ const fetched = await fetchPagedFromSelectedInstances<ConsoleClient>({
2244
+ c,
2245
+ selectedInstances: selection.selectedInstances,
2246
+ path: '/clients',
2247
+ query: forwardQuery,
2248
+ targetCount,
2249
+ schema: pageSchema,
2636
2250
  });
2251
+ if (!fetched.ok) {
2252
+ return fetched.response;
2253
+ }
2637
2254
 
2638
- return c.json({
2639
- items: merged.slice(query.offset, query.offset + query.limit),
2640
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
2641
- offset: query.offset,
2642
- limit: query.limit,
2643
- partial: failedInstances.length > 0,
2644
- failedInstances,
2255
+ const merged = fetched.successfulResults
2256
+ .flatMap(({ items, instance }) =>
2257
+ items.map((client) => ({
2258
+ ...client,
2259
+ instanceId: instance.instanceId,
2260
+ federatedClientId: `${instance.instanceId}:${client.clientId}`,
2261
+ }))
2262
+ )
2263
+ .sort((a, b) => {
2264
+ const byTime = compareIsoDesc(a.updatedAt, b.updatedAt);
2265
+ if (byTime !== 0) return byTime;
2266
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
2267
+ if (byInstance !== 0) return byInstance;
2268
+ return a.clientId.localeCompare(b.clientId);
2269
+ });
2270
+
2271
+ return c.json({
2272
+ items: merged.slice(query.offset, query.offset + query.limit),
2273
+ total: fetched.successfulResults.reduce(
2274
+ (acc, entry) => acc + entry.total,
2275
+ 0
2276
+ ),
2277
+ offset: query.offset,
2278
+ limit: query.limit,
2279
+ partial: fetched.failedInstances.length > 0,
2280
+ failedInstances: fetched.failedInstances,
2281
+ });
2645
2282
  });
2646
2283
  }
2647
2284
  );
@@ -2666,110 +2303,79 @@ export function createConsoleGatewayRoutes(
2666
2303
  }),
2667
2304
  zValidator('query', GatewayTimelineQuerySchema),
2668
2305
  async (c) => {
2669
- const auth = await options.authenticate(c);
2670
- if (!auth) {
2671
- return unauthorizedResponse(c);
2672
- }
2306
+ return withGatewayAuth(c, async () => {
2307
+ const query = c.req.valid('query');
2308
+ const selection = selectTargetInstances(c, query);
2309
+ if (!selection.ok) {
2310
+ return selection.response;
2311
+ }
2673
2312
 
2674
- const query = c.req.valid('query');
2675
- const selectedInstances = selectInstances({ instances, query });
2676
- if (selectedInstances.length === 0) {
2677
- return c.json(
2678
- {
2679
- error: 'NO_INSTANCES_SELECTED',
2680
- message:
2681
- 'No enabled instances matched the provided instance filter.',
2682
- },
2683
- 400
2313
+ const targetCount = query.offset + query.limit;
2314
+ const forwardQuery = sanitizeForwardQueryParams(
2315
+ new URL(c.req.url).searchParams
2684
2316
  );
2685
- }
2686
-
2687
- const targetCount = query.offset + query.limit;
2688
- const forwardQuery = sanitizeForwardQueryParams(
2689
- new URL(c.req.url).searchParams
2690
- );
2691
- forwardQuery.delete('limit');
2692
- forwardQuery.delete('offset');
2693
- const pageSchema = ConsolePaginatedResponseSchema(
2694
- ConsoleTimelineItemSchema
2695
- );
2696
-
2697
- const results = await Promise.all(
2698
- selectedInstances.map((instance) =>
2699
- fetchDownstreamPaged({
2317
+ forwardQuery.delete('limit');
2318
+ forwardQuery.delete('offset');
2319
+ const pageSchema = ConsolePaginatedResponseSchema(
2320
+ ConsoleTimelineItemSchema
2321
+ );
2322
+ const fetched =
2323
+ await fetchPagedFromSelectedInstances<ConsoleTimelineItem>({
2700
2324
  c,
2701
- instance,
2325
+ selectedInstances: selection.selectedInstances,
2702
2326
  path: '/timeline',
2703
2327
  query: forwardQuery,
2704
2328
  targetCount,
2705
2329
  schema: pageSchema,
2706
- fetchImpl,
2707
- })
2708
- )
2709
- );
2710
-
2711
- const failedInstances = results
2712
- .filter(
2713
- (result): result is { ok: false; failure: GatewayFailure } =>
2714
- !result.ok
2715
- )
2716
- .map((result) => result.failure);
2717
- const successful = results
2718
- .map((result, index) => ({
2719
- result,
2720
- instance: selectedInstances[index],
2721
- }))
2722
- .filter(
2723
- (
2724
- entry
2725
- ): entry is {
2726
- result: { ok: true; items: ConsoleTimelineItem[]; total: number };
2727
- instance: ConsoleGatewayInstance;
2728
- } => Boolean(entry.instance) && entry.result.ok
2729
- );
2730
-
2731
- if (successful.length === 0) {
2732
- return allInstancesFailedResponse(c, failedInstances);
2733
- }
2330
+ });
2331
+ if (!fetched.ok) {
2332
+ return fetched.response;
2333
+ }
2734
2334
 
2735
- const merged = successful
2736
- .flatMap(({ result, instance }) =>
2737
- result.items.map((item) => {
2738
- const localCommitSeq =
2739
- item.type === 'commit' ? (item.commit?.commitSeq ?? null) : null;
2740
- const localEventId =
2741
- item.type === 'event' ? (item.event?.eventId ?? null) : null;
2742
- const localIdSegment =
2743
- item.type === 'commit'
2744
- ? String(localCommitSeq ?? 'unknown')
2745
- : String(localEventId ?? 'unknown');
2746
-
2747
- return {
2748
- ...item,
2749
- instanceId: instance.instanceId,
2750
- federatedTimelineId: `${instance.instanceId}:${item.type}:${localIdSegment}`,
2751
- localCommitSeq,
2752
- localEventId,
2753
- };
2754
- })
2755
- )
2756
- .sort((a, b) => {
2757
- const byTime = compareIsoDesc(a.timestamp, b.timestamp);
2758
- if (byTime !== 0) return byTime;
2759
- const byInstance = a.instanceId.localeCompare(b.instanceId);
2760
- if (byInstance !== 0) return byInstance;
2761
- const aLocalId = a.localCommitSeq ?? a.localEventId ?? 0;
2762
- const bLocalId = b.localCommitSeq ?? b.localEventId ?? 0;
2763
- return bLocalId - aLocalId;
2335
+ const merged = fetched.successfulResults
2336
+ .flatMap(({ items, instance }) =>
2337
+ items.map((item) => {
2338
+ const localCommitSeq =
2339
+ item.type === 'commit'
2340
+ ? (item.commit?.commitSeq ?? null)
2341
+ : null;
2342
+ const localEventId =
2343
+ item.type === 'event' ? (item.event?.eventId ?? null) : null;
2344
+ const localIdSegment =
2345
+ item.type === 'commit'
2346
+ ? String(localCommitSeq ?? 'unknown')
2347
+ : String(localEventId ?? 'unknown');
2348
+
2349
+ return {
2350
+ ...item,
2351
+ instanceId: instance.instanceId,
2352
+ federatedTimelineId: `${instance.instanceId}:${item.type}:${localIdSegment}`,
2353
+ localCommitSeq,
2354
+ localEventId,
2355
+ };
2356
+ })
2357
+ )
2358
+ .sort((a, b) => {
2359
+ const byTime = compareIsoDesc(a.timestamp, b.timestamp);
2360
+ if (byTime !== 0) return byTime;
2361
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
2362
+ if (byInstance !== 0) return byInstance;
2363
+ const aLocalId = a.localCommitSeq ?? a.localEventId ?? 0;
2364
+ const bLocalId = b.localCommitSeq ?? b.localEventId ?? 0;
2365
+ return bLocalId - aLocalId;
2366
+ });
2367
+
2368
+ return c.json({
2369
+ items: merged.slice(query.offset, query.offset + query.limit),
2370
+ total: fetched.successfulResults.reduce(
2371
+ (acc, entry) => acc + entry.total,
2372
+ 0
2373
+ ),
2374
+ offset: query.offset,
2375
+ limit: query.limit,
2376
+ partial: fetched.failedInstances.length > 0,
2377
+ failedInstances: fetched.failedInstances,
2764
2378
  });
2765
-
2766
- return c.json({
2767
- items: merged.slice(query.offset, query.offset + query.limit),
2768
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
2769
- offset: query.offset,
2770
- limit: query.limit,
2771
- partial: failedInstances.length > 0,
2772
- failedInstances,
2773
2379
  });
2774
2380
  }
2775
2381
  );
@@ -2794,96 +2400,63 @@ export function createConsoleGatewayRoutes(
2794
2400
  }),
2795
2401
  zValidator('query', GatewayOperationsQuerySchema),
2796
2402
  async (c) => {
2797
- const auth = await options.authenticate(c);
2798
- if (!auth) {
2799
- return unauthorizedResponse(c);
2800
- }
2403
+ return withGatewayAuth(c, async () => {
2404
+ const query = c.req.valid('query');
2405
+ const selection = selectTargetInstances(c, query);
2406
+ if (!selection.ok) {
2407
+ return selection.response;
2408
+ }
2801
2409
 
2802
- const query = c.req.valid('query');
2803
- const selectedInstances = selectInstances({ instances, query });
2804
- if (selectedInstances.length === 0) {
2805
- return c.json(
2806
- {
2807
- error: 'NO_INSTANCES_SELECTED',
2808
- message:
2809
- 'No enabled instances matched the provided instance filter.',
2810
- },
2811
- 400
2410
+ const targetCount = query.offset + query.limit;
2411
+ const forwardQuery = sanitizeForwardQueryParams(
2412
+ new URL(c.req.url).searchParams
2812
2413
  );
2813
- }
2814
-
2815
- const targetCount = query.offset + query.limit;
2816
- const forwardQuery = sanitizeForwardQueryParams(
2817
- new URL(c.req.url).searchParams
2818
- );
2819
- forwardQuery.delete('limit');
2820
- forwardQuery.delete('offset');
2821
- const pageSchema = ConsolePaginatedResponseSchema(
2822
- ConsoleOperationEventSchema
2823
- );
2824
-
2825
- const results = await Promise.all(
2826
- selectedInstances.map((instance) =>
2827
- fetchDownstreamPaged({
2414
+ forwardQuery.delete('limit');
2415
+ forwardQuery.delete('offset');
2416
+ const pageSchema = ConsolePaginatedResponseSchema(
2417
+ ConsoleOperationEventSchema
2418
+ );
2419
+ const fetched =
2420
+ await fetchPagedFromSelectedInstances<ConsoleOperationEvent>({
2828
2421
  c,
2829
- instance,
2422
+ selectedInstances: selection.selectedInstances,
2830
2423
  path: '/operations',
2831
2424
  query: forwardQuery,
2832
2425
  targetCount,
2833
2426
  schema: pageSchema,
2834
- fetchImpl,
2835
- })
2836
- )
2837
- );
2838
-
2839
- const failedInstances = results
2840
- .filter(
2841
- (result): result is { ok: false; failure: GatewayFailure } =>
2842
- !result.ok
2843
- )
2844
- .map((result) => result.failure);
2845
- const successful = results
2846
- .map((result, index) => ({
2847
- result,
2848
- instance: selectedInstances[index],
2849
- }))
2850
- .filter(
2851
- (
2852
- entry
2853
- ): entry is {
2854
- result: { ok: true; items: ConsoleOperationEvent[]; total: number };
2855
- instance: ConsoleGatewayInstance;
2856
- } => Boolean(entry.instance) && entry.result.ok
2857
- );
2858
-
2859
- if (successful.length === 0) {
2860
- return allInstancesFailedResponse(c, failedInstances);
2861
- }
2427
+ });
2428
+ if (!fetched.ok) {
2429
+ return fetched.response;
2430
+ }
2862
2431
 
2863
- const merged = successful
2864
- .flatMap(({ result, instance }) =>
2865
- result.items.map((operation) => ({
2866
- ...operation,
2867
- instanceId: instance.instanceId,
2868
- federatedOperationId: `${instance.instanceId}:${operation.operationId}`,
2869
- localOperationId: operation.operationId,
2870
- }))
2871
- )
2872
- .sort((a, b) => {
2873
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2874
- if (byTime !== 0) return byTime;
2875
- const byInstance = a.instanceId.localeCompare(b.instanceId);
2876
- if (byInstance !== 0) return byInstance;
2877
- return b.localOperationId - a.localOperationId;
2432
+ const merged = fetched.successfulResults
2433
+ .flatMap(({ items, instance }) =>
2434
+ items.map((operation) => ({
2435
+ ...operation,
2436
+ instanceId: instance.instanceId,
2437
+ federatedOperationId: `${instance.instanceId}:${operation.operationId}`,
2438
+ localOperationId: operation.operationId,
2439
+ }))
2440
+ )
2441
+ .sort((a, b) => {
2442
+ const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2443
+ if (byTime !== 0) return byTime;
2444
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
2445
+ if (byInstance !== 0) return byInstance;
2446
+ return b.localOperationId - a.localOperationId;
2447
+ });
2448
+
2449
+ return c.json({
2450
+ items: merged.slice(query.offset, query.offset + query.limit),
2451
+ total: fetched.successfulResults.reduce(
2452
+ (acc, entry) => acc + entry.total,
2453
+ 0
2454
+ ),
2455
+ offset: query.offset,
2456
+ limit: query.limit,
2457
+ partial: fetched.failedInstances.length > 0,
2458
+ failedInstances: fetched.failedInstances,
2878
2459
  });
2879
-
2880
- return c.json({
2881
- items: merged.slice(query.offset, query.offset + query.limit),
2882
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
2883
- offset: query.offset,
2884
- limit: query.limit,
2885
- partial: failedInstances.length > 0,
2886
- failedInstances,
2887
2460
  });
2888
2461
  }
2889
2462
  );
@@ -2908,96 +2481,63 @@ export function createConsoleGatewayRoutes(
2908
2481
  }),
2909
2482
  zValidator('query', GatewayEventsQuerySchema),
2910
2483
  async (c) => {
2911
- const auth = await options.authenticate(c);
2912
- if (!auth) {
2913
- return unauthorizedResponse(c);
2914
- }
2484
+ return withGatewayAuth(c, async () => {
2485
+ const query = c.req.valid('query');
2486
+ const selection = selectTargetInstances(c, query);
2487
+ if (!selection.ok) {
2488
+ return selection.response;
2489
+ }
2915
2490
 
2916
- const query = c.req.valid('query');
2917
- const selectedInstances = selectInstances({ instances, query });
2918
- if (selectedInstances.length === 0) {
2919
- return c.json(
2920
- {
2921
- error: 'NO_INSTANCES_SELECTED',
2922
- message:
2923
- 'No enabled instances matched the provided instance filter.',
2924
- },
2925
- 400
2491
+ const targetCount = query.offset + query.limit;
2492
+ const forwardQuery = sanitizeForwardQueryParams(
2493
+ new URL(c.req.url).searchParams
2926
2494
  );
2927
- }
2928
-
2929
- const targetCount = query.offset + query.limit;
2930
- const forwardQuery = sanitizeForwardQueryParams(
2931
- new URL(c.req.url).searchParams
2932
- );
2933
- forwardQuery.delete('limit');
2934
- forwardQuery.delete('offset');
2935
- const pageSchema = ConsolePaginatedResponseSchema(
2936
- ConsoleRequestEventSchema
2937
- );
2938
-
2939
- const results = await Promise.all(
2940
- selectedInstances.map((instance) =>
2941
- fetchDownstreamPaged({
2495
+ forwardQuery.delete('limit');
2496
+ forwardQuery.delete('offset');
2497
+ const pageSchema = ConsolePaginatedResponseSchema(
2498
+ ConsoleRequestEventSchema
2499
+ );
2500
+ const fetched =
2501
+ await fetchPagedFromSelectedInstances<ConsoleRequestEvent>({
2942
2502
  c,
2943
- instance,
2503
+ selectedInstances: selection.selectedInstances,
2944
2504
  path: '/events',
2945
2505
  query: forwardQuery,
2946
2506
  targetCount,
2947
2507
  schema: pageSchema,
2948
- fetchImpl,
2949
- })
2950
- )
2951
- );
2952
-
2953
- const failedInstances = results
2954
- .filter(
2955
- (result): result is { ok: false; failure: GatewayFailure } =>
2956
- !result.ok
2957
- )
2958
- .map((result) => result.failure);
2959
- const successful = results
2960
- .map((result, index) => ({
2961
- result,
2962
- instance: selectedInstances[index],
2963
- }))
2964
- .filter(
2965
- (
2966
- entry
2967
- ): entry is {
2968
- result: { ok: true; items: ConsoleRequestEvent[]; total: number };
2969
- instance: ConsoleGatewayInstance;
2970
- } => Boolean(entry.instance) && entry.result.ok
2971
- );
2972
-
2973
- if (successful.length === 0) {
2974
- return allInstancesFailedResponse(c, failedInstances);
2975
- }
2508
+ });
2509
+ if (!fetched.ok) {
2510
+ return fetched.response;
2511
+ }
2976
2512
 
2977
- const merged = successful
2978
- .flatMap(({ result, instance }) =>
2979
- result.items.map((event) => ({
2980
- ...event,
2981
- instanceId: instance.instanceId,
2982
- federatedEventId: `${instance.instanceId}:${event.eventId}`,
2983
- localEventId: event.eventId,
2984
- }))
2985
- )
2986
- .sort((a, b) => {
2987
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2988
- if (byTime !== 0) return byTime;
2989
- const byInstance = a.instanceId.localeCompare(b.instanceId);
2990
- if (byInstance !== 0) return byInstance;
2991
- return b.localEventId - a.localEventId;
2513
+ const merged = fetched.successfulResults
2514
+ .flatMap(({ items, instance }) =>
2515
+ items.map((event) => ({
2516
+ ...event,
2517
+ instanceId: instance.instanceId,
2518
+ federatedEventId: `${instance.instanceId}:${event.eventId}`,
2519
+ localEventId: event.eventId,
2520
+ }))
2521
+ )
2522
+ .sort((a, b) => {
2523
+ const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2524
+ if (byTime !== 0) return byTime;
2525
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
2526
+ if (byInstance !== 0) return byInstance;
2527
+ return b.localEventId - a.localEventId;
2528
+ });
2529
+
2530
+ return c.json({
2531
+ items: merged.slice(query.offset, query.offset + query.limit),
2532
+ total: fetched.successfulResults.reduce(
2533
+ (acc, entry) => acc + entry.total,
2534
+ 0
2535
+ ),
2536
+ offset: query.offset,
2537
+ limit: query.limit,
2538
+ partial: fetched.failedInstances.length > 0,
2539
+ failedInstances: fetched.failedInstances,
2992
2540
  });
2993
-
2994
- return c.json({
2995
- items: merged.slice(query.offset, query.offset + query.limit),
2996
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
2997
- offset: query.offset,
2998
- limit: query.limit,
2999
- partial: failedInstances.length > 0,
3000
- failedInstances,
3001
2541
  });
3002
2542
  }
3003
2543
  );
@@ -3024,6 +2564,7 @@ export function createConsoleGatewayRoutes(
3024
2564
  heartbeatInterval: ReturnType<typeof setInterval> | null;
3025
2565
  authTimeout: ReturnType<typeof setTimeout> | null;
3026
2566
  isAuthenticated: boolean;
2567
+ startAuthenticatedSession: ((token: string | null) => void) | null;
3027
2568
  }
3028
2569
  >();
3029
2570
 
@@ -3066,17 +2607,6 @@ export function createConsoleGatewayRoutes(
3066
2607
  return options.authenticate(authContext);
3067
2608
  };
3068
2609
 
3069
- const closeUnauthenticated = (ws: WebSocketLike) => {
3070
- try {
3071
- ws.send(
3072
- JSON.stringify({ type: 'error', message: 'UNAUTHENTICATED' })
3073
- );
3074
- } catch {
3075
- // no-op
3076
- }
3077
- ws.close(4001, 'Unauthenticated');
3078
- };
3079
-
3080
2610
  const cleanup = (ws: WebSocketLike) => {
3081
2611
  const state = liveState.get(ws);
3082
2612
  if (!state) return;
@@ -3115,11 +2645,15 @@ export function createConsoleGatewayRoutes(
3115
2645
  heartbeatInterval: ReturnType<typeof setInterval> | null;
3116
2646
  authTimeout: ReturnType<typeof setTimeout> | null;
3117
2647
  isAuthenticated: boolean;
2648
+ startAuthenticatedSession:
2649
+ | ((token: string | null) => void)
2650
+ | null;
3118
2651
  } = {
3119
2652
  downstreamSockets: [],
3120
2653
  heartbeatInterval: null,
3121
2654
  authTimeout: null,
3122
2655
  isAuthenticated: false,
2656
+ startAuthenticatedSession: null,
3123
2657
  };
3124
2658
  liveState.set(ws, state);
3125
2659
 
@@ -3248,6 +2782,7 @@ export function createConsoleGatewayRoutes(
3248
2782
  }, heartbeatIntervalMs);
3249
2783
  state.heartbeatInterval = heartbeatInterval;
3250
2784
  };
2785
+ state.startAuthenticatedSession = startAuthenticatedSession;
3251
2786
 
3252
2787
  if (initialAuth) {
3253
2788
  startAuthenticatedSession(
@@ -3261,7 +2796,7 @@ export function createConsoleGatewayRoutes(
3261
2796
  if (!current || current.isAuthenticated) {
3262
2797
  return;
3263
2798
  }
3264
- closeUnauthenticated(ws);
2799
+ closeUnauthenticatedSocket(ws);
3265
2800
  cleanup(ws);
3266
2801
  }, 5_000);
3267
2802
  },
@@ -3272,30 +2807,15 @@ export function createConsoleGatewayRoutes(
3272
2807
  }
3273
2808
 
3274
2809
  if (typeof event.data !== 'string') {
3275
- closeUnauthenticated(ws);
2810
+ closeUnauthenticatedSocket(ws);
3276
2811
  cleanup(ws);
3277
2812
  return;
3278
2813
  }
3279
2814
 
3280
- let token = '';
3281
- try {
3282
- const parsed = JSON.parse(event.data) as {
3283
- type?: unknown;
3284
- token?: unknown;
3285
- };
3286
- if (
3287
- parsed.type === 'auth' &&
3288
- typeof parsed.token === 'string' &&
3289
- parsed.token.trim().length > 0
3290
- ) {
3291
- token = parsed.token;
3292
- }
3293
- } catch {
3294
- // Invalid auth message will be handled below.
3295
- }
2815
+ const token = parseWebSocketAuthToken(event.data);
3296
2816
 
3297
2817
  if (!token) {
3298
- closeUnauthenticated(ws);
2818
+ closeUnauthenticatedSocket(ws);
3299
2819
  cleanup(ws);
3300
2820
  return;
3301
2821
  }
@@ -3306,131 +2826,11 @@ export function createConsoleGatewayRoutes(
3306
2826
  return;
3307
2827
  }
3308
2828
  if (!auth) {
3309
- closeUnauthenticated(ws);
2829
+ closeUnauthenticatedSocket(ws);
3310
2830
  cleanup(ws);
3311
2831
  return;
3312
2832
  }
3313
-
3314
- current.isAuthenticated = true;
3315
- if (current.authTimeout) {
3316
- clearTimeout(current.authTimeout);
3317
- current.authTimeout = null;
3318
- }
3319
-
3320
- for (const instance of selectedInstances) {
3321
- const downstreamQuery = new URLSearchParams();
3322
- if (partitionId) {
3323
- downstreamQuery.set('partitionId', partitionId);
3324
- }
3325
- if (replaySince) {
3326
- downstreamQuery.set('since', replaySince);
3327
- }
3328
- downstreamQuery.set('replayLimit', String(replayLimit));
3329
-
3330
- const downstreamUrl = buildConsoleEndpointUrl({
3331
- instance,
3332
- requestUrl: c.req.url,
3333
- path: '/events/live',
3334
- query: downstreamQuery,
3335
- });
3336
-
3337
- const downstreamSocket = createDownstreamSocket(downstreamUrl);
3338
- const upstreamToken = token.trim();
3339
- const downstreamToken =
3340
- instance.token?.trim() ||
3341
- (upstreamToken.length > 0 ? upstreamToken : null);
3342
- if (downstreamToken && downstreamSocket.send) {
3343
- downstreamSocket.onopen = () => {
3344
- try {
3345
- downstreamSocket.send?.(
3346
- JSON.stringify({
3347
- type: 'auth',
3348
- token: downstreamToken,
3349
- })
3350
- );
3351
- } catch {
3352
- // no-op
3353
- }
3354
- };
3355
- }
3356
-
3357
- downstreamSocket.onmessage = (message: MessageEvent) => {
3358
- if (typeof message.data !== 'string') {
3359
- return;
3360
- }
3361
- try {
3362
- const payload = JSON.parse(message.data) as Record<
3363
- string,
3364
- unknown
3365
- >;
3366
- if (
3367
- typeof payload.type === 'string' &&
3368
- (payload.type === 'connected' ||
3369
- payload.type === 'heartbeat')
3370
- ) {
3371
- return;
3372
- }
3373
-
3374
- const payloadData =
3375
- payload.data &&
3376
- typeof payload.data === 'object' &&
3377
- !Array.isArray(payload.data)
3378
- ? { ...payload.data, instanceId: instance.instanceId }
3379
- : { instanceId: instance.instanceId };
3380
-
3381
- const liveEvent = {
3382
- ...payload,
3383
- data: payloadData,
3384
- instanceId: instance.instanceId,
3385
- timestamp:
3386
- typeof payload.timestamp === 'string'
3387
- ? payload.timestamp
3388
- : new Date().toISOString(),
3389
- };
3390
- ws.send(JSON.stringify(liveEvent));
3391
- } catch {
3392
- // Ignore malformed downstream events
3393
- }
3394
- };
3395
-
3396
- downstreamSocket.onerror = () => {
3397
- try {
3398
- ws.send(
3399
- JSON.stringify({
3400
- type: 'instance_error',
3401
- instanceId: instance.instanceId,
3402
- timestamp: new Date().toISOString(),
3403
- })
3404
- );
3405
- } catch {
3406
- // ignore send errors
3407
- }
3408
- };
3409
-
3410
- current.downstreamSockets.push(downstreamSocket);
3411
- }
3412
-
3413
- ws.send(
3414
- JSON.stringify({
3415
- type: 'connected',
3416
- timestamp: new Date().toISOString(),
3417
- instanceCount: selectedInstances.length,
3418
- })
3419
- );
3420
-
3421
- const heartbeatInterval = setInterval(() => {
3422
- try {
3423
- ws.send(
3424
- JSON.stringify({
3425
- type: 'heartbeat',
3426
- timestamp: new Date().toISOString(),
3427
- })
3428
- );
3429
- } catch {
3430
- clearInterval(heartbeatInterval);
3431
- }
3432
- }, heartbeatIntervalMs);
3433
- current.heartbeatInterval = heartbeatInterval;
2833
+ current.startAuthenticatedSession?.(token);
3434
2834
  },
3435
2835
  onClose(_event, ws) {
3436
2836
  cleanup(ws);
@@ -3465,58 +2865,55 @@ export function createConsoleGatewayRoutes(
3465
2865
  ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)
3466
2866
  ),
3467
2867
  async (c) => {
3468
- const auth = await options.authenticate(c);
3469
- if (!auth) {
3470
- return unauthorizedResponse(c);
3471
- }
2868
+ return withGatewayAuth(c, async () => {
2869
+ const { id } = c.req.valid('param');
2870
+ const query = c.req.valid('query');
2871
+ const target = resolveEventTarget({
2872
+ id,
2873
+ instances,
2874
+ query,
2875
+ });
2876
+ if (!target.ok) {
2877
+ return c.json(
2878
+ {
2879
+ error: target.error,
2880
+ ...(target.message ? { message: target.message } : {}),
2881
+ },
2882
+ target.status
2883
+ );
2884
+ }
3472
2885
 
3473
- const { id } = c.req.valid('param');
3474
- const query = c.req.valid('query');
3475
- const target = resolveEventTarget({
3476
- id,
3477
- instances,
3478
- query,
3479
- });
3480
- if (!target.ok) {
3481
- return c.json(
3482
- {
3483
- error: target.error,
3484
- ...(target.message ? { message: target.message } : {}),
3485
- },
3486
- target.status
2886
+ const forwardQuery = sanitizeForwardQueryParams(
2887
+ new URL(c.req.url).searchParams
3487
2888
  );
3488
- }
3489
-
3490
- const forwardQuery = sanitizeForwardQueryParams(
3491
- new URL(c.req.url).searchParams
3492
- );
3493
- const result = await fetchDownstreamJson({
3494
- c,
3495
- instance: target.instance,
3496
- path: `/events/${target.localEventId}`,
3497
- query: forwardQuery,
3498
- schema: ConsoleRequestEventSchema,
3499
- fetchImpl,
3500
- });
2889
+ const result = await fetchDownstreamJson({
2890
+ c,
2891
+ instance: target.instance,
2892
+ path: `/events/${target.localEventId}`,
2893
+ query: forwardQuery,
2894
+ schema: ConsoleRequestEventSchema,
2895
+ fetchImpl,
2896
+ });
3501
2897
 
3502
- if (!result.ok) {
3503
- if (result.failure.status === 404) {
3504
- return c.json({ error: 'NOT_FOUND' }, 404);
2898
+ if (!result.ok) {
2899
+ if (result.failure.status === 404) {
2900
+ return c.json({ error: 'NOT_FOUND' }, 404);
2901
+ }
2902
+ return c.json(
2903
+ {
2904
+ error: 'DOWNSTREAM_UNAVAILABLE',
2905
+ failedInstances: [result.failure],
2906
+ },
2907
+ 502
2908
+ );
3505
2909
  }
3506
- return c.json(
3507
- {
3508
- error: 'DOWNSTREAM_UNAVAILABLE',
3509
- failedInstances: [result.failure],
3510
- },
3511
- 502
3512
- );
3513
- }
3514
2910
 
3515
- return c.json({
3516
- ...result.data,
3517
- instanceId: target.instance.instanceId,
3518
- federatedEventId: `${target.instance.instanceId}:${result.data.eventId}`,
3519
- localEventId: result.data.eventId,
2911
+ return c.json({
2912
+ ...result.data,
2913
+ instanceId: target.instance.instanceId,
2914
+ federatedEventId: `${target.instance.instanceId}:${result.data.eventId}`,
2915
+ localEventId: result.data.eventId,
2916
+ });
3520
2917
  });
3521
2918
  }
3522
2919
  );
@@ -3543,58 +2940,55 @@ export function createConsoleGatewayRoutes(
3543
2940
  ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)
3544
2941
  ),
3545
2942
  async (c) => {
3546
- const auth = await options.authenticate(c);
3547
- if (!auth) {
3548
- return unauthorizedResponse(c);
3549
- }
2943
+ return withGatewayAuth(c, async () => {
2944
+ const { id } = c.req.valid('param');
2945
+ const query = c.req.valid('query');
2946
+ const target = resolveEventTarget({
2947
+ id,
2948
+ instances,
2949
+ query,
2950
+ });
2951
+ if (!target.ok) {
2952
+ return c.json(
2953
+ {
2954
+ error: target.error,
2955
+ ...(target.message ? { message: target.message } : {}),
2956
+ },
2957
+ target.status
2958
+ );
2959
+ }
3550
2960
 
3551
- const { id } = c.req.valid('param');
3552
- const query = c.req.valid('query');
3553
- const target = resolveEventTarget({
3554
- id,
3555
- instances,
3556
- query,
3557
- });
3558
- if (!target.ok) {
3559
- return c.json(
3560
- {
3561
- error: target.error,
3562
- ...(target.message ? { message: target.message } : {}),
3563
- },
3564
- target.status
2961
+ const forwardQuery = sanitizeForwardQueryParams(
2962
+ new URL(c.req.url).searchParams
3565
2963
  );
3566
- }
3567
-
3568
- const forwardQuery = sanitizeForwardQueryParams(
3569
- new URL(c.req.url).searchParams
3570
- );
3571
- const result = await fetchDownstreamJson({
3572
- c,
3573
- instance: target.instance,
3574
- path: `/events/${target.localEventId}/payload`,
3575
- query: forwardQuery,
3576
- schema: ConsoleRequestPayloadSchema,
3577
- fetchImpl,
3578
- });
2964
+ const result = await fetchDownstreamJson({
2965
+ c,
2966
+ instance: target.instance,
2967
+ path: `/events/${target.localEventId}/payload`,
2968
+ query: forwardQuery,
2969
+ schema: ConsoleRequestPayloadSchema,
2970
+ fetchImpl,
2971
+ });
3579
2972
 
3580
- if (!result.ok) {
3581
- if (result.failure.status === 404) {
3582
- return c.json({ error: 'NOT_FOUND' }, 404);
2973
+ if (!result.ok) {
2974
+ if (result.failure.status === 404) {
2975
+ return c.json({ error: 'NOT_FOUND' }, 404);
2976
+ }
2977
+ return c.json(
2978
+ {
2979
+ error: 'DOWNSTREAM_UNAVAILABLE',
2980
+ failedInstances: [result.failure],
2981
+ },
2982
+ 502
2983
+ );
3583
2984
  }
3584
- return c.json(
3585
- {
3586
- error: 'DOWNSTREAM_UNAVAILABLE',
3587
- failedInstances: [result.failure],
3588
- },
3589
- 502
3590
- );
3591
- }
3592
2985
 
3593
- return c.json({
3594
- ...result.data,
3595
- instanceId: target.instance.instanceId,
3596
- federatedEventId: `${target.instance.instanceId}:${target.localEventId}`,
3597
- localEventId: target.localEventId,
2986
+ return c.json({
2987
+ ...result.data,
2988
+ instanceId: target.instance.instanceId,
2989
+ federatedEventId: `${target.instance.instanceId}:${target.localEventId}`,
2990
+ localEventId: target.localEventId,
2991
+ });
3598
2992
  });
3599
2993
  }
3600
2994
  );