@syncular/server-hono 0.0.6-95 → 0.2.0

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.
Files changed (104) hide show
  1. package/README.md +5 -23
  2. package/dist/admin-page.d.ts +14 -0
  3. package/dist/admin-page.js +217 -0
  4. package/dist/admin.d.ts +39 -0
  5. package/dist/admin.js +142 -0
  6. package/dist/index.d.ts +16 -14
  7. package/dist/index.js +134 -23
  8. package/package.json +19 -53
  9. package/src/admin-page.ts +217 -0
  10. package/src/admin.ts +193 -0
  11. package/src/index.ts +175 -31
  12. package/dist/api-key-auth.d.ts +0 -49
  13. package/dist/api-key-auth.d.ts.map +0 -1
  14. package/dist/api-key-auth.js +0 -110
  15. package/dist/api-key-auth.js.map +0 -1
  16. package/dist/blobs.d.ts +0 -69
  17. package/dist/blobs.d.ts.map +0 -1
  18. package/dist/blobs.js +0 -383
  19. package/dist/blobs.js.map +0 -1
  20. package/dist/console/gateway.d.ts +0 -33
  21. package/dist/console/gateway.d.ts.map +0 -1
  22. package/dist/console/gateway.js +0 -2534
  23. package/dist/console/gateway.js.map +0 -1
  24. package/dist/console/index.d.ts +0 -11
  25. package/dist/console/index.d.ts.map +0 -1
  26. package/dist/console/index.js +0 -11
  27. package/dist/console/index.js.map +0 -1
  28. package/dist/console/routes.d.ts +0 -33
  29. package/dist/console/routes.d.ts.map +0 -1
  30. package/dist/console/routes.js +0 -2890
  31. package/dist/console/routes.js.map +0 -1
  32. package/dist/console/schemas.d.ts +0 -490
  33. package/dist/console/schemas.d.ts.map +0 -1
  34. package/dist/console/schemas.js +0 -303
  35. package/dist/console/schemas.js.map +0 -1
  36. package/dist/console/types.d.ts +0 -142
  37. package/dist/console/types.d.ts.map +0 -1
  38. package/dist/console/types.js +0 -2
  39. package/dist/console/types.js.map +0 -1
  40. package/dist/console/ui.d.ts +0 -38
  41. package/dist/console/ui.d.ts.map +0 -1
  42. package/dist/console/ui.js +0 -43
  43. package/dist/console/ui.js.map +0 -1
  44. package/dist/create-server.d.ts +0 -68
  45. package/dist/create-server.d.ts.map +0 -1
  46. package/dist/create-server.js +0 -109
  47. package/dist/create-server.js.map +0 -1
  48. package/dist/index.d.ts.map +0 -1
  49. package/dist/index.js.map +0 -1
  50. package/dist/openapi.d.ts +0 -45
  51. package/dist/openapi.d.ts.map +0 -1
  52. package/dist/openapi.js +0 -59
  53. package/dist/openapi.js.map +0 -1
  54. package/dist/proxy/connection-manager.d.ts +0 -78
  55. package/dist/proxy/connection-manager.d.ts.map +0 -1
  56. package/dist/proxy/connection-manager.js +0 -251
  57. package/dist/proxy/connection-manager.js.map +0 -1
  58. package/dist/proxy/index.d.ts +0 -8
  59. package/dist/proxy/index.d.ts.map +0 -1
  60. package/dist/proxy/index.js +0 -8
  61. package/dist/proxy/index.js.map +0 -1
  62. package/dist/proxy/routes.d.ts +0 -74
  63. package/dist/proxy/routes.d.ts.map +0 -1
  64. package/dist/proxy/routes.js +0 -147
  65. package/dist/proxy/routes.js.map +0 -1
  66. package/dist/rate-limit.d.ts +0 -101
  67. package/dist/rate-limit.d.ts.map +0 -1
  68. package/dist/rate-limit.js +0 -186
  69. package/dist/rate-limit.js.map +0 -1
  70. package/dist/routes.d.ts +0 -145
  71. package/dist/routes.d.ts.map +0 -1
  72. package/dist/routes.js +0 -1471
  73. package/dist/routes.js.map +0 -1
  74. package/dist/ws.d.ts +0 -235
  75. package/dist/ws.d.ts.map +0 -1
  76. package/dist/ws.js +0 -614
  77. package/dist/ws.js.map +0 -1
  78. package/src/__tests__/blob-routes.test.ts +0 -424
  79. package/src/__tests__/console-gateway-live-routes.test.ts +0 -297
  80. package/src/__tests__/console-gateway-routes.test.ts +0 -1364
  81. package/src/__tests__/console-routes.test.ts +0 -1543
  82. package/src/__tests__/console-ui.test.ts +0 -114
  83. package/src/__tests__/create-server.test.ts +0 -342
  84. package/src/__tests__/pull-chunk-storage.test.ts +0 -576
  85. package/src/__tests__/rate-limit.test.ts +0 -78
  86. package/src/__tests__/realtime-bridge.test.ts +0 -135
  87. package/src/__tests__/sync-rate-limit-routing.test.ts +0 -185
  88. package/src/__tests__/ws-connection-manager.test.ts +0 -176
  89. package/src/api-key-auth.ts +0 -179
  90. package/src/blobs.ts +0 -534
  91. package/src/console/gateway.ts +0 -3603
  92. package/src/console/index.ts +0 -11
  93. package/src/console/routes.ts +0 -3748
  94. package/src/console/schemas.ts +0 -434
  95. package/src/console/types.ts +0 -151
  96. package/src/console/ui.ts +0 -100
  97. package/src/create-server.ts +0 -206
  98. package/src/openapi.ts +0 -74
  99. package/src/proxy/connection-manager.ts +0 -340
  100. package/src/proxy/index.ts +0 -8
  101. package/src/proxy/routes.ts +0 -223
  102. package/src/rate-limit.ts +0 -321
  103. package/src/routes.ts +0 -2007
  104. package/src/ws.ts +0 -802
@@ -1,2534 +0,0 @@
1
- import { Hono } from 'hono';
2
- import { cors } from 'hono/cors';
3
- import { describeRoute, resolver, validator as zValidator } from 'hono-openapi';
4
- import { z } from 'zod';
5
- import { ApiKeyTypeSchema, ConsoleApiKeyBulkRevokeRequestSchema, ConsoleApiKeyBulkRevokeResponseSchema, ConsoleApiKeyCreateRequestSchema, ConsoleApiKeyCreateResponseSchema, ConsoleApiKeyRevokeResponseSchema, ConsoleApiKeySchema, ConsoleClearEventsResultSchema, ConsoleClientSchema, ConsoleCommitDetailSchema, ConsoleCommitListItemSchema, ConsoleCompactResultSchema, ConsoleEvictResultSchema, ConsoleHandlerSchema, ConsoleOperationEventSchema, ConsoleOperationsQuerySchema, ConsolePaginatedResponseSchema, ConsolePaginationQuerySchema, ConsolePartitionedPaginationQuerySchema, ConsolePartitionQuerySchema, ConsolePruneEventsResultSchema, ConsolePrunePreviewSchema, ConsolePruneResultSchema, ConsoleRequestEventSchema, ConsoleRequestPayloadSchema, ConsoleTimelineItemSchema, ConsoleTimelineQuerySchema, LatencyQuerySchema, LatencyStatsResponseSchema, SyncStatsSchema, TimeseriesQuerySchema, TimeseriesStatsResponseSchema, } from './schemas.js';
6
- const GatewayFailureSchema = z.object({
7
- instanceId: z.string(),
8
- reason: z.string(),
9
- status: z.number().int().optional(),
10
- });
11
- const GatewayMetadataSchema = z.object({
12
- partial: z.boolean(),
13
- failedInstances: z.array(GatewayFailureSchema),
14
- });
15
- const GatewayInstanceSchema = z.object({
16
- instanceId: z.string(),
17
- label: z.string(),
18
- baseUrl: z.string(),
19
- enabled: z.boolean(),
20
- });
21
- const GatewayInstancesResponseSchema = z.object({
22
- items: z.array(GatewayInstanceSchema),
23
- });
24
- const GatewayInstanceHealthSchema = GatewayInstanceSchema.extend({
25
- healthy: z.boolean(),
26
- status: z.number().int().optional(),
27
- reason: z.string().optional(),
28
- responseTimeMs: z.number().int().nonnegative(),
29
- checkedAt: z.string(),
30
- });
31
- const GatewayInstancesHealthResponseSchema = z.object({
32
- items: z.array(GatewayInstanceHealthSchema),
33
- partial: GatewayMetadataSchema.shape.partial,
34
- failedInstances: GatewayMetadataSchema.shape.failedInstances,
35
- });
36
- const GatewayInstanceFilterSchema = z.object({
37
- instanceId: z.string().min(1).optional(),
38
- instanceIds: z.string().min(1).optional(),
39
- });
40
- const GatewayStatsQuerySchema = ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape);
41
- const GatewayTimeseriesQuerySchema = TimeseriesQuerySchema.extend(GatewayInstanceFilterSchema.shape);
42
- const GatewayLatencyQuerySchema = LatencyQuerySchema.extend(GatewayInstanceFilterSchema.shape);
43
- const GatewaySingleInstanceQuerySchema = GatewayInstanceFilterSchema;
44
- const GatewaySingleInstancePartitionQuerySchema = ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape);
45
- const GatewayApiKeyStatusSchema = z.enum(['active', 'revoked', 'expiring']);
46
- const GatewayApiKeysQuerySchema = ConsolePaginationQuerySchema.extend({
47
- ...GatewayInstanceFilterSchema.shape,
48
- type: ApiKeyTypeSchema.optional(),
49
- status: GatewayApiKeyStatusSchema.optional(),
50
- expiresWithinDays: z.coerce.number().int().min(1).max(365).optional(),
51
- });
52
- const GatewayPaginatedQuerySchema = ConsolePartitionedPaginationQuerySchema.extend(GatewayInstanceFilterSchema.shape);
53
- const GatewayTimelineQuerySchema = ConsoleTimelineQuerySchema.extend(GatewayInstanceFilterSchema.shape);
54
- const GatewayOperationsQuerySchema = ConsoleOperationsQuerySchema.extend(GatewayInstanceFilterSchema.shape);
55
- const GatewayEventsQuerySchema = ConsolePartitionedPaginationQuerySchema.extend({
56
- ...GatewayInstanceFilterSchema.shape,
57
- eventType: z.enum(['push', 'pull']).optional(),
58
- actorId: z.string().optional(),
59
- clientId: z.string().optional(),
60
- requestId: z.string().optional(),
61
- traceId: z.string().optional(),
62
- outcome: z.string().optional(),
63
- });
64
- const GatewayEventPathParamSchema = z.object({
65
- id: z.string().min(1),
66
- });
67
- const GatewayCommitPathParamSchema = z.object({
68
- seq: z.string().min(1),
69
- });
70
- const GatewayClientPathParamSchema = z.object({
71
- id: z.string().min(1),
72
- });
73
- const GatewayApiKeyPathParamSchema = z.object({
74
- id: z.string().min(1),
75
- });
76
- const GatewayNotifyDataChangeRequestSchema = z.object({
77
- tables: z.array(z.string().min(1)).min(1),
78
- partitionId: z.string().optional(),
79
- });
80
- const GatewayNotifyDataChangeResponseSchema = z.object({
81
- commitSeq: z.number(),
82
- tables: z.array(z.string()),
83
- deletedChunks: z.number(),
84
- });
85
- const GatewayHandlersResponseSchema = z.object({
86
- items: z.array(ConsoleHandlerSchema),
87
- });
88
- const GatewayCommitItemSchema = ConsoleCommitListItemSchema.extend({
89
- instanceId: z.string(),
90
- federatedCommitId: z.string(),
91
- });
92
- const GatewayCommitDetailSchema = ConsoleCommitDetailSchema.extend({
93
- instanceId: z.string(),
94
- federatedCommitId: z.string(),
95
- localCommitSeq: z.number().int(),
96
- });
97
- const GatewayClientItemSchema = ConsoleClientSchema.extend({
98
- instanceId: z.string(),
99
- federatedClientId: z.string(),
100
- });
101
- const GatewayTimelineItemSchema = ConsoleTimelineItemSchema.extend({
102
- instanceId: z.string(),
103
- federatedTimelineId: z.string(),
104
- localCommitSeq: z.number().int().nullable(),
105
- localEventId: z.number().int().nullable(),
106
- });
107
- const GatewayOperationItemSchema = ConsoleOperationEventSchema.extend({
108
- instanceId: z.string(),
109
- federatedOperationId: z.string(),
110
- localOperationId: z.number().int(),
111
- });
112
- const GatewayEventItemSchema = ConsoleRequestEventSchema.extend({
113
- instanceId: z.string(),
114
- federatedEventId: z.string(),
115
- localEventId: z.number().int(),
116
- });
117
- const GatewayEventPayloadSchema = ConsoleRequestPayloadSchema.extend({
118
- instanceId: z.string(),
119
- federatedEventId: z.string(),
120
- localEventId: z.number().int(),
121
- });
122
- const GatewayStatsResponseSchema = SyncStatsSchema.extend({
123
- maxCommitSeqByInstance: z.record(z.string(), z.number().int()),
124
- minCommitSeqByInstance: z.record(z.string(), z.number().int()),
125
- partial: GatewayMetadataSchema.shape.partial,
126
- failedInstances: GatewayMetadataSchema.shape.failedInstances,
127
- });
128
- const GatewayTimeseriesResponseSchema = TimeseriesStatsResponseSchema.extend({
129
- partial: GatewayMetadataSchema.shape.partial,
130
- failedInstances: GatewayMetadataSchema.shape.failedInstances,
131
- });
132
- const GatewayLatencyResponseSchema = LatencyStatsResponseSchema.extend({
133
- partial: GatewayMetadataSchema.shape.partial,
134
- failedInstances: GatewayMetadataSchema.shape.failedInstances,
135
- });
136
- const GatewayPaginatedResponseSchema = (itemSchema) => ConsolePaginatedResponseSchema(itemSchema).extend({
137
- partial: GatewayMetadataSchema.shape.partial,
138
- failedInstances: GatewayMetadataSchema.shape.failedInstances,
139
- });
140
- function toErrorMessage(error) {
141
- if (error instanceof Error && error.message.trim().length > 0) {
142
- return error.message;
143
- }
144
- if (typeof error === 'string' && error.trim().length > 0) {
145
- return error;
146
- }
147
- return 'Request failed';
148
- }
149
- function resolveBaseUrl(baseUrl, requestUrl) {
150
- try {
151
- return new URL(baseUrl);
152
- }
153
- catch {
154
- return new URL(baseUrl, requestUrl);
155
- }
156
- }
157
- function normalizeInstances(instances) {
158
- if (instances.length === 0) {
159
- throw new Error('Console gateway requires at least one instance');
160
- }
161
- const seen = new Set();
162
- return instances.map((instance) => {
163
- const normalizedInstanceId = instance.instanceId.trim();
164
- if (!normalizedInstanceId) {
165
- throw new Error('Console gateway instanceId cannot be empty');
166
- }
167
- if (seen.has(normalizedInstanceId)) {
168
- throw new Error(`Duplicate console gateway instanceId: ${normalizedInstanceId}`);
169
- }
170
- seen.add(normalizedInstanceId);
171
- const normalizedBaseUrl = instance.baseUrl.trim();
172
- if (!normalizedBaseUrl) {
173
- throw new Error(`Console gateway baseUrl cannot be empty for instance: ${normalizedInstanceId}`);
174
- }
175
- return {
176
- instanceId: normalizedInstanceId,
177
- label: instance.label?.trim() || normalizedInstanceId,
178
- baseUrl: normalizedBaseUrl,
179
- token: instance.token?.trim(),
180
- enabled: instance.enabled ?? true,
181
- };
182
- });
183
- }
184
- function parseRequestedInstanceIds(query) {
185
- const ids = new Set();
186
- const single = query.instanceId?.trim();
187
- if (single) {
188
- ids.add(single);
189
- }
190
- const multi = query.instanceIds
191
- ?.split(',')
192
- .map((value) => value.trim())
193
- .filter((value) => value.length > 0);
194
- for (const value of multi ?? []) {
195
- ids.add(value);
196
- }
197
- return ids;
198
- }
199
- function selectInstances(args) {
200
- const enabledInstances = args.instances.filter((instance) => instance.enabled);
201
- const requestedIds = parseRequestedInstanceIds(args.query);
202
- if (requestedIds.size === 0) {
203
- return enabledInstances;
204
- }
205
- return enabledInstances.filter((instance) => requestedIds.has(instance.instanceId));
206
- }
207
- function findInstanceById(args) {
208
- const instance = args.instances.find((candidate) => candidate.instanceId === args.instanceId && Boolean(candidate.enabled));
209
- return instance ?? null;
210
- }
211
- function parseFederatedNumericId(value) {
212
- const separatorIndex = value.indexOf(':');
213
- if (separatorIndex <= 0 || separatorIndex >= value.length - 1) {
214
- return null;
215
- }
216
- const instanceId = value.slice(0, separatorIndex).trim();
217
- const localIdRaw = value.slice(separatorIndex + 1).trim();
218
- const localId = Number(localIdRaw);
219
- if (!instanceId || !Number.isInteger(localId) || localId <= 0) {
220
- return null;
221
- }
222
- return { instanceId, localId };
223
- }
224
- function parseLocalNumericId(value) {
225
- const normalized = value.trim();
226
- if (!normalized)
227
- return null;
228
- const parsed = Number(normalized);
229
- if (!Number.isInteger(parsed) || parsed <= 0)
230
- return null;
231
- return parsed;
232
- }
233
- function resolveEventTarget(args) {
234
- const federated = parseFederatedNumericId(args.id);
235
- if (federated) {
236
- const instance = findInstanceById({
237
- instances: args.instances,
238
- instanceId: federated.instanceId,
239
- });
240
- if (!instance) {
241
- return {
242
- ok: false,
243
- status: 404,
244
- error: 'NOT_FOUND',
245
- message: 'Instance not found',
246
- };
247
- }
248
- return { ok: true, instance, localEventId: federated.localId };
249
- }
250
- const localEventId = parseLocalNumericId(args.id);
251
- if (localEventId === null) {
252
- return {
253
- ok: false,
254
- status: 400,
255
- error: 'INVALID_FEDERATED_ID',
256
- message: 'Expected either "<instanceId>:<eventId>" or "<eventId>" with an explicit instance filter.',
257
- };
258
- }
259
- const selectedInstances = selectInstances({
260
- instances: args.instances,
261
- query: args.query,
262
- });
263
- if (selectedInstances.length === 0) {
264
- return {
265
- ok: false,
266
- status: 400,
267
- error: 'NO_INSTANCES_SELECTED',
268
- message: 'No enabled instances matched the provided instance filter.',
269
- };
270
- }
271
- if (selectedInstances.length > 1) {
272
- return {
273
- ok: false,
274
- status: 400,
275
- error: 'AMBIGUOUS_EVENT_ID',
276
- message: 'Local event IDs are ambiguous across multiple instances. Use "<instanceId>:<eventId>" or select one instance.',
277
- };
278
- }
279
- const instance = selectedInstances[0];
280
- if (!instance) {
281
- return {
282
- ok: false,
283
- status: 400,
284
- error: 'NO_INSTANCES_SELECTED',
285
- message: 'No enabled instances matched the provided instance filter.',
286
- };
287
- }
288
- return { ok: true, instance, localEventId };
289
- }
290
- function resolveCommitTarget(args) {
291
- const federated = parseFederatedNumericId(args.seq);
292
- if (federated) {
293
- const instance = findInstanceById({
294
- instances: args.instances,
295
- instanceId: federated.instanceId,
296
- });
297
- if (!instance) {
298
- return {
299
- ok: false,
300
- status: 404,
301
- error: 'NOT_FOUND',
302
- message: 'Instance not found',
303
- };
304
- }
305
- return { ok: true, instance, localCommitSeq: federated.localId };
306
- }
307
- const localCommitSeq = parseLocalNumericId(args.seq);
308
- if (localCommitSeq === null) {
309
- return {
310
- ok: false,
311
- status: 400,
312
- error: 'INVALID_FEDERATED_ID',
313
- message: 'Expected either "<instanceId>:<commitSeq>" or "<commitSeq>" with an explicit instance filter.',
314
- };
315
- }
316
- const selectedInstances = selectInstances({
317
- instances: args.instances,
318
- query: args.query,
319
- });
320
- if (selectedInstances.length === 0) {
321
- return {
322
- ok: false,
323
- status: 400,
324
- error: 'NO_INSTANCES_SELECTED',
325
- message: 'No enabled instances matched the provided instance filter.',
326
- };
327
- }
328
- if (selectedInstances.length > 1) {
329
- return {
330
- ok: false,
331
- status: 400,
332
- error: 'AMBIGUOUS_COMMIT_ID',
333
- message: 'Local commit IDs are ambiguous across multiple instances. Use "<instanceId>:<commitSeq>" or select one instance.',
334
- };
335
- }
336
- const instance = selectedInstances[0];
337
- if (!instance) {
338
- return {
339
- ok: false,
340
- status: 400,
341
- error: 'NO_INSTANCES_SELECTED',
342
- message: 'No enabled instances matched the provided instance filter.',
343
- };
344
- }
345
- return { ok: true, instance, localCommitSeq };
346
- }
347
- function resolveSingleInstanceTarget(args) {
348
- const selectedInstances = selectInstances(args);
349
- if (selectedInstances.length === 0) {
350
- return {
351
- ok: false,
352
- status: 400,
353
- error: 'NO_INSTANCES_SELECTED',
354
- message: 'No enabled instances matched the provided instance filter.',
355
- };
356
- }
357
- if (selectedInstances.length > 1) {
358
- return {
359
- ok: false,
360
- status: 400,
361
- error: 'INSTANCE_REQUIRED',
362
- message: 'This endpoint requires exactly one target instance. Provide `instanceId` or a single-value `instanceIds` filter.',
363
- };
364
- }
365
- const instance = selectedInstances[0];
366
- if (!instance) {
367
- return {
368
- ok: false,
369
- status: 400,
370
- error: 'NO_INSTANCES_SELECTED',
371
- message: 'No enabled instances matched the provided instance filter.',
372
- };
373
- }
374
- return { ok: true, instance };
375
- }
376
- function minNullable(values) {
377
- const filtered = values.filter((value) => value !== null);
378
- if (filtered.length === 0)
379
- return null;
380
- return Math.min(...filtered);
381
- }
382
- function maxNullable(values) {
383
- const filtered = values.filter((value) => value !== null);
384
- if (filtered.length === 0)
385
- return null;
386
- return Math.max(...filtered);
387
- }
388
- function compareIsoDesc(a, b) {
389
- const aMs = Date.parse(a);
390
- const bMs = Date.parse(b);
391
- if (!Number.isFinite(aMs) && !Number.isFinite(bMs))
392
- return 0;
393
- if (!Number.isFinite(aMs))
394
- return 1;
395
- if (!Number.isFinite(bMs))
396
- return -1;
397
- return bMs - aMs;
398
- }
399
- function createTimeseriesBucketAccumulator() {
400
- return {
401
- pushCount: 0,
402
- pullCount: 0,
403
- errorCount: 0,
404
- latencySum: 0,
405
- eventCount: 0,
406
- };
407
- }
408
- function mergeTimeseriesBuckets(responses) {
409
- const bucketMap = new Map();
410
- for (const response of responses) {
411
- for (const bucket of response.buckets) {
412
- const existing = bucketMap.get(bucket.timestamp) ?? createTimeseriesBucketAccumulator();
413
- existing.pushCount += bucket.pushCount;
414
- existing.pullCount += bucket.pullCount;
415
- existing.errorCount += bucket.errorCount;
416
- const bucketEventCount = bucket.pushCount + bucket.pullCount;
417
- if (bucketEventCount > 0) {
418
- existing.latencySum += bucket.avgLatencyMs * bucketEventCount;
419
- existing.eventCount += bucketEventCount;
420
- }
421
- bucketMap.set(bucket.timestamp, existing);
422
- }
423
- }
424
- return Array.from(bucketMap.entries())
425
- .sort(([a], [b]) => a.localeCompare(b))
426
- .map(([timestamp, bucket]) => ({
427
- timestamp,
428
- pushCount: bucket.pushCount,
429
- pullCount: bucket.pullCount,
430
- errorCount: bucket.errorCount,
431
- avgLatencyMs: bucket.eventCount > 0 ? bucket.latencySum / bucket.eventCount : 0,
432
- }));
433
- }
434
- function averagePercentiles(values) {
435
- if (values.length === 0) {
436
- return { p50: 0, p90: 0, p99: 0 };
437
- }
438
- return {
439
- p50: values.reduce((acc, value) => acc + value.p50, 0) / values.length,
440
- p90: values.reduce((acc, value) => acc + value.p90, 0) / values.length,
441
- p99: values.reduce((acc, value) => acc + value.p99, 0) / values.length,
442
- };
443
- }
444
- function sanitizeForwardQueryParams(query) {
445
- const sanitized = new URLSearchParams(query);
446
- sanitized.delete('instanceId');
447
- sanitized.delete('instanceIds');
448
- return sanitized;
449
- }
450
- function withPaging(params, paging) {
451
- const next = new URLSearchParams(params);
452
- next.set('limit', String(paging.limit));
453
- next.set('offset', String(paging.offset));
454
- return next;
455
- }
456
- function buildConsoleEndpointUrl(args) {
457
- const baseUrl = resolveBaseUrl(args.instance.baseUrl, args.requestUrl);
458
- const basePath = baseUrl.pathname.endsWith('/')
459
- ? baseUrl.pathname.slice(0, -1)
460
- : baseUrl.pathname;
461
- const suffix = args.path.startsWith('/') ? args.path : `/${args.path}`;
462
- baseUrl.pathname = `${basePath}/console${suffix}`;
463
- baseUrl.search = args.query?.toString() ?? '';
464
- return baseUrl.toString();
465
- }
466
- function resolveForwardAuthorization(args) {
467
- if (args.instance.token) {
468
- return `Bearer ${args.instance.token}`;
469
- }
470
- const header = args.c.req.header('Authorization')?.trim();
471
- if (header) {
472
- return header;
473
- }
474
- return null;
475
- }
476
- function parseBearerToken(authHeader) {
477
- const value = authHeader?.trim();
478
- if (!value?.startsWith('Bearer ')) {
479
- return null;
480
- }
481
- const token = value.slice(7).trim();
482
- return token.length > 0 ? token : null;
483
- }
484
- async function fetchDownstreamJson(args) {
485
- const url = buildConsoleEndpointUrl({
486
- instance: args.instance,
487
- requestUrl: args.c.req.url,
488
- path: args.path,
489
- query: args.query,
490
- });
491
- const headers = new Headers();
492
- headers.set('Accept', 'application/json');
493
- const authorization = resolveForwardAuthorization({
494
- c: args.c,
495
- instance: args.instance,
496
- });
497
- if (authorization) {
498
- headers.set('Authorization', authorization);
499
- }
500
- try {
501
- const response = await args.fetchImpl(url, {
502
- method: 'GET',
503
- headers,
504
- });
505
- if (!response.ok) {
506
- return {
507
- ok: false,
508
- failure: {
509
- instanceId: args.instance.instanceId,
510
- reason: `HTTP ${response.status}`,
511
- status: response.status,
512
- },
513
- };
514
- }
515
- const payload = await response.json();
516
- const parsed = args.schema.safeParse(payload);
517
- if (!parsed.success) {
518
- return {
519
- ok: false,
520
- failure: {
521
- instanceId: args.instance.instanceId,
522
- reason: 'Invalid response payload',
523
- },
524
- };
525
- }
526
- return { ok: true, data: parsed.data };
527
- }
528
- catch (error) {
529
- return {
530
- ok: false,
531
- failure: {
532
- instanceId: args.instance.instanceId,
533
- reason: toErrorMessage(error),
534
- },
535
- };
536
- }
537
- }
538
- async function parseDownstreamBody(response) {
539
- const text = await response.text();
540
- if (!text.trim()) {
541
- return null;
542
- }
543
- try {
544
- return JSON.parse(text);
545
- }
546
- catch {
547
- return text;
548
- }
549
- }
550
- function normalizeDownstreamError(args) {
551
- if (args.body && typeof args.body === 'object' && !Array.isArray(args.body)) {
552
- return {
553
- ...args.body,
554
- instanceId: args.instanceId,
555
- };
556
- }
557
- if (typeof args.body === 'string' && args.body.trim().length > 0) {
558
- return {
559
- error: 'DOWNSTREAM_ERROR',
560
- message: args.body,
561
- instanceId: args.instanceId,
562
- };
563
- }
564
- return {
565
- error: 'DOWNSTREAM_ERROR',
566
- status: args.status,
567
- instanceId: args.instanceId,
568
- };
569
- }
570
- async function forwardDownstreamJsonRequest(args) {
571
- const url = buildConsoleEndpointUrl({
572
- instance: args.instance,
573
- requestUrl: args.c.req.url,
574
- path: args.path,
575
- query: args.query,
576
- });
577
- const headers = new Headers();
578
- headers.set('Accept', 'application/json');
579
- const authorization = resolveForwardAuthorization({
580
- c: args.c,
581
- instance: args.instance,
582
- });
583
- if (authorization) {
584
- headers.set('Authorization', authorization);
585
- }
586
- let requestBody;
587
- if (args.body !== undefined) {
588
- headers.set('Content-Type', 'application/json');
589
- requestBody = JSON.stringify(args.body);
590
- }
591
- try {
592
- const response = await args.fetchImpl(url, {
593
- method: args.method,
594
- headers,
595
- ...(requestBody !== undefined ? { body: requestBody } : {}),
596
- });
597
- const payload = await parseDownstreamBody(response);
598
- if (!response.ok) {
599
- return {
600
- ok: false,
601
- status: response.status,
602
- body: normalizeDownstreamError({
603
- body: payload,
604
- status: response.status,
605
- instanceId: args.instance.instanceId,
606
- }),
607
- };
608
- }
609
- const parsed = args.responseSchema.safeParse(payload);
610
- if (!parsed.success) {
611
- return {
612
- ok: false,
613
- status: 502,
614
- body: {
615
- error: 'INVALID_DOWNSTREAM_RESPONSE',
616
- message: 'Downstream response failed validation.',
617
- instanceId: args.instance.instanceId,
618
- },
619
- };
620
- }
621
- return {
622
- ok: true,
623
- data: parsed.data,
624
- status: response.status,
625
- };
626
- }
627
- catch (error) {
628
- return {
629
- ok: false,
630
- status: 502,
631
- body: {
632
- error: 'DOWNSTREAM_UNAVAILABLE',
633
- message: toErrorMessage(error),
634
- instanceId: args.instance.instanceId,
635
- },
636
- };
637
- }
638
- }
639
- async function fetchDownstreamPaged(args) {
640
- const items = [];
641
- let total = null;
642
- let localOffset = 0;
643
- let pageCount = 0;
644
- while (items.length < args.targetCount &&
645
- (total === null || localOffset < total) &&
646
- pageCount < 100) {
647
- const limit = Math.min(100, Math.max(1, args.targetCount - items.length));
648
- const pagedQuery = withPaging(args.query, { limit, offset: localOffset });
649
- const result = await fetchDownstreamJson({
650
- c: args.c,
651
- instance: args.instance,
652
- path: args.path,
653
- query: pagedQuery,
654
- schema: args.schema,
655
- fetchImpl: args.fetchImpl,
656
- });
657
- if (!result.ok) {
658
- return result;
659
- }
660
- const page = result.data;
661
- total = page.total;
662
- items.push(...page.items);
663
- localOffset += page.items.length;
664
- pageCount += 1;
665
- if (page.items.length === 0) {
666
- break;
667
- }
668
- }
669
- return {
670
- ok: true,
671
- items,
672
- total: total ?? items.length,
673
- };
674
- }
675
- async function checkDownstreamInstanceHealth(args) {
676
- const startedAt = Date.now();
677
- const result = await fetchDownstreamJson({
678
- c: args.c,
679
- instance: args.instance,
680
- path: '/stats',
681
- schema: SyncStatsSchema,
682
- fetchImpl: args.fetchImpl,
683
- });
684
- const responseTimeMs = Math.max(0, Date.now() - startedAt);
685
- const checkedAt = new Date().toISOString();
686
- const base = {
687
- instanceId: args.instance.instanceId,
688
- label: args.instance.label ?? args.instance.instanceId,
689
- baseUrl: args.instance.baseUrl,
690
- enabled: args.instance.enabled ?? true,
691
- responseTimeMs,
692
- checkedAt,
693
- };
694
- if (result.ok) {
695
- return {
696
- ...base,
697
- healthy: true,
698
- status: 200,
699
- };
700
- }
701
- return {
702
- ...base,
703
- healthy: false,
704
- status: result.failure.status,
705
- reason: result.failure.reason,
706
- };
707
- }
708
- function unauthorizedResponse(c) {
709
- return c.json({ error: 'UNAUTHORIZED' }, 401);
710
- }
711
- function jsonResponse(payload, status) {
712
- return new Response(JSON.stringify(payload), {
713
- status,
714
- headers: {
715
- 'content-type': 'application/json; charset=utf-8',
716
- },
717
- });
718
- }
719
- function allInstancesFailedResponse(c, failedInstances) {
720
- return c.json({
721
- error: 'DOWNSTREAM_UNAVAILABLE',
722
- failedInstances,
723
- }, 502);
724
- }
725
- export function createConsoleGatewayRoutes(options) {
726
- const routes = new Hono();
727
- const instances = normalizeInstances(options.instances);
728
- const fetchImpl = options.fetchImpl ?? fetch;
729
- const corsOrigins = options.corsOrigins ?? '*';
730
- routes.use('*', cors({
731
- origin: corsOrigins === '*' ? '*' : corsOrigins,
732
- allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
733
- allowHeaders: [
734
- 'Content-Type',
735
- 'Authorization',
736
- 'X-Syncular-Transport-Path',
737
- 'Baggage',
738
- 'Sentry-Trace',
739
- 'Traceparent',
740
- 'Tracestate',
741
- ],
742
- credentials: true,
743
- }));
744
- routes.get('/instances', describeRoute({
745
- tags: ['console-gateway'],
746
- summary: 'List configured downstream console instances',
747
- responses: {
748
- 200: {
749
- description: 'Configured instances',
750
- content: {
751
- 'application/json': {
752
- schema: resolver(GatewayInstancesResponseSchema),
753
- },
754
- },
755
- },
756
- 401: {
757
- description: 'Unauthenticated',
758
- content: {
759
- 'application/json': {
760
- schema: resolver(z.object({ error: z.string() })),
761
- },
762
- },
763
- },
764
- },
765
- }), async (c) => {
766
- const auth = await options.authenticate(c);
767
- if (!auth) {
768
- return unauthorizedResponse(c);
769
- }
770
- return c.json({
771
- items: instances.map((instance) => ({
772
- instanceId: instance.instanceId,
773
- label: instance.label ?? instance.instanceId,
774
- baseUrl: instance.baseUrl,
775
- enabled: instance.enabled ?? true,
776
- })),
777
- });
778
- });
779
- routes.get('/instances/health', describeRoute({
780
- tags: ['console-gateway'],
781
- summary: 'Probe downstream console health by instance',
782
- responses: {
783
- 200: {
784
- description: 'Per-instance health results',
785
- content: {
786
- 'application/json': {
787
- schema: resolver(GatewayInstancesHealthResponseSchema),
788
- },
789
- },
790
- },
791
- 401: {
792
- description: 'Unauthenticated',
793
- content: {
794
- 'application/json': {
795
- schema: resolver(z.object({ error: z.string() })),
796
- },
797
- },
798
- },
799
- },
800
- }), zValidator('query', GatewayInstanceFilterSchema), async (c) => {
801
- const auth = await options.authenticate(c);
802
- if (!auth) {
803
- return unauthorizedResponse(c);
804
- }
805
- const query = c.req.valid('query');
806
- const selectedInstances = selectInstances({ instances, query });
807
- if (selectedInstances.length === 0) {
808
- return c.json({
809
- error: 'NO_INSTANCES_SELECTED',
810
- message: 'No enabled instances matched the provided instance filter.',
811
- }, 400);
812
- }
813
- const items = await Promise.all(selectedInstances.map((instance) => checkDownstreamInstanceHealth({
814
- c,
815
- instance,
816
- fetchImpl,
817
- })));
818
- const failedInstances = items
819
- .filter((item) => !item.healthy)
820
- .map((item) => ({
821
- instanceId: item.instanceId,
822
- reason: item.reason ?? 'Health probe failed',
823
- ...(item.status !== undefined ? { status: item.status } : {}),
824
- }));
825
- return c.json({
826
- items,
827
- partial: failedInstances.length > 0,
828
- failedInstances,
829
- });
830
- });
831
- routes.get('/handlers', describeRoute({
832
- tags: ['console-gateway'],
833
- summary: 'List handlers for a single target instance (requires instance selection)',
834
- responses: {
835
- 200: {
836
- description: 'Handlers',
837
- content: {
838
- 'application/json': {
839
- schema: resolver(GatewayHandlersResponseSchema),
840
- },
841
- },
842
- },
843
- },
844
- }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
845
- const auth = await options.authenticate(c);
846
- if (!auth) {
847
- return unauthorizedResponse(c);
848
- }
849
- const query = c.req.valid('query');
850
- const target = resolveSingleInstanceTarget({ instances, query });
851
- if (!target.ok) {
852
- return c.json({
853
- error: target.error,
854
- message: target.message,
855
- }, target.status);
856
- }
857
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
858
- const result = await forwardDownstreamJsonRequest({
859
- c,
860
- instance: target.instance,
861
- method: 'GET',
862
- path: '/handlers',
863
- query: forwardQuery,
864
- responseSchema: GatewayHandlersResponseSchema,
865
- fetchImpl,
866
- });
867
- if (!result.ok) {
868
- return jsonResponse(result.body, result.status);
869
- }
870
- return jsonResponse(result.data, result.status);
871
- });
872
- routes.post('/prune/preview', describeRoute({
873
- tags: ['console-gateway'],
874
- summary: 'Preview prune on a single target instance (requires instance selection)',
875
- responses: {
876
- 200: {
877
- description: 'Prune preview',
878
- content: {
879
- 'application/json': {
880
- schema: resolver(ConsolePrunePreviewSchema),
881
- },
882
- },
883
- },
884
- },
885
- }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
886
- const auth = await options.authenticate(c);
887
- if (!auth) {
888
- return unauthorizedResponse(c);
889
- }
890
- const query = c.req.valid('query');
891
- const target = resolveSingleInstanceTarget({ instances, query });
892
- if (!target.ok) {
893
- return c.json({
894
- error: target.error,
895
- message: target.message,
896
- }, target.status);
897
- }
898
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
899
- const result = await forwardDownstreamJsonRequest({
900
- c,
901
- instance: target.instance,
902
- method: 'POST',
903
- path: '/prune/preview',
904
- query: forwardQuery,
905
- responseSchema: ConsolePrunePreviewSchema,
906
- fetchImpl,
907
- });
908
- if (!result.ok) {
909
- return jsonResponse(result.body, result.status);
910
- }
911
- return jsonResponse(result.data, result.status);
912
- });
913
- routes.post('/prune', describeRoute({
914
- tags: ['console-gateway'],
915
- summary: 'Trigger prune on a single target instance (requires instance selection)',
916
- responses: {
917
- 200: {
918
- description: 'Prune result',
919
- content: {
920
- 'application/json': {
921
- schema: resolver(ConsolePruneResultSchema),
922
- },
923
- },
924
- },
925
- },
926
- }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
927
- const auth = await options.authenticate(c);
928
- if (!auth) {
929
- return unauthorizedResponse(c);
930
- }
931
- const query = c.req.valid('query');
932
- const target = resolveSingleInstanceTarget({ instances, query });
933
- if (!target.ok) {
934
- return c.json({
935
- error: target.error,
936
- message: target.message,
937
- }, target.status);
938
- }
939
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
940
- const result = await forwardDownstreamJsonRequest({
941
- c,
942
- instance: target.instance,
943
- method: 'POST',
944
- path: '/prune',
945
- query: forwardQuery,
946
- responseSchema: ConsolePruneResultSchema,
947
- fetchImpl,
948
- });
949
- if (!result.ok) {
950
- return jsonResponse(result.body, result.status);
951
- }
952
- return jsonResponse(result.data, result.status);
953
- });
954
- routes.post('/compact', describeRoute({
955
- tags: ['console-gateway'],
956
- summary: 'Trigger compaction on a single target instance (requires instance selection)',
957
- responses: {
958
- 200: {
959
- description: 'Compaction result',
960
- content: {
961
- 'application/json': {
962
- schema: resolver(ConsoleCompactResultSchema),
963
- },
964
- },
965
- },
966
- },
967
- }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
968
- const auth = await options.authenticate(c);
969
- if (!auth) {
970
- return unauthorizedResponse(c);
971
- }
972
- const query = c.req.valid('query');
973
- const target = resolveSingleInstanceTarget({ instances, query });
974
- if (!target.ok) {
975
- return c.json({
976
- error: target.error,
977
- message: target.message,
978
- }, target.status);
979
- }
980
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
981
- const result = await forwardDownstreamJsonRequest({
982
- c,
983
- instance: target.instance,
984
- method: 'POST',
985
- path: '/compact',
986
- query: forwardQuery,
987
- responseSchema: ConsoleCompactResultSchema,
988
- fetchImpl,
989
- });
990
- if (!result.ok) {
991
- return jsonResponse(result.body, result.status);
992
- }
993
- return jsonResponse(result.data, result.status);
994
- });
995
- routes.post('/notify-data-change', describeRoute({
996
- tags: ['console-gateway'],
997
- summary: 'Notify data change on a single target instance (requires instance selection)',
998
- responses: {
999
- 200: {
1000
- description: 'Notification result',
1001
- content: {
1002
- 'application/json': {
1003
- schema: resolver(GatewayNotifyDataChangeResponseSchema),
1004
- },
1005
- },
1006
- },
1007
- },
1008
- }), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', GatewayNotifyDataChangeRequestSchema), async (c) => {
1009
- const auth = await options.authenticate(c);
1010
- if (!auth) {
1011
- return unauthorizedResponse(c);
1012
- }
1013
- const query = c.req.valid('query');
1014
- const body = c.req.valid('json');
1015
- const target = resolveSingleInstanceTarget({ instances, query });
1016
- if (!target.ok) {
1017
- return c.json({
1018
- error: target.error,
1019
- message: target.message,
1020
- }, target.status);
1021
- }
1022
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1023
- const result = await forwardDownstreamJsonRequest({
1024
- c,
1025
- instance: target.instance,
1026
- method: 'POST',
1027
- path: '/notify-data-change',
1028
- query: forwardQuery,
1029
- body,
1030
- responseSchema: GatewayNotifyDataChangeResponseSchema,
1031
- fetchImpl,
1032
- });
1033
- if (!result.ok) {
1034
- return jsonResponse(result.body, result.status);
1035
- }
1036
- return jsonResponse(result.data, result.status);
1037
- });
1038
- routes.delete('/clients/:id', describeRoute({
1039
- tags: ['console-gateway'],
1040
- summary: 'Evict client on a single target instance (requires instance selection)',
1041
- responses: {
1042
- 200: {
1043
- description: 'Evict result',
1044
- content: {
1045
- 'application/json': {
1046
- schema: resolver(ConsoleEvictResultSchema),
1047
- },
1048
- },
1049
- },
1050
- },
1051
- }), zValidator('param', GatewayClientPathParamSchema), zValidator('query', GatewaySingleInstancePartitionQuerySchema), async (c) => {
1052
- const auth = await options.authenticate(c);
1053
- if (!auth) {
1054
- return unauthorizedResponse(c);
1055
- }
1056
- const { id } = c.req.valid('param');
1057
- const query = c.req.valid('query');
1058
- const target = resolveSingleInstanceTarget({ instances, query });
1059
- if (!target.ok) {
1060
- return c.json({
1061
- error: target.error,
1062
- message: target.message,
1063
- }, target.status);
1064
- }
1065
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1066
- const result = await forwardDownstreamJsonRequest({
1067
- c,
1068
- instance: target.instance,
1069
- method: 'DELETE',
1070
- path: `/clients/${encodeURIComponent(id)}`,
1071
- query: forwardQuery,
1072
- responseSchema: ConsoleEvictResultSchema,
1073
- fetchImpl,
1074
- });
1075
- if (!result.ok) {
1076
- return jsonResponse(result.body, result.status);
1077
- }
1078
- return jsonResponse(result.data, result.status);
1079
- });
1080
- routes.delete('/events', describeRoute({
1081
- tags: ['console-gateway'],
1082
- summary: 'Clear request events on a single target instance (requires instance selection)',
1083
- responses: {
1084
- 200: {
1085
- description: 'Clear result',
1086
- content: {
1087
- 'application/json': {
1088
- schema: resolver(ConsoleClearEventsResultSchema),
1089
- },
1090
- },
1091
- },
1092
- },
1093
- }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1094
- const auth = await options.authenticate(c);
1095
- if (!auth) {
1096
- return unauthorizedResponse(c);
1097
- }
1098
- const query = c.req.valid('query');
1099
- const target = resolveSingleInstanceTarget({ instances, query });
1100
- if (!target.ok) {
1101
- return c.json({
1102
- error: target.error,
1103
- message: target.message,
1104
- }, target.status);
1105
- }
1106
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1107
- const result = await forwardDownstreamJsonRequest({
1108
- c,
1109
- instance: target.instance,
1110
- method: 'DELETE',
1111
- path: '/events',
1112
- query: forwardQuery,
1113
- responseSchema: ConsoleClearEventsResultSchema,
1114
- fetchImpl,
1115
- });
1116
- if (!result.ok) {
1117
- return jsonResponse(result.body, result.status);
1118
- }
1119
- return jsonResponse(result.data, result.status);
1120
- });
1121
- routes.post('/events/prune', describeRoute({
1122
- tags: ['console-gateway'],
1123
- summary: 'Prune request events on a single target instance (requires instance selection)',
1124
- responses: {
1125
- 200: {
1126
- description: 'Prune events result',
1127
- content: {
1128
- 'application/json': {
1129
- schema: resolver(ConsolePruneEventsResultSchema),
1130
- },
1131
- },
1132
- },
1133
- },
1134
- }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1135
- const auth = await options.authenticate(c);
1136
- if (!auth) {
1137
- return unauthorizedResponse(c);
1138
- }
1139
- const query = c.req.valid('query');
1140
- const target = resolveSingleInstanceTarget({ instances, query });
1141
- if (!target.ok) {
1142
- return c.json({
1143
- error: target.error,
1144
- message: target.message,
1145
- }, target.status);
1146
- }
1147
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1148
- const result = await forwardDownstreamJsonRequest({
1149
- c,
1150
- instance: target.instance,
1151
- method: 'POST',
1152
- path: '/events/prune',
1153
- query: forwardQuery,
1154
- responseSchema: ConsolePruneEventsResultSchema,
1155
- fetchImpl,
1156
- });
1157
- if (!result.ok) {
1158
- return jsonResponse(result.body, result.status);
1159
- }
1160
- return jsonResponse(result.data, result.status);
1161
- });
1162
- routes.get('/api-keys', describeRoute({
1163
- tags: ['console-gateway'],
1164
- summary: 'List API keys for a single target instance (requires instance selection)',
1165
- responses: {
1166
- 200: {
1167
- description: 'Paginated API key list',
1168
- content: {
1169
- 'application/json': {
1170
- schema: resolver(ConsolePaginatedResponseSchema(ConsoleApiKeySchema)),
1171
- },
1172
- },
1173
- },
1174
- },
1175
- }), zValidator('query', GatewayApiKeysQuerySchema), async (c) => {
1176
- const auth = await options.authenticate(c);
1177
- if (!auth) {
1178
- return unauthorizedResponse(c);
1179
- }
1180
- const query = c.req.valid('query');
1181
- const target = resolveSingleInstanceTarget({ instances, query });
1182
- if (!target.ok) {
1183
- return c.json({
1184
- error: target.error,
1185
- message: target.message,
1186
- }, target.status);
1187
- }
1188
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1189
- const result = await forwardDownstreamJsonRequest({
1190
- c,
1191
- instance: target.instance,
1192
- method: 'GET',
1193
- path: '/api-keys',
1194
- query: forwardQuery,
1195
- responseSchema: ConsolePaginatedResponseSchema(ConsoleApiKeySchema),
1196
- fetchImpl,
1197
- });
1198
- if (!result.ok) {
1199
- return jsonResponse(result.body, result.status);
1200
- }
1201
- return jsonResponse(result.data, result.status);
1202
- });
1203
- routes.post('/api-keys', describeRoute({
1204
- tags: ['console-gateway'],
1205
- summary: 'Create API key on a single target instance (requires instance selection)',
1206
- responses: {
1207
- 201: {
1208
- description: 'Created API key',
1209
- content: {
1210
- 'application/json': {
1211
- schema: resolver(ConsoleApiKeyCreateResponseSchema),
1212
- },
1213
- },
1214
- },
1215
- },
1216
- }), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', ConsoleApiKeyCreateRequestSchema), async (c) => {
1217
- const auth = await options.authenticate(c);
1218
- if (!auth) {
1219
- return unauthorizedResponse(c);
1220
- }
1221
- const query = c.req.valid('query');
1222
- const body = c.req.valid('json');
1223
- const target = resolveSingleInstanceTarget({ instances, query });
1224
- if (!target.ok) {
1225
- return c.json({
1226
- error: target.error,
1227
- message: target.message,
1228
- }, target.status);
1229
- }
1230
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1231
- const result = await forwardDownstreamJsonRequest({
1232
- c,
1233
- instance: target.instance,
1234
- method: 'POST',
1235
- path: '/api-keys',
1236
- query: forwardQuery,
1237
- body,
1238
- responseSchema: ConsoleApiKeyCreateResponseSchema,
1239
- fetchImpl,
1240
- });
1241
- if (!result.ok) {
1242
- return jsonResponse(result.body, result.status);
1243
- }
1244
- return jsonResponse(result.data, result.status);
1245
- });
1246
- routes.get('/api-keys/:id', describeRoute({
1247
- tags: ['console-gateway'],
1248
- summary: 'Get API key from a single target instance (requires instance selection)',
1249
- responses: {
1250
- 200: {
1251
- description: 'API key details',
1252
- content: {
1253
- 'application/json': {
1254
- schema: resolver(ConsoleApiKeySchema),
1255
- },
1256
- },
1257
- },
1258
- },
1259
- }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1260
- const auth = await options.authenticate(c);
1261
- if (!auth) {
1262
- return unauthorizedResponse(c);
1263
- }
1264
- const { id } = c.req.valid('param');
1265
- const query = c.req.valid('query');
1266
- const target = resolveSingleInstanceTarget({ instances, query });
1267
- if (!target.ok) {
1268
- return c.json({
1269
- error: target.error,
1270
- message: target.message,
1271
- }, target.status);
1272
- }
1273
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1274
- const result = await forwardDownstreamJsonRequest({
1275
- c,
1276
- instance: target.instance,
1277
- method: 'GET',
1278
- path: `/api-keys/${encodeURIComponent(id)}`,
1279
- query: forwardQuery,
1280
- responseSchema: ConsoleApiKeySchema,
1281
- fetchImpl,
1282
- });
1283
- if (!result.ok) {
1284
- return jsonResponse(result.body, result.status);
1285
- }
1286
- return jsonResponse(result.data, result.status);
1287
- });
1288
- routes.delete('/api-keys/:id', describeRoute({
1289
- tags: ['console-gateway'],
1290
- summary: 'Revoke API key on a single target instance (requires instance selection)',
1291
- responses: {
1292
- 200: {
1293
- description: 'Revoke result',
1294
- content: {
1295
- 'application/json': {
1296
- schema: resolver(ConsoleApiKeyRevokeResponseSchema),
1297
- },
1298
- },
1299
- },
1300
- },
1301
- }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1302
- const auth = await options.authenticate(c);
1303
- if (!auth) {
1304
- return unauthorizedResponse(c);
1305
- }
1306
- const { id } = c.req.valid('param');
1307
- const query = c.req.valid('query');
1308
- const target = resolveSingleInstanceTarget({ instances, query });
1309
- if (!target.ok) {
1310
- return c.json({
1311
- error: target.error,
1312
- message: target.message,
1313
- }, target.status);
1314
- }
1315
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1316
- const result = await forwardDownstreamJsonRequest({
1317
- c,
1318
- instance: target.instance,
1319
- method: 'DELETE',
1320
- path: `/api-keys/${encodeURIComponent(id)}`,
1321
- query: forwardQuery,
1322
- responseSchema: ConsoleApiKeyRevokeResponseSchema,
1323
- fetchImpl,
1324
- });
1325
- if (!result.ok) {
1326
- return jsonResponse(result.body, result.status);
1327
- }
1328
- return jsonResponse(result.data, result.status);
1329
- });
1330
- routes.post('/api-keys/bulk-revoke', describeRoute({
1331
- tags: ['console-gateway'],
1332
- summary: 'Bulk revoke API keys on a single target instance (requires instance selection)',
1333
- responses: {
1334
- 200: {
1335
- description: 'Bulk revoke result',
1336
- content: {
1337
- 'application/json': {
1338
- schema: resolver(ConsoleApiKeyBulkRevokeResponseSchema),
1339
- },
1340
- },
1341
- },
1342
- },
1343
- }), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', ConsoleApiKeyBulkRevokeRequestSchema), async (c) => {
1344
- const auth = await options.authenticate(c);
1345
- if (!auth) {
1346
- return unauthorizedResponse(c);
1347
- }
1348
- const query = c.req.valid('query');
1349
- const body = c.req.valid('json');
1350
- const target = resolveSingleInstanceTarget({ instances, query });
1351
- if (!target.ok) {
1352
- return c.json({
1353
- error: target.error,
1354
- message: target.message,
1355
- }, target.status);
1356
- }
1357
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1358
- const result = await forwardDownstreamJsonRequest({
1359
- c,
1360
- instance: target.instance,
1361
- method: 'POST',
1362
- path: '/api-keys/bulk-revoke',
1363
- query: forwardQuery,
1364
- body,
1365
- responseSchema: ConsoleApiKeyBulkRevokeResponseSchema,
1366
- fetchImpl,
1367
- });
1368
- if (!result.ok) {
1369
- return jsonResponse(result.body, result.status);
1370
- }
1371
- return jsonResponse(result.data, result.status);
1372
- });
1373
- routes.post('/api-keys/:id/rotate/stage', describeRoute({
1374
- tags: ['console-gateway'],
1375
- summary: 'Stage-rotate API key on a single target instance (requires instance selection)',
1376
- responses: {
1377
- 200: {
1378
- description: 'Staged API key replacement',
1379
- content: {
1380
- 'application/json': {
1381
- schema: resolver(ConsoleApiKeyCreateResponseSchema),
1382
- },
1383
- },
1384
- },
1385
- },
1386
- }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1387
- const auth = await options.authenticate(c);
1388
- if (!auth) {
1389
- return unauthorizedResponse(c);
1390
- }
1391
- const { id } = c.req.valid('param');
1392
- const query = c.req.valid('query');
1393
- const target = resolveSingleInstanceTarget({ instances, query });
1394
- if (!target.ok) {
1395
- return c.json({
1396
- error: target.error,
1397
- message: target.message,
1398
- }, target.status);
1399
- }
1400
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1401
- const result = await forwardDownstreamJsonRequest({
1402
- c,
1403
- instance: target.instance,
1404
- method: 'POST',
1405
- path: `/api-keys/${encodeURIComponent(id)}/rotate/stage`,
1406
- query: forwardQuery,
1407
- responseSchema: ConsoleApiKeyCreateResponseSchema,
1408
- fetchImpl,
1409
- });
1410
- if (!result.ok) {
1411
- return jsonResponse(result.body, result.status);
1412
- }
1413
- return jsonResponse(result.data, result.status);
1414
- });
1415
- routes.post('/api-keys/:id/rotate', describeRoute({
1416
- tags: ['console-gateway'],
1417
- summary: 'Rotate API key on a single target instance (requires instance selection)',
1418
- responses: {
1419
- 200: {
1420
- description: 'Rotated API key',
1421
- content: {
1422
- 'application/json': {
1423
- schema: resolver(ConsoleApiKeyCreateResponseSchema),
1424
- },
1425
- },
1426
- },
1427
- },
1428
- }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1429
- const auth = await options.authenticate(c);
1430
- if (!auth) {
1431
- return unauthorizedResponse(c);
1432
- }
1433
- const { id } = c.req.valid('param');
1434
- const query = c.req.valid('query');
1435
- const target = resolveSingleInstanceTarget({ instances, query });
1436
- if (!target.ok) {
1437
- return c.json({
1438
- error: target.error,
1439
- message: target.message,
1440
- }, target.status);
1441
- }
1442
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1443
- const result = await forwardDownstreamJsonRequest({
1444
- c,
1445
- instance: target.instance,
1446
- method: 'POST',
1447
- path: `/api-keys/${encodeURIComponent(id)}/rotate`,
1448
- query: forwardQuery,
1449
- responseSchema: ConsoleApiKeyCreateResponseSchema,
1450
- fetchImpl,
1451
- });
1452
- if (!result.ok) {
1453
- return jsonResponse(result.body, result.status);
1454
- }
1455
- return jsonResponse(result.data, result.status);
1456
- });
1457
- routes.get('/stats', describeRoute({
1458
- tags: ['console-gateway'],
1459
- summary: 'Get merged sync stats across instances',
1460
- responses: {
1461
- 200: {
1462
- description: 'Merged stats',
1463
- content: {
1464
- 'application/json': {
1465
- schema: resolver(GatewayStatsResponseSchema),
1466
- },
1467
- },
1468
- },
1469
- },
1470
- }), zValidator('query', GatewayStatsQuerySchema), async (c) => {
1471
- const auth = await options.authenticate(c);
1472
- if (!auth) {
1473
- return unauthorizedResponse(c);
1474
- }
1475
- const query = c.req.valid('query');
1476
- const selectedInstances = selectInstances({ instances, query });
1477
- if (selectedInstances.length === 0) {
1478
- return c.json({
1479
- error: 'NO_INSTANCES_SELECTED',
1480
- message: 'No enabled instances matched the provided instance filter.',
1481
- }, 400);
1482
- }
1483
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1484
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
1485
- c,
1486
- instance,
1487
- path: '/stats',
1488
- query: forwardQuery,
1489
- schema: SyncStatsSchema,
1490
- fetchImpl,
1491
- })));
1492
- const failedInstances = results
1493
- .filter((result) => !result.ok)
1494
- .map((result) => result.failure);
1495
- const successfulResults = results.filter((result) => result.ok);
1496
- if (successfulResults.length === 0) {
1497
- return allInstancesFailedResponse(c, failedInstances);
1498
- }
1499
- const statsByInstance = new Map();
1500
- for (let i = 0; i < selectedInstances.length; i++) {
1501
- const result = results[i];
1502
- if (!result || !result.ok)
1503
- continue;
1504
- const instance = selectedInstances[i];
1505
- if (!instance)
1506
- continue;
1507
- statsByInstance.set(instance.instanceId, result.data);
1508
- }
1509
- const statsValues = Array.from(statsByInstance.values());
1510
- const sum = (selector) => statsValues.reduce((acc, stats) => acc + selector(stats), 0);
1511
- const minCommitSeqByInstance = {};
1512
- const maxCommitSeqByInstance = {};
1513
- for (const [instanceId, stats] of statsByInstance.entries()) {
1514
- minCommitSeqByInstance[instanceId] = stats.minCommitSeq;
1515
- maxCommitSeqByInstance[instanceId] = stats.maxCommitSeq;
1516
- }
1517
- return c.json({
1518
- commitCount: sum((stats) => stats.commitCount),
1519
- changeCount: sum((stats) => stats.changeCount),
1520
- minCommitSeq: Math.min(...statsValues.map((stats) => stats.minCommitSeq)),
1521
- maxCommitSeq: Math.max(...statsValues.map((stats) => stats.maxCommitSeq)),
1522
- clientCount: sum((stats) => stats.clientCount),
1523
- activeClientCount: sum((stats) => stats.activeClientCount),
1524
- minActiveClientCursor: minNullable(statsValues.map((stats) => stats.minActiveClientCursor)),
1525
- maxActiveClientCursor: maxNullable(statsValues.map((stats) => stats.maxActiveClientCursor)),
1526
- minCommitSeqByInstance,
1527
- maxCommitSeqByInstance,
1528
- partial: failedInstances.length > 0,
1529
- failedInstances,
1530
- });
1531
- });
1532
- routes.get('/stats/timeseries', describeRoute({
1533
- tags: ['console-gateway'],
1534
- summary: 'Get merged time-series stats across instances',
1535
- responses: {
1536
- 200: {
1537
- description: 'Merged time-series stats',
1538
- content: {
1539
- 'application/json': {
1540
- schema: resolver(GatewayTimeseriesResponseSchema),
1541
- },
1542
- },
1543
- },
1544
- },
1545
- }), zValidator('query', GatewayTimeseriesQuerySchema), async (c) => {
1546
- const auth = await options.authenticate(c);
1547
- if (!auth) {
1548
- return unauthorizedResponse(c);
1549
- }
1550
- const query = c.req.valid('query');
1551
- const selectedInstances = selectInstances({ instances, query });
1552
- if (selectedInstances.length === 0) {
1553
- return c.json({
1554
- error: 'NO_INSTANCES_SELECTED',
1555
- message: 'No enabled instances matched the provided instance filter.',
1556
- }, 400);
1557
- }
1558
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1559
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
1560
- c,
1561
- instance,
1562
- path: '/stats/timeseries',
1563
- query: forwardQuery,
1564
- schema: TimeseriesStatsResponseSchema,
1565
- fetchImpl,
1566
- })));
1567
- const failedInstances = results
1568
- .filter((result) => !result.ok)
1569
- .map((result) => result.failure);
1570
- const successfulResults = results.filter((result) => result.ok);
1571
- if (successfulResults.length === 0) {
1572
- return allInstancesFailedResponse(c, failedInstances);
1573
- }
1574
- return c.json({
1575
- buckets: mergeTimeseriesBuckets(successfulResults.map((result) => result.data)),
1576
- interval: query.interval,
1577
- range: query.range,
1578
- partial: failedInstances.length > 0,
1579
- failedInstances,
1580
- });
1581
- });
1582
- routes.get('/stats/latency', describeRoute({
1583
- tags: ['console-gateway'],
1584
- summary: 'Get merged latency stats across instances',
1585
- responses: {
1586
- 200: {
1587
- description: 'Merged latency stats',
1588
- content: {
1589
- 'application/json': {
1590
- schema: resolver(GatewayLatencyResponseSchema),
1591
- },
1592
- },
1593
- },
1594
- },
1595
- }), zValidator('query', GatewayLatencyQuerySchema), async (c) => {
1596
- const auth = await options.authenticate(c);
1597
- if (!auth) {
1598
- return unauthorizedResponse(c);
1599
- }
1600
- const query = c.req.valid('query');
1601
- const selectedInstances = selectInstances({ instances, query });
1602
- if (selectedInstances.length === 0) {
1603
- return c.json({
1604
- error: 'NO_INSTANCES_SELECTED',
1605
- message: 'No enabled instances matched the provided instance filter.',
1606
- }, 400);
1607
- }
1608
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1609
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
1610
- c,
1611
- instance,
1612
- path: '/stats/latency',
1613
- query: forwardQuery,
1614
- schema: LatencyStatsResponseSchema,
1615
- fetchImpl,
1616
- })));
1617
- const failedInstances = results
1618
- .filter((result) => !result.ok)
1619
- .map((result) => result.failure);
1620
- const successfulResults = results.filter((result) => result.ok);
1621
- if (successfulResults.length === 0) {
1622
- return allInstancesFailedResponse(c, failedInstances);
1623
- }
1624
- return c.json({
1625
- push: averagePercentiles(successfulResults.map((result) => result.data.push)),
1626
- pull: averagePercentiles(successfulResults.map((result) => result.data.pull)),
1627
- range: query.range,
1628
- partial: failedInstances.length > 0,
1629
- failedInstances,
1630
- });
1631
- });
1632
- routes.get('/commits', describeRoute({
1633
- tags: ['console-gateway'],
1634
- summary: 'List merged commits across instances',
1635
- responses: {
1636
- 200: {
1637
- description: 'Merged commits',
1638
- content: {
1639
- 'application/json': {
1640
- schema: resolver(GatewayPaginatedResponseSchema(GatewayCommitItemSchema)),
1641
- },
1642
- },
1643
- },
1644
- },
1645
- }), zValidator('query', GatewayPaginatedQuerySchema), async (c) => {
1646
- const auth = await options.authenticate(c);
1647
- if (!auth) {
1648
- return unauthorizedResponse(c);
1649
- }
1650
- const query = c.req.valid('query');
1651
- const selectedInstances = selectInstances({ instances, query });
1652
- if (selectedInstances.length === 0) {
1653
- return c.json({
1654
- error: 'NO_INSTANCES_SELECTED',
1655
- message: 'No enabled instances matched the provided instance filter.',
1656
- }, 400);
1657
- }
1658
- const targetCount = query.offset + query.limit;
1659
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1660
- forwardQuery.delete('limit');
1661
- forwardQuery.delete('offset');
1662
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleCommitListItemSchema);
1663
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1664
- c,
1665
- instance,
1666
- path: '/commits',
1667
- query: forwardQuery,
1668
- targetCount,
1669
- schema: pageSchema,
1670
- fetchImpl,
1671
- })));
1672
- const failedInstances = results
1673
- .filter((result) => !result.ok)
1674
- .map((result) => result.failure);
1675
- const successful = results
1676
- .map((result, index) => ({
1677
- result,
1678
- instance: selectedInstances[index],
1679
- }))
1680
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1681
- if (successful.length === 0) {
1682
- return allInstancesFailedResponse(c, failedInstances);
1683
- }
1684
- const merged = successful
1685
- .flatMap(({ result, instance }) => result.items.map((commit) => ({
1686
- ...commit,
1687
- instanceId: instance.instanceId,
1688
- federatedCommitId: `${instance.instanceId}:${commit.commitSeq}`,
1689
- })))
1690
- .sort((a, b) => {
1691
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
1692
- if (byTime !== 0)
1693
- return byTime;
1694
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1695
- if (byInstance !== 0)
1696
- return byInstance;
1697
- return b.commitSeq - a.commitSeq;
1698
- });
1699
- return c.json({
1700
- items: merged.slice(query.offset, query.offset + query.limit),
1701
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1702
- offset: query.offset,
1703
- limit: query.limit,
1704
- partial: failedInstances.length > 0,
1705
- failedInstances,
1706
- });
1707
- });
1708
- routes.get('/commits/:seq', describeRoute({
1709
- tags: ['console-gateway'],
1710
- summary: 'Get merged commit detail by federated id',
1711
- responses: {
1712
- 200: {
1713
- description: 'Commit detail',
1714
- content: {
1715
- 'application/json': {
1716
- schema: resolver(GatewayCommitDetailSchema),
1717
- },
1718
- },
1719
- },
1720
- },
1721
- }), zValidator('param', GatewayCommitPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
1722
- const auth = await options.authenticate(c);
1723
- if (!auth) {
1724
- return unauthorizedResponse(c);
1725
- }
1726
- const { seq } = c.req.valid('param');
1727
- const query = c.req.valid('query');
1728
- const target = resolveCommitTarget({ seq, instances, query });
1729
- if (!target.ok) {
1730
- return c.json({
1731
- error: target.error,
1732
- ...(target.message ? { message: target.message } : {}),
1733
- }, target.status);
1734
- }
1735
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1736
- const result = await fetchDownstreamJson({
1737
- c,
1738
- instance: target.instance,
1739
- path: `/commits/${target.localCommitSeq}`,
1740
- query: forwardQuery,
1741
- schema: ConsoleCommitDetailSchema,
1742
- fetchImpl,
1743
- });
1744
- if (!result.ok) {
1745
- if (result.failure.status === 404) {
1746
- return c.json({ error: 'NOT_FOUND' }, 404);
1747
- }
1748
- return c.json({
1749
- error: 'DOWNSTREAM_UNAVAILABLE',
1750
- failedInstances: [result.failure],
1751
- }, 502);
1752
- }
1753
- return c.json({
1754
- ...result.data,
1755
- instanceId: target.instance.instanceId,
1756
- federatedCommitId: `${target.instance.instanceId}:${result.data.commitSeq}`,
1757
- localCommitSeq: result.data.commitSeq,
1758
- });
1759
- });
1760
- routes.get('/clients', describeRoute({
1761
- tags: ['console-gateway'],
1762
- summary: 'List merged clients across instances',
1763
- responses: {
1764
- 200: {
1765
- description: 'Merged clients',
1766
- content: {
1767
- 'application/json': {
1768
- schema: resolver(GatewayPaginatedResponseSchema(GatewayClientItemSchema)),
1769
- },
1770
- },
1771
- },
1772
- },
1773
- }), zValidator('query', GatewayPaginatedQuerySchema), async (c) => {
1774
- const auth = await options.authenticate(c);
1775
- if (!auth) {
1776
- return unauthorizedResponse(c);
1777
- }
1778
- const query = c.req.valid('query');
1779
- const selectedInstances = selectInstances({ instances, query });
1780
- if (selectedInstances.length === 0) {
1781
- return c.json({
1782
- error: 'NO_INSTANCES_SELECTED',
1783
- message: 'No enabled instances matched the provided instance filter.',
1784
- }, 400);
1785
- }
1786
- const targetCount = query.offset + query.limit;
1787
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1788
- forwardQuery.delete('limit');
1789
- forwardQuery.delete('offset');
1790
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleClientSchema);
1791
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1792
- c,
1793
- instance,
1794
- path: '/clients',
1795
- query: forwardQuery,
1796
- targetCount,
1797
- schema: pageSchema,
1798
- fetchImpl,
1799
- })));
1800
- const failedInstances = results
1801
- .filter((result) => !result.ok)
1802
- .map((result) => result.failure);
1803
- const successful = results
1804
- .map((result, index) => ({
1805
- result,
1806
- instance: selectedInstances[index],
1807
- }))
1808
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1809
- if (successful.length === 0) {
1810
- return allInstancesFailedResponse(c, failedInstances);
1811
- }
1812
- const merged = successful
1813
- .flatMap(({ result, instance }) => result.items.map((client) => ({
1814
- ...client,
1815
- instanceId: instance.instanceId,
1816
- federatedClientId: `${instance.instanceId}:${client.clientId}`,
1817
- })))
1818
- .sort((a, b) => {
1819
- const byTime = compareIsoDesc(a.updatedAt, b.updatedAt);
1820
- if (byTime !== 0)
1821
- return byTime;
1822
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1823
- if (byInstance !== 0)
1824
- return byInstance;
1825
- return a.clientId.localeCompare(b.clientId);
1826
- });
1827
- return c.json({
1828
- items: merged.slice(query.offset, query.offset + query.limit),
1829
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1830
- offset: query.offset,
1831
- limit: query.limit,
1832
- partial: failedInstances.length > 0,
1833
- failedInstances,
1834
- });
1835
- });
1836
- routes.get('/timeline', describeRoute({
1837
- tags: ['console-gateway'],
1838
- summary: 'List merged timeline items across instances',
1839
- responses: {
1840
- 200: {
1841
- description: 'Merged timeline',
1842
- content: {
1843
- 'application/json': {
1844
- schema: resolver(GatewayPaginatedResponseSchema(GatewayTimelineItemSchema)),
1845
- },
1846
- },
1847
- },
1848
- },
1849
- }), zValidator('query', GatewayTimelineQuerySchema), async (c) => {
1850
- const auth = await options.authenticate(c);
1851
- if (!auth) {
1852
- return unauthorizedResponse(c);
1853
- }
1854
- const query = c.req.valid('query');
1855
- const selectedInstances = selectInstances({ instances, query });
1856
- if (selectedInstances.length === 0) {
1857
- return c.json({
1858
- error: 'NO_INSTANCES_SELECTED',
1859
- message: 'No enabled instances matched the provided instance filter.',
1860
- }, 400);
1861
- }
1862
- const targetCount = query.offset + query.limit;
1863
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1864
- forwardQuery.delete('limit');
1865
- forwardQuery.delete('offset');
1866
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleTimelineItemSchema);
1867
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1868
- c,
1869
- instance,
1870
- path: '/timeline',
1871
- query: forwardQuery,
1872
- targetCount,
1873
- schema: pageSchema,
1874
- fetchImpl,
1875
- })));
1876
- const failedInstances = results
1877
- .filter((result) => !result.ok)
1878
- .map((result) => result.failure);
1879
- const successful = results
1880
- .map((result, index) => ({
1881
- result,
1882
- instance: selectedInstances[index],
1883
- }))
1884
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1885
- if (successful.length === 0) {
1886
- return allInstancesFailedResponse(c, failedInstances);
1887
- }
1888
- const merged = successful
1889
- .flatMap(({ result, instance }) => result.items.map((item) => {
1890
- const localCommitSeq = item.type === 'commit' ? (item.commit?.commitSeq ?? null) : null;
1891
- const localEventId = item.type === 'event' ? (item.event?.eventId ?? null) : null;
1892
- const localIdSegment = item.type === 'commit'
1893
- ? String(localCommitSeq ?? 'unknown')
1894
- : String(localEventId ?? 'unknown');
1895
- return {
1896
- ...item,
1897
- instanceId: instance.instanceId,
1898
- federatedTimelineId: `${instance.instanceId}:${item.type}:${localIdSegment}`,
1899
- localCommitSeq,
1900
- localEventId,
1901
- };
1902
- }))
1903
- .sort((a, b) => {
1904
- const byTime = compareIsoDesc(a.timestamp, b.timestamp);
1905
- if (byTime !== 0)
1906
- return byTime;
1907
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1908
- if (byInstance !== 0)
1909
- return byInstance;
1910
- const aLocalId = a.localCommitSeq ?? a.localEventId ?? 0;
1911
- const bLocalId = b.localCommitSeq ?? b.localEventId ?? 0;
1912
- return bLocalId - aLocalId;
1913
- });
1914
- return c.json({
1915
- items: merged.slice(query.offset, query.offset + query.limit),
1916
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1917
- offset: query.offset,
1918
- limit: query.limit,
1919
- partial: failedInstances.length > 0,
1920
- failedInstances,
1921
- });
1922
- });
1923
- routes.get('/operations', describeRoute({
1924
- tags: ['console-gateway'],
1925
- summary: 'List merged operation events across instances',
1926
- responses: {
1927
- 200: {
1928
- description: 'Merged operations',
1929
- content: {
1930
- 'application/json': {
1931
- schema: resolver(GatewayPaginatedResponseSchema(GatewayOperationItemSchema)),
1932
- },
1933
- },
1934
- },
1935
- },
1936
- }), zValidator('query', GatewayOperationsQuerySchema), async (c) => {
1937
- const auth = await options.authenticate(c);
1938
- if (!auth) {
1939
- return unauthorizedResponse(c);
1940
- }
1941
- const query = c.req.valid('query');
1942
- const selectedInstances = selectInstances({ instances, query });
1943
- if (selectedInstances.length === 0) {
1944
- return c.json({
1945
- error: 'NO_INSTANCES_SELECTED',
1946
- message: 'No enabled instances matched the provided instance filter.',
1947
- }, 400);
1948
- }
1949
- const targetCount = query.offset + query.limit;
1950
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1951
- forwardQuery.delete('limit');
1952
- forwardQuery.delete('offset');
1953
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleOperationEventSchema);
1954
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1955
- c,
1956
- instance,
1957
- path: '/operations',
1958
- query: forwardQuery,
1959
- targetCount,
1960
- schema: pageSchema,
1961
- fetchImpl,
1962
- })));
1963
- const failedInstances = results
1964
- .filter((result) => !result.ok)
1965
- .map((result) => result.failure);
1966
- const successful = results
1967
- .map((result, index) => ({
1968
- result,
1969
- instance: selectedInstances[index],
1970
- }))
1971
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1972
- if (successful.length === 0) {
1973
- return allInstancesFailedResponse(c, failedInstances);
1974
- }
1975
- const merged = successful
1976
- .flatMap(({ result, instance }) => result.items.map((operation) => ({
1977
- ...operation,
1978
- instanceId: instance.instanceId,
1979
- federatedOperationId: `${instance.instanceId}:${operation.operationId}`,
1980
- localOperationId: operation.operationId,
1981
- })))
1982
- .sort((a, b) => {
1983
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
1984
- if (byTime !== 0)
1985
- return byTime;
1986
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1987
- if (byInstance !== 0)
1988
- return byInstance;
1989
- return b.localOperationId - a.localOperationId;
1990
- });
1991
- return c.json({
1992
- items: merged.slice(query.offset, query.offset + query.limit),
1993
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1994
- offset: query.offset,
1995
- limit: query.limit,
1996
- partial: failedInstances.length > 0,
1997
- failedInstances,
1998
- });
1999
- });
2000
- routes.get('/events', describeRoute({
2001
- tags: ['console-gateway'],
2002
- summary: 'List merged request events across instances',
2003
- responses: {
2004
- 200: {
2005
- description: 'Merged events',
2006
- content: {
2007
- 'application/json': {
2008
- schema: resolver(GatewayPaginatedResponseSchema(GatewayEventItemSchema)),
2009
- },
2010
- },
2011
- },
2012
- },
2013
- }), zValidator('query', GatewayEventsQuerySchema), async (c) => {
2014
- const auth = await options.authenticate(c);
2015
- if (!auth) {
2016
- return unauthorizedResponse(c);
2017
- }
2018
- const query = c.req.valid('query');
2019
- const selectedInstances = selectInstances({ instances, query });
2020
- if (selectedInstances.length === 0) {
2021
- return c.json({
2022
- error: 'NO_INSTANCES_SELECTED',
2023
- message: 'No enabled instances matched the provided instance filter.',
2024
- }, 400);
2025
- }
2026
- const targetCount = query.offset + query.limit;
2027
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2028
- forwardQuery.delete('limit');
2029
- forwardQuery.delete('offset');
2030
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleRequestEventSchema);
2031
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
2032
- c,
2033
- instance,
2034
- path: '/events',
2035
- query: forwardQuery,
2036
- targetCount,
2037
- schema: pageSchema,
2038
- fetchImpl,
2039
- })));
2040
- const failedInstances = results
2041
- .filter((result) => !result.ok)
2042
- .map((result) => result.failure);
2043
- const successful = results
2044
- .map((result, index) => ({
2045
- result,
2046
- instance: selectedInstances[index],
2047
- }))
2048
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
2049
- if (successful.length === 0) {
2050
- return allInstancesFailedResponse(c, failedInstances);
2051
- }
2052
- const merged = successful
2053
- .flatMap(({ result, instance }) => result.items.map((event) => ({
2054
- ...event,
2055
- instanceId: instance.instanceId,
2056
- federatedEventId: `${instance.instanceId}:${event.eventId}`,
2057
- localEventId: event.eventId,
2058
- })))
2059
- .sort((a, b) => {
2060
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2061
- if (byTime !== 0)
2062
- return byTime;
2063
- const byInstance = a.instanceId.localeCompare(b.instanceId);
2064
- if (byInstance !== 0)
2065
- return byInstance;
2066
- return b.localEventId - a.localEventId;
2067
- });
2068
- return c.json({
2069
- items: merged.slice(query.offset, query.offset + query.limit),
2070
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
2071
- offset: query.offset,
2072
- limit: query.limit,
2073
- partial: failedInstances.length > 0,
2074
- failedInstances,
2075
- });
2076
- });
2077
- if (options.websocket?.enabled &&
2078
- options.websocket?.upgradeWebSocket !== undefined) {
2079
- const upgradeWebSocket = options.websocket.upgradeWebSocket;
2080
- const heartbeatIntervalMs = options.websocket.heartbeatIntervalMs ?? 30000;
2081
- const createDownstreamSocket = options.websocket.createWebSocket ??
2082
- ((url) => new WebSocket(url));
2083
- const liveState = new WeakMap();
2084
- routes.get('/events/live', upgradeWebSocket(async (c) => {
2085
- const initialAuth = await options.authenticate(c);
2086
- const partitionId = c.req.query('partitionId')?.trim() || undefined;
2087
- const replaySince = c.req.query('since')?.trim() || undefined;
2088
- const replayLimitRaw = c.req.query('replayLimit');
2089
- const replayLimitNumber = replayLimitRaw
2090
- ? Number.parseInt(replayLimitRaw, 10)
2091
- : Number.NaN;
2092
- const replayLimit = Number.isFinite(replayLimitNumber)
2093
- ? Math.max(1, Math.min(500, replayLimitNumber))
2094
- : 100;
2095
- const selectedInstances = selectInstances({
2096
- instances,
2097
- query: {
2098
- instanceId: c.req.query('instanceId') ?? undefined,
2099
- instanceIds: c.req.query('instanceIds') ?? undefined,
2100
- },
2101
- });
2102
- const authenticateWithBearer = async (token) => {
2103
- const trimmedToken = token.trim();
2104
- if (!trimmedToken) {
2105
- return null;
2106
- }
2107
- const authContext = {
2108
- req: {
2109
- header: (name) => name === 'Authorization' ? `Bearer ${trimmedToken}` : undefined,
2110
- query: () => undefined,
2111
- },
2112
- };
2113
- return options.authenticate(authContext);
2114
- };
2115
- const closeUnauthenticated = (ws) => {
2116
- try {
2117
- ws.send(JSON.stringify({ type: 'error', message: 'UNAUTHENTICATED' }));
2118
- }
2119
- catch {
2120
- // no-op
2121
- }
2122
- ws.close(4001, 'Unauthenticated');
2123
- };
2124
- const cleanup = (ws) => {
2125
- const state = liveState.get(ws);
2126
- if (!state)
2127
- return;
2128
- if (state.heartbeatInterval) {
2129
- clearInterval(state.heartbeatInterval);
2130
- }
2131
- if (state.authTimeout) {
2132
- clearTimeout(state.authTimeout);
2133
- }
2134
- for (const downstream of state.downstreamSockets) {
2135
- try {
2136
- downstream.close();
2137
- }
2138
- catch {
2139
- // no-op
2140
- }
2141
- }
2142
- liveState.delete(ws);
2143
- };
2144
- return {
2145
- onOpen(_event, ws) {
2146
- if (selectedInstances.length === 0) {
2147
- ws.send(JSON.stringify({
2148
- type: 'error',
2149
- message: 'No enabled instances matched the provided instance filter.',
2150
- }));
2151
- ws.close(4004, 'No instances selected');
2152
- return;
2153
- }
2154
- const state = {
2155
- downstreamSockets: [],
2156
- heartbeatInterval: null,
2157
- authTimeout: null,
2158
- isAuthenticated: false,
2159
- };
2160
- liveState.set(ws, state);
2161
- const startAuthenticatedSession = (upstreamBearerToken) => {
2162
- if (state.isAuthenticated) {
2163
- return;
2164
- }
2165
- state.isAuthenticated = true;
2166
- if (state.authTimeout) {
2167
- clearTimeout(state.authTimeout);
2168
- state.authTimeout = null;
2169
- }
2170
- for (const instance of selectedInstances) {
2171
- const downstreamQuery = new URLSearchParams();
2172
- if (partitionId) {
2173
- downstreamQuery.set('partitionId', partitionId);
2174
- }
2175
- if (replaySince) {
2176
- downstreamQuery.set('since', replaySince);
2177
- }
2178
- downstreamQuery.set('replayLimit', String(replayLimit));
2179
- const downstreamUrl = buildConsoleEndpointUrl({
2180
- instance,
2181
- requestUrl: c.req.url,
2182
- path: '/events/live',
2183
- query: downstreamQuery,
2184
- });
2185
- const downstreamSocket = createDownstreamSocket(downstreamUrl);
2186
- const downstreamToken = instance.token?.trim() ?? upstreamBearerToken?.trim() ?? null;
2187
- if (downstreamToken && downstreamSocket.send) {
2188
- downstreamSocket.onopen = () => {
2189
- try {
2190
- downstreamSocket.send?.(JSON.stringify({
2191
- type: 'auth',
2192
- token: downstreamToken,
2193
- }));
2194
- }
2195
- catch {
2196
- // no-op
2197
- }
2198
- };
2199
- }
2200
- downstreamSocket.onmessage = (message) => {
2201
- if (typeof message.data !== 'string') {
2202
- return;
2203
- }
2204
- try {
2205
- const payload = JSON.parse(message.data);
2206
- if (typeof payload.type === 'string' &&
2207
- (payload.type === 'connected' ||
2208
- payload.type === 'heartbeat')) {
2209
- return;
2210
- }
2211
- const payloadData = payload.data &&
2212
- typeof payload.data === 'object' &&
2213
- !Array.isArray(payload.data)
2214
- ? { ...payload.data, instanceId: instance.instanceId }
2215
- : { instanceId: instance.instanceId };
2216
- const event = {
2217
- ...payload,
2218
- data: payloadData,
2219
- instanceId: instance.instanceId,
2220
- timestamp: typeof payload.timestamp === 'string'
2221
- ? payload.timestamp
2222
- : new Date().toISOString(),
2223
- };
2224
- ws.send(JSON.stringify(event));
2225
- }
2226
- catch {
2227
- // Ignore malformed downstream events
2228
- }
2229
- };
2230
- downstreamSocket.onerror = () => {
2231
- try {
2232
- ws.send(JSON.stringify({
2233
- type: 'instance_error',
2234
- instanceId: instance.instanceId,
2235
- timestamp: new Date().toISOString(),
2236
- }));
2237
- }
2238
- catch {
2239
- // ignore send errors
2240
- }
2241
- };
2242
- state.downstreamSockets.push(downstreamSocket);
2243
- }
2244
- ws.send(JSON.stringify({
2245
- type: 'connected',
2246
- timestamp: new Date().toISOString(),
2247
- instanceCount: selectedInstances.length,
2248
- }));
2249
- const heartbeatInterval = setInterval(() => {
2250
- try {
2251
- ws.send(JSON.stringify({
2252
- type: 'heartbeat',
2253
- timestamp: new Date().toISOString(),
2254
- }));
2255
- }
2256
- catch {
2257
- clearInterval(heartbeatInterval);
2258
- }
2259
- }, heartbeatIntervalMs);
2260
- state.heartbeatInterval = heartbeatInterval;
2261
- };
2262
- if (initialAuth) {
2263
- startAuthenticatedSession(parseBearerToken(c.req.header('Authorization')));
2264
- return;
2265
- }
2266
- state.authTimeout = setTimeout(() => {
2267
- const current = liveState.get(ws);
2268
- if (!current || current.isAuthenticated) {
2269
- return;
2270
- }
2271
- closeUnauthenticated(ws);
2272
- cleanup(ws);
2273
- }, 5_000);
2274
- },
2275
- async onMessage(event, ws) {
2276
- const state = liveState.get(ws);
2277
- if (!state || state.isAuthenticated) {
2278
- return;
2279
- }
2280
- if (typeof event.data !== 'string') {
2281
- closeUnauthenticated(ws);
2282
- cleanup(ws);
2283
- return;
2284
- }
2285
- let token = '';
2286
- try {
2287
- const parsed = JSON.parse(event.data);
2288
- if (parsed.type === 'auth' &&
2289
- typeof parsed.token === 'string' &&
2290
- parsed.token.trim().length > 0) {
2291
- token = parsed.token;
2292
- }
2293
- }
2294
- catch {
2295
- // Invalid auth message will be handled below.
2296
- }
2297
- if (!token) {
2298
- closeUnauthenticated(ws);
2299
- cleanup(ws);
2300
- return;
2301
- }
2302
- const auth = await authenticateWithBearer(token);
2303
- const current = liveState.get(ws);
2304
- if (!current || current.isAuthenticated) {
2305
- return;
2306
- }
2307
- if (!auth) {
2308
- closeUnauthenticated(ws);
2309
- cleanup(ws);
2310
- return;
2311
- }
2312
- current.isAuthenticated = true;
2313
- if (current.authTimeout) {
2314
- clearTimeout(current.authTimeout);
2315
- current.authTimeout = null;
2316
- }
2317
- for (const instance of selectedInstances) {
2318
- const downstreamQuery = new URLSearchParams();
2319
- if (partitionId) {
2320
- downstreamQuery.set('partitionId', partitionId);
2321
- }
2322
- if (replaySince) {
2323
- downstreamQuery.set('since', replaySince);
2324
- }
2325
- downstreamQuery.set('replayLimit', String(replayLimit));
2326
- const downstreamUrl = buildConsoleEndpointUrl({
2327
- instance,
2328
- requestUrl: c.req.url,
2329
- path: '/events/live',
2330
- query: downstreamQuery,
2331
- });
2332
- const downstreamSocket = createDownstreamSocket(downstreamUrl);
2333
- const upstreamToken = token.trim();
2334
- const downstreamToken = instance.token?.trim() ||
2335
- (upstreamToken.length > 0 ? upstreamToken : null);
2336
- if (downstreamToken && downstreamSocket.send) {
2337
- downstreamSocket.onopen = () => {
2338
- try {
2339
- downstreamSocket.send?.(JSON.stringify({
2340
- type: 'auth',
2341
- token: downstreamToken,
2342
- }));
2343
- }
2344
- catch {
2345
- // no-op
2346
- }
2347
- };
2348
- }
2349
- downstreamSocket.onmessage = (message) => {
2350
- if (typeof message.data !== 'string') {
2351
- return;
2352
- }
2353
- try {
2354
- const payload = JSON.parse(message.data);
2355
- if (typeof payload.type === 'string' &&
2356
- (payload.type === 'connected' ||
2357
- payload.type === 'heartbeat')) {
2358
- return;
2359
- }
2360
- const payloadData = payload.data &&
2361
- typeof payload.data === 'object' &&
2362
- !Array.isArray(payload.data)
2363
- ? { ...payload.data, instanceId: instance.instanceId }
2364
- : { instanceId: instance.instanceId };
2365
- const liveEvent = {
2366
- ...payload,
2367
- data: payloadData,
2368
- instanceId: instance.instanceId,
2369
- timestamp: typeof payload.timestamp === 'string'
2370
- ? payload.timestamp
2371
- : new Date().toISOString(),
2372
- };
2373
- ws.send(JSON.stringify(liveEvent));
2374
- }
2375
- catch {
2376
- // Ignore malformed downstream events
2377
- }
2378
- };
2379
- downstreamSocket.onerror = () => {
2380
- try {
2381
- ws.send(JSON.stringify({
2382
- type: 'instance_error',
2383
- instanceId: instance.instanceId,
2384
- timestamp: new Date().toISOString(),
2385
- }));
2386
- }
2387
- catch {
2388
- // ignore send errors
2389
- }
2390
- };
2391
- current.downstreamSockets.push(downstreamSocket);
2392
- }
2393
- ws.send(JSON.stringify({
2394
- type: 'connected',
2395
- timestamp: new Date().toISOString(),
2396
- instanceCount: selectedInstances.length,
2397
- }));
2398
- const heartbeatInterval = setInterval(() => {
2399
- try {
2400
- ws.send(JSON.stringify({
2401
- type: 'heartbeat',
2402
- timestamp: new Date().toISOString(),
2403
- }));
2404
- }
2405
- catch {
2406
- clearInterval(heartbeatInterval);
2407
- }
2408
- }, heartbeatIntervalMs);
2409
- current.heartbeatInterval = heartbeatInterval;
2410
- },
2411
- onClose(_event, ws) {
2412
- cleanup(ws);
2413
- },
2414
- onError(_event, ws) {
2415
- cleanup(ws);
2416
- },
2417
- };
2418
- }));
2419
- }
2420
- routes.get('/events/:id', describeRoute({
2421
- tags: ['console-gateway'],
2422
- summary: 'Get merged event detail by federated id',
2423
- responses: {
2424
- 200: {
2425
- description: 'Event detail',
2426
- content: {
2427
- 'application/json': {
2428
- schema: resolver(GatewayEventItemSchema),
2429
- },
2430
- },
2431
- },
2432
- },
2433
- }), zValidator('param', GatewayEventPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
2434
- const auth = await options.authenticate(c);
2435
- if (!auth) {
2436
- return unauthorizedResponse(c);
2437
- }
2438
- const { id } = c.req.valid('param');
2439
- const query = c.req.valid('query');
2440
- const target = resolveEventTarget({
2441
- id,
2442
- instances,
2443
- query,
2444
- });
2445
- if (!target.ok) {
2446
- return c.json({
2447
- error: target.error,
2448
- ...(target.message ? { message: target.message } : {}),
2449
- }, target.status);
2450
- }
2451
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2452
- const result = await fetchDownstreamJson({
2453
- c,
2454
- instance: target.instance,
2455
- path: `/events/${target.localEventId}`,
2456
- query: forwardQuery,
2457
- schema: ConsoleRequestEventSchema,
2458
- fetchImpl,
2459
- });
2460
- if (!result.ok) {
2461
- if (result.failure.status === 404) {
2462
- return c.json({ error: 'NOT_FOUND' }, 404);
2463
- }
2464
- return c.json({
2465
- error: 'DOWNSTREAM_UNAVAILABLE',
2466
- failedInstances: [result.failure],
2467
- }, 502);
2468
- }
2469
- return c.json({
2470
- ...result.data,
2471
- instanceId: target.instance.instanceId,
2472
- federatedEventId: `${target.instance.instanceId}:${result.data.eventId}`,
2473
- localEventId: result.data.eventId,
2474
- });
2475
- });
2476
- routes.get('/events/:id/payload', describeRoute({
2477
- tags: ['console-gateway'],
2478
- summary: 'Get merged event payload by federated id',
2479
- responses: {
2480
- 200: {
2481
- description: 'Event payload',
2482
- content: {
2483
- 'application/json': {
2484
- schema: resolver(GatewayEventPayloadSchema),
2485
- },
2486
- },
2487
- },
2488
- },
2489
- }), zValidator('param', GatewayEventPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
2490
- const auth = await options.authenticate(c);
2491
- if (!auth) {
2492
- return unauthorizedResponse(c);
2493
- }
2494
- const { id } = c.req.valid('param');
2495
- const query = c.req.valid('query');
2496
- const target = resolveEventTarget({
2497
- id,
2498
- instances,
2499
- query,
2500
- });
2501
- if (!target.ok) {
2502
- return c.json({
2503
- error: target.error,
2504
- ...(target.message ? { message: target.message } : {}),
2505
- }, target.status);
2506
- }
2507
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2508
- const result = await fetchDownstreamJson({
2509
- c,
2510
- instance: target.instance,
2511
- path: `/events/${target.localEventId}/payload`,
2512
- query: forwardQuery,
2513
- schema: ConsoleRequestPayloadSchema,
2514
- fetchImpl,
2515
- });
2516
- if (!result.ok) {
2517
- if (result.failure.status === 404) {
2518
- return c.json({ error: 'NOT_FOUND' }, 404);
2519
- }
2520
- return c.json({
2521
- error: 'DOWNSTREAM_UNAVAILABLE',
2522
- failedInstances: [result.failure],
2523
- }, 502);
2524
- }
2525
- return c.json({
2526
- ...result.data,
2527
- instanceId: target.instance.instanceId,
2528
- federatedEventId: `${target.instance.instanceId}:${target.localEventId}`,
2529
- localEventId: target.localEventId,
2530
- });
2531
- });
2532
- return routes;
2533
- }
2534
- //# sourceMappingURL=gateway.js.map