@syncular/server-hono 0.0.2-2 → 0.0.3-14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/console/gateway.d.ts +31 -0
- package/dist/console/gateway.d.ts.map +1 -0
- package/dist/console/gateway.js +2349 -0
- package/dist/console/gateway.js.map +1 -0
- package/dist/console/index.d.ts +3 -2
- package/dist/console/index.d.ts.map +1 -1
- package/dist/console/index.js +3 -1
- package/dist/console/index.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +116 -94
- package/dist/routes.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/console-gateway-live-routes.test.ts +246 -0
- package/src/__tests__/console-gateway-routes.test.ts +1364 -0
- package/src/__tests__/console-routes.test.ts +7 -3
- package/src/console/gateway.ts +3371 -0
- package/src/console/index.ts +3 -11
- package/src/routes.ts +122 -96
|
@@ -0,0 +1,2349 @@
|
|
|
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
|
+
const queryToken = args.c.req.query('token')?.trim();
|
|
475
|
+
if (queryToken) {
|
|
476
|
+
return `Bearer ${queryToken}`;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
function resolveForwardBearerToken(args) {
|
|
481
|
+
if (args.instance.token) {
|
|
482
|
+
return args.instance.token;
|
|
483
|
+
}
|
|
484
|
+
const authHeader = args.c.req.header('Authorization')?.trim();
|
|
485
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
486
|
+
const token = authHeader.slice(7).trim();
|
|
487
|
+
if (token.length > 0) {
|
|
488
|
+
return token;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const queryToken = args.c.req.query('token')?.trim();
|
|
492
|
+
if (queryToken) {
|
|
493
|
+
return queryToken;
|
|
494
|
+
}
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
async function fetchDownstreamJson(args) {
|
|
498
|
+
const url = buildConsoleEndpointUrl({
|
|
499
|
+
instance: args.instance,
|
|
500
|
+
requestUrl: args.c.req.url,
|
|
501
|
+
path: args.path,
|
|
502
|
+
query: args.query,
|
|
503
|
+
});
|
|
504
|
+
const headers = new Headers();
|
|
505
|
+
headers.set('Accept', 'application/json');
|
|
506
|
+
const authorization = resolveForwardAuthorization({
|
|
507
|
+
c: args.c,
|
|
508
|
+
instance: args.instance,
|
|
509
|
+
});
|
|
510
|
+
if (authorization) {
|
|
511
|
+
headers.set('Authorization', authorization);
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const response = await args.fetchImpl(url, {
|
|
515
|
+
method: 'GET',
|
|
516
|
+
headers,
|
|
517
|
+
});
|
|
518
|
+
if (!response.ok) {
|
|
519
|
+
return {
|
|
520
|
+
ok: false,
|
|
521
|
+
failure: {
|
|
522
|
+
instanceId: args.instance.instanceId,
|
|
523
|
+
reason: `HTTP ${response.status}`,
|
|
524
|
+
status: response.status,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const payload = await response.json();
|
|
529
|
+
const parsed = args.schema.safeParse(payload);
|
|
530
|
+
if (!parsed.success) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
failure: {
|
|
534
|
+
instanceId: args.instance.instanceId,
|
|
535
|
+
reason: 'Invalid response payload',
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return { ok: true, data: parsed.data };
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
failure: {
|
|
545
|
+
instanceId: args.instance.instanceId,
|
|
546
|
+
reason: toErrorMessage(error),
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function parseDownstreamBody(response) {
|
|
552
|
+
const text = await response.text();
|
|
553
|
+
if (!text.trim()) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
return JSON.parse(text);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return text;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function normalizeDownstreamError(args) {
|
|
564
|
+
if (args.body && typeof args.body === 'object' && !Array.isArray(args.body)) {
|
|
565
|
+
return {
|
|
566
|
+
...args.body,
|
|
567
|
+
instanceId: args.instanceId,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (typeof args.body === 'string' && args.body.trim().length > 0) {
|
|
571
|
+
return {
|
|
572
|
+
error: 'DOWNSTREAM_ERROR',
|
|
573
|
+
message: args.body,
|
|
574
|
+
instanceId: args.instanceId,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
error: 'DOWNSTREAM_ERROR',
|
|
579
|
+
status: args.status,
|
|
580
|
+
instanceId: args.instanceId,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
async function forwardDownstreamJsonRequest(args) {
|
|
584
|
+
const url = buildConsoleEndpointUrl({
|
|
585
|
+
instance: args.instance,
|
|
586
|
+
requestUrl: args.c.req.url,
|
|
587
|
+
path: args.path,
|
|
588
|
+
query: args.query,
|
|
589
|
+
});
|
|
590
|
+
const headers = new Headers();
|
|
591
|
+
headers.set('Accept', 'application/json');
|
|
592
|
+
const authorization = resolveForwardAuthorization({
|
|
593
|
+
c: args.c,
|
|
594
|
+
instance: args.instance,
|
|
595
|
+
});
|
|
596
|
+
if (authorization) {
|
|
597
|
+
headers.set('Authorization', authorization);
|
|
598
|
+
}
|
|
599
|
+
let requestBody;
|
|
600
|
+
if (args.body !== undefined) {
|
|
601
|
+
headers.set('Content-Type', 'application/json');
|
|
602
|
+
requestBody = JSON.stringify(args.body);
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const response = await args.fetchImpl(url, {
|
|
606
|
+
method: args.method,
|
|
607
|
+
headers,
|
|
608
|
+
...(requestBody !== undefined ? { body: requestBody } : {}),
|
|
609
|
+
});
|
|
610
|
+
const payload = await parseDownstreamBody(response);
|
|
611
|
+
if (!response.ok) {
|
|
612
|
+
return {
|
|
613
|
+
ok: false,
|
|
614
|
+
status: response.status,
|
|
615
|
+
body: normalizeDownstreamError({
|
|
616
|
+
body: payload,
|
|
617
|
+
status: response.status,
|
|
618
|
+
instanceId: args.instance.instanceId,
|
|
619
|
+
}),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const parsed = args.responseSchema.safeParse(payload);
|
|
623
|
+
if (!parsed.success) {
|
|
624
|
+
return {
|
|
625
|
+
ok: false,
|
|
626
|
+
status: 502,
|
|
627
|
+
body: {
|
|
628
|
+
error: 'INVALID_DOWNSTREAM_RESPONSE',
|
|
629
|
+
message: 'Downstream response failed validation.',
|
|
630
|
+
instanceId: args.instance.instanceId,
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
ok: true,
|
|
636
|
+
data: parsed.data,
|
|
637
|
+
status: response.status,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
return {
|
|
642
|
+
ok: false,
|
|
643
|
+
status: 502,
|
|
644
|
+
body: {
|
|
645
|
+
error: 'DOWNSTREAM_UNAVAILABLE',
|
|
646
|
+
message: toErrorMessage(error),
|
|
647
|
+
instanceId: args.instance.instanceId,
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async function fetchDownstreamPaged(args) {
|
|
653
|
+
const items = [];
|
|
654
|
+
let total = null;
|
|
655
|
+
let localOffset = 0;
|
|
656
|
+
let pageCount = 0;
|
|
657
|
+
while (items.length < args.targetCount &&
|
|
658
|
+
(total === null || localOffset < total) &&
|
|
659
|
+
pageCount < 100) {
|
|
660
|
+
const limit = Math.min(100, Math.max(1, args.targetCount - items.length));
|
|
661
|
+
const pagedQuery = withPaging(args.query, { limit, offset: localOffset });
|
|
662
|
+
const result = await fetchDownstreamJson({
|
|
663
|
+
c: args.c,
|
|
664
|
+
instance: args.instance,
|
|
665
|
+
path: args.path,
|
|
666
|
+
query: pagedQuery,
|
|
667
|
+
schema: args.schema,
|
|
668
|
+
fetchImpl: args.fetchImpl,
|
|
669
|
+
});
|
|
670
|
+
if (!result.ok) {
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
const page = result.data;
|
|
674
|
+
total = page.total;
|
|
675
|
+
items.push(...page.items);
|
|
676
|
+
localOffset += page.items.length;
|
|
677
|
+
pageCount += 1;
|
|
678
|
+
if (page.items.length === 0) {
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
ok: true,
|
|
684
|
+
items,
|
|
685
|
+
total: total ?? items.length,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
async function checkDownstreamInstanceHealth(args) {
|
|
689
|
+
const startedAt = Date.now();
|
|
690
|
+
const result = await fetchDownstreamJson({
|
|
691
|
+
c: args.c,
|
|
692
|
+
instance: args.instance,
|
|
693
|
+
path: '/stats',
|
|
694
|
+
schema: SyncStatsSchema,
|
|
695
|
+
fetchImpl: args.fetchImpl,
|
|
696
|
+
});
|
|
697
|
+
const responseTimeMs = Math.max(0, Date.now() - startedAt);
|
|
698
|
+
const checkedAt = new Date().toISOString();
|
|
699
|
+
const base = {
|
|
700
|
+
instanceId: args.instance.instanceId,
|
|
701
|
+
label: args.instance.label ?? args.instance.instanceId,
|
|
702
|
+
baseUrl: args.instance.baseUrl,
|
|
703
|
+
enabled: args.instance.enabled ?? true,
|
|
704
|
+
responseTimeMs,
|
|
705
|
+
checkedAt,
|
|
706
|
+
};
|
|
707
|
+
if (result.ok) {
|
|
708
|
+
return {
|
|
709
|
+
...base,
|
|
710
|
+
healthy: true,
|
|
711
|
+
status: 200,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
...base,
|
|
716
|
+
healthy: false,
|
|
717
|
+
status: result.failure.status,
|
|
718
|
+
reason: result.failure.reason,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function unauthorizedResponse(c) {
|
|
722
|
+
return c.json({ error: 'UNAUTHORIZED' }, 401);
|
|
723
|
+
}
|
|
724
|
+
function jsonResponse(payload, status) {
|
|
725
|
+
return new Response(JSON.stringify(payload), {
|
|
726
|
+
status,
|
|
727
|
+
headers: {
|
|
728
|
+
'content-type': 'application/json; charset=utf-8',
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function allInstancesFailedResponse(c, failedInstances) {
|
|
733
|
+
return c.json({
|
|
734
|
+
error: 'DOWNSTREAM_UNAVAILABLE',
|
|
735
|
+
failedInstances,
|
|
736
|
+
}, 502);
|
|
737
|
+
}
|
|
738
|
+
export function createConsoleGatewayRoutes(options) {
|
|
739
|
+
const routes = new Hono();
|
|
740
|
+
const instances = normalizeInstances(options.instances);
|
|
741
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
742
|
+
const corsOrigins = options.corsOrigins ?? '*';
|
|
743
|
+
routes.use('*', cors({
|
|
744
|
+
origin: corsOrigins === '*' ? '*' : corsOrigins,
|
|
745
|
+
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
|
746
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
747
|
+
credentials: true,
|
|
748
|
+
}));
|
|
749
|
+
routes.get('/instances', describeRoute({
|
|
750
|
+
tags: ['console-gateway'],
|
|
751
|
+
summary: 'List configured downstream console instances',
|
|
752
|
+
responses: {
|
|
753
|
+
200: {
|
|
754
|
+
description: 'Configured instances',
|
|
755
|
+
content: {
|
|
756
|
+
'application/json': {
|
|
757
|
+
schema: resolver(GatewayInstancesResponseSchema),
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
401: {
|
|
762
|
+
description: 'Unauthenticated',
|
|
763
|
+
content: {
|
|
764
|
+
'application/json': {
|
|
765
|
+
schema: resolver(z.object({ error: z.string() })),
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
}), async (c) => {
|
|
771
|
+
const auth = await options.authenticate(c);
|
|
772
|
+
if (!auth) {
|
|
773
|
+
return unauthorizedResponse(c);
|
|
774
|
+
}
|
|
775
|
+
return c.json({
|
|
776
|
+
items: instances.map((instance) => ({
|
|
777
|
+
instanceId: instance.instanceId,
|
|
778
|
+
label: instance.label ?? instance.instanceId,
|
|
779
|
+
baseUrl: instance.baseUrl,
|
|
780
|
+
enabled: instance.enabled ?? true,
|
|
781
|
+
})),
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
routes.get('/instances/health', describeRoute({
|
|
785
|
+
tags: ['console-gateway'],
|
|
786
|
+
summary: 'Probe downstream console health by instance',
|
|
787
|
+
responses: {
|
|
788
|
+
200: {
|
|
789
|
+
description: 'Per-instance health results',
|
|
790
|
+
content: {
|
|
791
|
+
'application/json': {
|
|
792
|
+
schema: resolver(GatewayInstancesHealthResponseSchema),
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
401: {
|
|
797
|
+
description: 'Unauthenticated',
|
|
798
|
+
content: {
|
|
799
|
+
'application/json': {
|
|
800
|
+
schema: resolver(z.object({ error: z.string() })),
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
}), zValidator('query', GatewayInstanceFilterSchema), async (c) => {
|
|
806
|
+
const auth = await options.authenticate(c);
|
|
807
|
+
if (!auth) {
|
|
808
|
+
return unauthorizedResponse(c);
|
|
809
|
+
}
|
|
810
|
+
const query = c.req.valid('query');
|
|
811
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
812
|
+
if (selectedInstances.length === 0) {
|
|
813
|
+
return c.json({
|
|
814
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
815
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
816
|
+
}, 400);
|
|
817
|
+
}
|
|
818
|
+
const items = await Promise.all(selectedInstances.map((instance) => checkDownstreamInstanceHealth({
|
|
819
|
+
c,
|
|
820
|
+
instance,
|
|
821
|
+
fetchImpl,
|
|
822
|
+
})));
|
|
823
|
+
const failedInstances = items
|
|
824
|
+
.filter((item) => !item.healthy)
|
|
825
|
+
.map((item) => ({
|
|
826
|
+
instanceId: item.instanceId,
|
|
827
|
+
reason: item.reason ?? 'Health probe failed',
|
|
828
|
+
...(item.status !== undefined ? { status: item.status } : {}),
|
|
829
|
+
}));
|
|
830
|
+
return c.json({
|
|
831
|
+
items,
|
|
832
|
+
partial: failedInstances.length > 0,
|
|
833
|
+
failedInstances,
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
routes.get('/handlers', describeRoute({
|
|
837
|
+
tags: ['console-gateway'],
|
|
838
|
+
summary: 'List handlers for a single target instance (requires instance selection)',
|
|
839
|
+
responses: {
|
|
840
|
+
200: {
|
|
841
|
+
description: 'Handlers',
|
|
842
|
+
content: {
|
|
843
|
+
'application/json': {
|
|
844
|
+
schema: resolver(GatewayHandlersResponseSchema),
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
850
|
+
const auth = await options.authenticate(c);
|
|
851
|
+
if (!auth) {
|
|
852
|
+
return unauthorizedResponse(c);
|
|
853
|
+
}
|
|
854
|
+
const query = c.req.valid('query');
|
|
855
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
856
|
+
if (!target.ok) {
|
|
857
|
+
return c.json({
|
|
858
|
+
error: target.error,
|
|
859
|
+
message: target.message,
|
|
860
|
+
}, target.status);
|
|
861
|
+
}
|
|
862
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
863
|
+
const result = await forwardDownstreamJsonRequest({
|
|
864
|
+
c,
|
|
865
|
+
instance: target.instance,
|
|
866
|
+
method: 'GET',
|
|
867
|
+
path: '/handlers',
|
|
868
|
+
query: forwardQuery,
|
|
869
|
+
responseSchema: GatewayHandlersResponseSchema,
|
|
870
|
+
fetchImpl,
|
|
871
|
+
});
|
|
872
|
+
if (!result.ok) {
|
|
873
|
+
return jsonResponse(result.body, result.status);
|
|
874
|
+
}
|
|
875
|
+
return jsonResponse(result.data, result.status);
|
|
876
|
+
});
|
|
877
|
+
routes.post('/prune/preview', describeRoute({
|
|
878
|
+
tags: ['console-gateway'],
|
|
879
|
+
summary: 'Preview prune on a single target instance (requires instance selection)',
|
|
880
|
+
responses: {
|
|
881
|
+
200: {
|
|
882
|
+
description: 'Prune preview',
|
|
883
|
+
content: {
|
|
884
|
+
'application/json': {
|
|
885
|
+
schema: resolver(ConsolePrunePreviewSchema),
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
891
|
+
const auth = await options.authenticate(c);
|
|
892
|
+
if (!auth) {
|
|
893
|
+
return unauthorizedResponse(c);
|
|
894
|
+
}
|
|
895
|
+
const query = c.req.valid('query');
|
|
896
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
897
|
+
if (!target.ok) {
|
|
898
|
+
return c.json({
|
|
899
|
+
error: target.error,
|
|
900
|
+
message: target.message,
|
|
901
|
+
}, target.status);
|
|
902
|
+
}
|
|
903
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
904
|
+
const result = await forwardDownstreamJsonRequest({
|
|
905
|
+
c,
|
|
906
|
+
instance: target.instance,
|
|
907
|
+
method: 'POST',
|
|
908
|
+
path: '/prune/preview',
|
|
909
|
+
query: forwardQuery,
|
|
910
|
+
responseSchema: ConsolePrunePreviewSchema,
|
|
911
|
+
fetchImpl,
|
|
912
|
+
});
|
|
913
|
+
if (!result.ok) {
|
|
914
|
+
return jsonResponse(result.body, result.status);
|
|
915
|
+
}
|
|
916
|
+
return jsonResponse(result.data, result.status);
|
|
917
|
+
});
|
|
918
|
+
routes.post('/prune', describeRoute({
|
|
919
|
+
tags: ['console-gateway'],
|
|
920
|
+
summary: 'Trigger prune on a single target instance (requires instance selection)',
|
|
921
|
+
responses: {
|
|
922
|
+
200: {
|
|
923
|
+
description: 'Prune result',
|
|
924
|
+
content: {
|
|
925
|
+
'application/json': {
|
|
926
|
+
schema: resolver(ConsolePruneResultSchema),
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
932
|
+
const auth = await options.authenticate(c);
|
|
933
|
+
if (!auth) {
|
|
934
|
+
return unauthorizedResponse(c);
|
|
935
|
+
}
|
|
936
|
+
const query = c.req.valid('query');
|
|
937
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
938
|
+
if (!target.ok) {
|
|
939
|
+
return c.json({
|
|
940
|
+
error: target.error,
|
|
941
|
+
message: target.message,
|
|
942
|
+
}, target.status);
|
|
943
|
+
}
|
|
944
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
945
|
+
const result = await forwardDownstreamJsonRequest({
|
|
946
|
+
c,
|
|
947
|
+
instance: target.instance,
|
|
948
|
+
method: 'POST',
|
|
949
|
+
path: '/prune',
|
|
950
|
+
query: forwardQuery,
|
|
951
|
+
responseSchema: ConsolePruneResultSchema,
|
|
952
|
+
fetchImpl,
|
|
953
|
+
});
|
|
954
|
+
if (!result.ok) {
|
|
955
|
+
return jsonResponse(result.body, result.status);
|
|
956
|
+
}
|
|
957
|
+
return jsonResponse(result.data, result.status);
|
|
958
|
+
});
|
|
959
|
+
routes.post('/compact', describeRoute({
|
|
960
|
+
tags: ['console-gateway'],
|
|
961
|
+
summary: 'Trigger compaction on a single target instance (requires instance selection)',
|
|
962
|
+
responses: {
|
|
963
|
+
200: {
|
|
964
|
+
description: 'Compaction result',
|
|
965
|
+
content: {
|
|
966
|
+
'application/json': {
|
|
967
|
+
schema: resolver(ConsoleCompactResultSchema),
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
973
|
+
const auth = await options.authenticate(c);
|
|
974
|
+
if (!auth) {
|
|
975
|
+
return unauthorizedResponse(c);
|
|
976
|
+
}
|
|
977
|
+
const query = c.req.valid('query');
|
|
978
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
979
|
+
if (!target.ok) {
|
|
980
|
+
return c.json({
|
|
981
|
+
error: target.error,
|
|
982
|
+
message: target.message,
|
|
983
|
+
}, target.status);
|
|
984
|
+
}
|
|
985
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
986
|
+
const result = await forwardDownstreamJsonRequest({
|
|
987
|
+
c,
|
|
988
|
+
instance: target.instance,
|
|
989
|
+
method: 'POST',
|
|
990
|
+
path: '/compact',
|
|
991
|
+
query: forwardQuery,
|
|
992
|
+
responseSchema: ConsoleCompactResultSchema,
|
|
993
|
+
fetchImpl,
|
|
994
|
+
});
|
|
995
|
+
if (!result.ok) {
|
|
996
|
+
return jsonResponse(result.body, result.status);
|
|
997
|
+
}
|
|
998
|
+
return jsonResponse(result.data, result.status);
|
|
999
|
+
});
|
|
1000
|
+
routes.post('/notify-data-change', describeRoute({
|
|
1001
|
+
tags: ['console-gateway'],
|
|
1002
|
+
summary: 'Notify data change on a single target instance (requires instance selection)',
|
|
1003
|
+
responses: {
|
|
1004
|
+
200: {
|
|
1005
|
+
description: 'Notification result',
|
|
1006
|
+
content: {
|
|
1007
|
+
'application/json': {
|
|
1008
|
+
schema: resolver(GatewayNotifyDataChangeResponseSchema),
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', GatewayNotifyDataChangeRequestSchema), async (c) => {
|
|
1014
|
+
const auth = await options.authenticate(c);
|
|
1015
|
+
if (!auth) {
|
|
1016
|
+
return unauthorizedResponse(c);
|
|
1017
|
+
}
|
|
1018
|
+
const query = c.req.valid('query');
|
|
1019
|
+
const body = c.req.valid('json');
|
|
1020
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1021
|
+
if (!target.ok) {
|
|
1022
|
+
return c.json({
|
|
1023
|
+
error: target.error,
|
|
1024
|
+
message: target.message,
|
|
1025
|
+
}, target.status);
|
|
1026
|
+
}
|
|
1027
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1028
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1029
|
+
c,
|
|
1030
|
+
instance: target.instance,
|
|
1031
|
+
method: 'POST',
|
|
1032
|
+
path: '/notify-data-change',
|
|
1033
|
+
query: forwardQuery,
|
|
1034
|
+
body,
|
|
1035
|
+
responseSchema: GatewayNotifyDataChangeResponseSchema,
|
|
1036
|
+
fetchImpl,
|
|
1037
|
+
});
|
|
1038
|
+
if (!result.ok) {
|
|
1039
|
+
return jsonResponse(result.body, result.status);
|
|
1040
|
+
}
|
|
1041
|
+
return jsonResponse(result.data, result.status);
|
|
1042
|
+
});
|
|
1043
|
+
routes.delete('/clients/:id', describeRoute({
|
|
1044
|
+
tags: ['console-gateway'],
|
|
1045
|
+
summary: 'Evict client on a single target instance (requires instance selection)',
|
|
1046
|
+
responses: {
|
|
1047
|
+
200: {
|
|
1048
|
+
description: 'Evict result',
|
|
1049
|
+
content: {
|
|
1050
|
+
'application/json': {
|
|
1051
|
+
schema: resolver(ConsoleEvictResultSchema),
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
}), zValidator('param', GatewayClientPathParamSchema), zValidator('query', GatewaySingleInstancePartitionQuerySchema), async (c) => {
|
|
1057
|
+
const auth = await options.authenticate(c);
|
|
1058
|
+
if (!auth) {
|
|
1059
|
+
return unauthorizedResponse(c);
|
|
1060
|
+
}
|
|
1061
|
+
const { id } = c.req.valid('param');
|
|
1062
|
+
const query = c.req.valid('query');
|
|
1063
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1064
|
+
if (!target.ok) {
|
|
1065
|
+
return c.json({
|
|
1066
|
+
error: target.error,
|
|
1067
|
+
message: target.message,
|
|
1068
|
+
}, target.status);
|
|
1069
|
+
}
|
|
1070
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1071
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1072
|
+
c,
|
|
1073
|
+
instance: target.instance,
|
|
1074
|
+
method: 'DELETE',
|
|
1075
|
+
path: `/clients/${encodeURIComponent(id)}`,
|
|
1076
|
+
query: forwardQuery,
|
|
1077
|
+
responseSchema: ConsoleEvictResultSchema,
|
|
1078
|
+
fetchImpl,
|
|
1079
|
+
});
|
|
1080
|
+
if (!result.ok) {
|
|
1081
|
+
return jsonResponse(result.body, result.status);
|
|
1082
|
+
}
|
|
1083
|
+
return jsonResponse(result.data, result.status);
|
|
1084
|
+
});
|
|
1085
|
+
routes.delete('/events', describeRoute({
|
|
1086
|
+
tags: ['console-gateway'],
|
|
1087
|
+
summary: 'Clear request events on a single target instance (requires instance selection)',
|
|
1088
|
+
responses: {
|
|
1089
|
+
200: {
|
|
1090
|
+
description: 'Clear result',
|
|
1091
|
+
content: {
|
|
1092
|
+
'application/json': {
|
|
1093
|
+
schema: resolver(ConsoleClearEventsResultSchema),
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
1099
|
+
const auth = await options.authenticate(c);
|
|
1100
|
+
if (!auth) {
|
|
1101
|
+
return unauthorizedResponse(c);
|
|
1102
|
+
}
|
|
1103
|
+
const query = c.req.valid('query');
|
|
1104
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1105
|
+
if (!target.ok) {
|
|
1106
|
+
return c.json({
|
|
1107
|
+
error: target.error,
|
|
1108
|
+
message: target.message,
|
|
1109
|
+
}, target.status);
|
|
1110
|
+
}
|
|
1111
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1112
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1113
|
+
c,
|
|
1114
|
+
instance: target.instance,
|
|
1115
|
+
method: 'DELETE',
|
|
1116
|
+
path: '/events',
|
|
1117
|
+
query: forwardQuery,
|
|
1118
|
+
responseSchema: ConsoleClearEventsResultSchema,
|
|
1119
|
+
fetchImpl,
|
|
1120
|
+
});
|
|
1121
|
+
if (!result.ok) {
|
|
1122
|
+
return jsonResponse(result.body, result.status);
|
|
1123
|
+
}
|
|
1124
|
+
return jsonResponse(result.data, result.status);
|
|
1125
|
+
});
|
|
1126
|
+
routes.post('/events/prune', describeRoute({
|
|
1127
|
+
tags: ['console-gateway'],
|
|
1128
|
+
summary: 'Prune request events on a single target instance (requires instance selection)',
|
|
1129
|
+
responses: {
|
|
1130
|
+
200: {
|
|
1131
|
+
description: 'Prune events result',
|
|
1132
|
+
content: {
|
|
1133
|
+
'application/json': {
|
|
1134
|
+
schema: resolver(ConsolePruneEventsResultSchema),
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
1140
|
+
const auth = await options.authenticate(c);
|
|
1141
|
+
if (!auth) {
|
|
1142
|
+
return unauthorizedResponse(c);
|
|
1143
|
+
}
|
|
1144
|
+
const query = c.req.valid('query');
|
|
1145
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1146
|
+
if (!target.ok) {
|
|
1147
|
+
return c.json({
|
|
1148
|
+
error: target.error,
|
|
1149
|
+
message: target.message,
|
|
1150
|
+
}, target.status);
|
|
1151
|
+
}
|
|
1152
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1153
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1154
|
+
c,
|
|
1155
|
+
instance: target.instance,
|
|
1156
|
+
method: 'POST',
|
|
1157
|
+
path: '/events/prune',
|
|
1158
|
+
query: forwardQuery,
|
|
1159
|
+
responseSchema: ConsolePruneEventsResultSchema,
|
|
1160
|
+
fetchImpl,
|
|
1161
|
+
});
|
|
1162
|
+
if (!result.ok) {
|
|
1163
|
+
return jsonResponse(result.body, result.status);
|
|
1164
|
+
}
|
|
1165
|
+
return jsonResponse(result.data, result.status);
|
|
1166
|
+
});
|
|
1167
|
+
routes.get('/api-keys', describeRoute({
|
|
1168
|
+
tags: ['console-gateway'],
|
|
1169
|
+
summary: 'List API keys for a single target instance (requires instance selection)',
|
|
1170
|
+
responses: {
|
|
1171
|
+
200: {
|
|
1172
|
+
description: 'Paginated API key list',
|
|
1173
|
+
content: {
|
|
1174
|
+
'application/json': {
|
|
1175
|
+
schema: resolver(ConsolePaginatedResponseSchema(ConsoleApiKeySchema)),
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
},
|
|
1180
|
+
}), zValidator('query', GatewayApiKeysQuerySchema), async (c) => {
|
|
1181
|
+
const auth = await options.authenticate(c);
|
|
1182
|
+
if (!auth) {
|
|
1183
|
+
return unauthorizedResponse(c);
|
|
1184
|
+
}
|
|
1185
|
+
const query = c.req.valid('query');
|
|
1186
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1187
|
+
if (!target.ok) {
|
|
1188
|
+
return c.json({
|
|
1189
|
+
error: target.error,
|
|
1190
|
+
message: target.message,
|
|
1191
|
+
}, target.status);
|
|
1192
|
+
}
|
|
1193
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1194
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1195
|
+
c,
|
|
1196
|
+
instance: target.instance,
|
|
1197
|
+
method: 'GET',
|
|
1198
|
+
path: '/api-keys',
|
|
1199
|
+
query: forwardQuery,
|
|
1200
|
+
responseSchema: ConsolePaginatedResponseSchema(ConsoleApiKeySchema),
|
|
1201
|
+
fetchImpl,
|
|
1202
|
+
});
|
|
1203
|
+
if (!result.ok) {
|
|
1204
|
+
return jsonResponse(result.body, result.status);
|
|
1205
|
+
}
|
|
1206
|
+
return jsonResponse(result.data, result.status);
|
|
1207
|
+
});
|
|
1208
|
+
routes.post('/api-keys', describeRoute({
|
|
1209
|
+
tags: ['console-gateway'],
|
|
1210
|
+
summary: 'Create API key on a single target instance (requires instance selection)',
|
|
1211
|
+
responses: {
|
|
1212
|
+
201: {
|
|
1213
|
+
description: 'Created API key',
|
|
1214
|
+
content: {
|
|
1215
|
+
'application/json': {
|
|
1216
|
+
schema: resolver(ConsoleApiKeyCreateResponseSchema),
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', ConsoleApiKeyCreateRequestSchema), async (c) => {
|
|
1222
|
+
const auth = await options.authenticate(c);
|
|
1223
|
+
if (!auth) {
|
|
1224
|
+
return unauthorizedResponse(c);
|
|
1225
|
+
}
|
|
1226
|
+
const query = c.req.valid('query');
|
|
1227
|
+
const body = c.req.valid('json');
|
|
1228
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1229
|
+
if (!target.ok) {
|
|
1230
|
+
return c.json({
|
|
1231
|
+
error: target.error,
|
|
1232
|
+
message: target.message,
|
|
1233
|
+
}, target.status);
|
|
1234
|
+
}
|
|
1235
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1236
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1237
|
+
c,
|
|
1238
|
+
instance: target.instance,
|
|
1239
|
+
method: 'POST',
|
|
1240
|
+
path: '/api-keys',
|
|
1241
|
+
query: forwardQuery,
|
|
1242
|
+
body,
|
|
1243
|
+
responseSchema: ConsoleApiKeyCreateResponseSchema,
|
|
1244
|
+
fetchImpl,
|
|
1245
|
+
});
|
|
1246
|
+
if (!result.ok) {
|
|
1247
|
+
return jsonResponse(result.body, result.status);
|
|
1248
|
+
}
|
|
1249
|
+
return jsonResponse(result.data, result.status);
|
|
1250
|
+
});
|
|
1251
|
+
routes.get('/api-keys/:id', describeRoute({
|
|
1252
|
+
tags: ['console-gateway'],
|
|
1253
|
+
summary: 'Get API key from a single target instance (requires instance selection)',
|
|
1254
|
+
responses: {
|
|
1255
|
+
200: {
|
|
1256
|
+
description: 'API key details',
|
|
1257
|
+
content: {
|
|
1258
|
+
'application/json': {
|
|
1259
|
+
schema: resolver(ConsoleApiKeySchema),
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
}), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
1265
|
+
const auth = await options.authenticate(c);
|
|
1266
|
+
if (!auth) {
|
|
1267
|
+
return unauthorizedResponse(c);
|
|
1268
|
+
}
|
|
1269
|
+
const { id } = c.req.valid('param');
|
|
1270
|
+
const query = c.req.valid('query');
|
|
1271
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1272
|
+
if (!target.ok) {
|
|
1273
|
+
return c.json({
|
|
1274
|
+
error: target.error,
|
|
1275
|
+
message: target.message,
|
|
1276
|
+
}, target.status);
|
|
1277
|
+
}
|
|
1278
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1279
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1280
|
+
c,
|
|
1281
|
+
instance: target.instance,
|
|
1282
|
+
method: 'GET',
|
|
1283
|
+
path: `/api-keys/${encodeURIComponent(id)}`,
|
|
1284
|
+
query: forwardQuery,
|
|
1285
|
+
responseSchema: ConsoleApiKeySchema,
|
|
1286
|
+
fetchImpl,
|
|
1287
|
+
});
|
|
1288
|
+
if (!result.ok) {
|
|
1289
|
+
return jsonResponse(result.body, result.status);
|
|
1290
|
+
}
|
|
1291
|
+
return jsonResponse(result.data, result.status);
|
|
1292
|
+
});
|
|
1293
|
+
routes.delete('/api-keys/:id', describeRoute({
|
|
1294
|
+
tags: ['console-gateway'],
|
|
1295
|
+
summary: 'Revoke API key on a single target instance (requires instance selection)',
|
|
1296
|
+
responses: {
|
|
1297
|
+
200: {
|
|
1298
|
+
description: 'Revoke result',
|
|
1299
|
+
content: {
|
|
1300
|
+
'application/json': {
|
|
1301
|
+
schema: resolver(ConsoleApiKeyRevokeResponseSchema),
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
},
|
|
1306
|
+
}), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
1307
|
+
const auth = await options.authenticate(c);
|
|
1308
|
+
if (!auth) {
|
|
1309
|
+
return unauthorizedResponse(c);
|
|
1310
|
+
}
|
|
1311
|
+
const { id } = c.req.valid('param');
|
|
1312
|
+
const query = c.req.valid('query');
|
|
1313
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1314
|
+
if (!target.ok) {
|
|
1315
|
+
return c.json({
|
|
1316
|
+
error: target.error,
|
|
1317
|
+
message: target.message,
|
|
1318
|
+
}, target.status);
|
|
1319
|
+
}
|
|
1320
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1321
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1322
|
+
c,
|
|
1323
|
+
instance: target.instance,
|
|
1324
|
+
method: 'DELETE',
|
|
1325
|
+
path: `/api-keys/${encodeURIComponent(id)}`,
|
|
1326
|
+
query: forwardQuery,
|
|
1327
|
+
responseSchema: ConsoleApiKeyRevokeResponseSchema,
|
|
1328
|
+
fetchImpl,
|
|
1329
|
+
});
|
|
1330
|
+
if (!result.ok) {
|
|
1331
|
+
return jsonResponse(result.body, result.status);
|
|
1332
|
+
}
|
|
1333
|
+
return jsonResponse(result.data, result.status);
|
|
1334
|
+
});
|
|
1335
|
+
routes.post('/api-keys/bulk-revoke', describeRoute({
|
|
1336
|
+
tags: ['console-gateway'],
|
|
1337
|
+
summary: 'Bulk revoke API keys on a single target instance (requires instance selection)',
|
|
1338
|
+
responses: {
|
|
1339
|
+
200: {
|
|
1340
|
+
description: 'Bulk revoke result',
|
|
1341
|
+
content: {
|
|
1342
|
+
'application/json': {
|
|
1343
|
+
schema: resolver(ConsoleApiKeyBulkRevokeResponseSchema),
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
}), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', ConsoleApiKeyBulkRevokeRequestSchema), async (c) => {
|
|
1349
|
+
const auth = await options.authenticate(c);
|
|
1350
|
+
if (!auth) {
|
|
1351
|
+
return unauthorizedResponse(c);
|
|
1352
|
+
}
|
|
1353
|
+
const query = c.req.valid('query');
|
|
1354
|
+
const body = c.req.valid('json');
|
|
1355
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1356
|
+
if (!target.ok) {
|
|
1357
|
+
return c.json({
|
|
1358
|
+
error: target.error,
|
|
1359
|
+
message: target.message,
|
|
1360
|
+
}, target.status);
|
|
1361
|
+
}
|
|
1362
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1363
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1364
|
+
c,
|
|
1365
|
+
instance: target.instance,
|
|
1366
|
+
method: 'POST',
|
|
1367
|
+
path: '/api-keys/bulk-revoke',
|
|
1368
|
+
query: forwardQuery,
|
|
1369
|
+
body,
|
|
1370
|
+
responseSchema: ConsoleApiKeyBulkRevokeResponseSchema,
|
|
1371
|
+
fetchImpl,
|
|
1372
|
+
});
|
|
1373
|
+
if (!result.ok) {
|
|
1374
|
+
return jsonResponse(result.body, result.status);
|
|
1375
|
+
}
|
|
1376
|
+
return jsonResponse(result.data, result.status);
|
|
1377
|
+
});
|
|
1378
|
+
routes.post('/api-keys/:id/rotate/stage', describeRoute({
|
|
1379
|
+
tags: ['console-gateway'],
|
|
1380
|
+
summary: 'Stage-rotate API key on a single target instance (requires instance selection)',
|
|
1381
|
+
responses: {
|
|
1382
|
+
200: {
|
|
1383
|
+
description: 'Staged API key replacement',
|
|
1384
|
+
content: {
|
|
1385
|
+
'application/json': {
|
|
1386
|
+
schema: resolver(ConsoleApiKeyCreateResponseSchema),
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
},
|
|
1390
|
+
},
|
|
1391
|
+
}), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
1392
|
+
const auth = await options.authenticate(c);
|
|
1393
|
+
if (!auth) {
|
|
1394
|
+
return unauthorizedResponse(c);
|
|
1395
|
+
}
|
|
1396
|
+
const { id } = c.req.valid('param');
|
|
1397
|
+
const query = c.req.valid('query');
|
|
1398
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1399
|
+
if (!target.ok) {
|
|
1400
|
+
return c.json({
|
|
1401
|
+
error: target.error,
|
|
1402
|
+
message: target.message,
|
|
1403
|
+
}, target.status);
|
|
1404
|
+
}
|
|
1405
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1406
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1407
|
+
c,
|
|
1408
|
+
instance: target.instance,
|
|
1409
|
+
method: 'POST',
|
|
1410
|
+
path: `/api-keys/${encodeURIComponent(id)}/rotate/stage`,
|
|
1411
|
+
query: forwardQuery,
|
|
1412
|
+
responseSchema: ConsoleApiKeyCreateResponseSchema,
|
|
1413
|
+
fetchImpl,
|
|
1414
|
+
});
|
|
1415
|
+
if (!result.ok) {
|
|
1416
|
+
return jsonResponse(result.body, result.status);
|
|
1417
|
+
}
|
|
1418
|
+
return jsonResponse(result.data, result.status);
|
|
1419
|
+
});
|
|
1420
|
+
routes.post('/api-keys/:id/rotate', describeRoute({
|
|
1421
|
+
tags: ['console-gateway'],
|
|
1422
|
+
summary: 'Rotate API key on a single target instance (requires instance selection)',
|
|
1423
|
+
responses: {
|
|
1424
|
+
200: {
|
|
1425
|
+
description: 'Rotated API key',
|
|
1426
|
+
content: {
|
|
1427
|
+
'application/json': {
|
|
1428
|
+
schema: resolver(ConsoleApiKeyCreateResponseSchema),
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
}), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
|
|
1434
|
+
const auth = await options.authenticate(c);
|
|
1435
|
+
if (!auth) {
|
|
1436
|
+
return unauthorizedResponse(c);
|
|
1437
|
+
}
|
|
1438
|
+
const { id } = c.req.valid('param');
|
|
1439
|
+
const query = c.req.valid('query');
|
|
1440
|
+
const target = resolveSingleInstanceTarget({ instances, query });
|
|
1441
|
+
if (!target.ok) {
|
|
1442
|
+
return c.json({
|
|
1443
|
+
error: target.error,
|
|
1444
|
+
message: target.message,
|
|
1445
|
+
}, target.status);
|
|
1446
|
+
}
|
|
1447
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1448
|
+
const result = await forwardDownstreamJsonRequest({
|
|
1449
|
+
c,
|
|
1450
|
+
instance: target.instance,
|
|
1451
|
+
method: 'POST',
|
|
1452
|
+
path: `/api-keys/${encodeURIComponent(id)}/rotate`,
|
|
1453
|
+
query: forwardQuery,
|
|
1454
|
+
responseSchema: ConsoleApiKeyCreateResponseSchema,
|
|
1455
|
+
fetchImpl,
|
|
1456
|
+
});
|
|
1457
|
+
if (!result.ok) {
|
|
1458
|
+
return jsonResponse(result.body, result.status);
|
|
1459
|
+
}
|
|
1460
|
+
return jsonResponse(result.data, result.status);
|
|
1461
|
+
});
|
|
1462
|
+
routes.get('/stats', describeRoute({
|
|
1463
|
+
tags: ['console-gateway'],
|
|
1464
|
+
summary: 'Get merged sync stats across instances',
|
|
1465
|
+
responses: {
|
|
1466
|
+
200: {
|
|
1467
|
+
description: 'Merged stats',
|
|
1468
|
+
content: {
|
|
1469
|
+
'application/json': {
|
|
1470
|
+
schema: resolver(GatewayStatsResponseSchema),
|
|
1471
|
+
},
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1474
|
+
},
|
|
1475
|
+
}), zValidator('query', GatewayStatsQuerySchema), async (c) => {
|
|
1476
|
+
const auth = await options.authenticate(c);
|
|
1477
|
+
if (!auth) {
|
|
1478
|
+
return unauthorizedResponse(c);
|
|
1479
|
+
}
|
|
1480
|
+
const query = c.req.valid('query');
|
|
1481
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
1482
|
+
if (selectedInstances.length === 0) {
|
|
1483
|
+
return c.json({
|
|
1484
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
1485
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
1486
|
+
}, 400);
|
|
1487
|
+
}
|
|
1488
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1489
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
|
|
1490
|
+
c,
|
|
1491
|
+
instance,
|
|
1492
|
+
path: '/stats',
|
|
1493
|
+
query: forwardQuery,
|
|
1494
|
+
schema: SyncStatsSchema,
|
|
1495
|
+
fetchImpl,
|
|
1496
|
+
})));
|
|
1497
|
+
const failedInstances = results
|
|
1498
|
+
.filter((result) => !result.ok)
|
|
1499
|
+
.map((result) => result.failure);
|
|
1500
|
+
const successfulResults = results.filter((result) => result.ok);
|
|
1501
|
+
if (successfulResults.length === 0) {
|
|
1502
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
1503
|
+
}
|
|
1504
|
+
const statsByInstance = new Map();
|
|
1505
|
+
for (let i = 0; i < selectedInstances.length; i++) {
|
|
1506
|
+
const result = results[i];
|
|
1507
|
+
if (!result || !result.ok)
|
|
1508
|
+
continue;
|
|
1509
|
+
const instance = selectedInstances[i];
|
|
1510
|
+
if (!instance)
|
|
1511
|
+
continue;
|
|
1512
|
+
statsByInstance.set(instance.instanceId, result.data);
|
|
1513
|
+
}
|
|
1514
|
+
const statsValues = Array.from(statsByInstance.values());
|
|
1515
|
+
const sum = (selector) => statsValues.reduce((acc, stats) => acc + selector(stats), 0);
|
|
1516
|
+
const minCommitSeqByInstance = {};
|
|
1517
|
+
const maxCommitSeqByInstance = {};
|
|
1518
|
+
for (const [instanceId, stats] of statsByInstance.entries()) {
|
|
1519
|
+
minCommitSeqByInstance[instanceId] = stats.minCommitSeq;
|
|
1520
|
+
maxCommitSeqByInstance[instanceId] = stats.maxCommitSeq;
|
|
1521
|
+
}
|
|
1522
|
+
return c.json({
|
|
1523
|
+
commitCount: sum((stats) => stats.commitCount),
|
|
1524
|
+
changeCount: sum((stats) => stats.changeCount),
|
|
1525
|
+
minCommitSeq: Math.min(...statsValues.map((stats) => stats.minCommitSeq)),
|
|
1526
|
+
maxCommitSeq: Math.max(...statsValues.map((stats) => stats.maxCommitSeq)),
|
|
1527
|
+
clientCount: sum((stats) => stats.clientCount),
|
|
1528
|
+
activeClientCount: sum((stats) => stats.activeClientCount),
|
|
1529
|
+
minActiveClientCursor: minNullable(statsValues.map((stats) => stats.minActiveClientCursor)),
|
|
1530
|
+
maxActiveClientCursor: maxNullable(statsValues.map((stats) => stats.maxActiveClientCursor)),
|
|
1531
|
+
minCommitSeqByInstance,
|
|
1532
|
+
maxCommitSeqByInstance,
|
|
1533
|
+
partial: failedInstances.length > 0,
|
|
1534
|
+
failedInstances,
|
|
1535
|
+
});
|
|
1536
|
+
});
|
|
1537
|
+
routes.get('/stats/timeseries', describeRoute({
|
|
1538
|
+
tags: ['console-gateway'],
|
|
1539
|
+
summary: 'Get merged time-series stats across instances',
|
|
1540
|
+
responses: {
|
|
1541
|
+
200: {
|
|
1542
|
+
description: 'Merged time-series stats',
|
|
1543
|
+
content: {
|
|
1544
|
+
'application/json': {
|
|
1545
|
+
schema: resolver(GatewayTimeseriesResponseSchema),
|
|
1546
|
+
},
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
}), zValidator('query', GatewayTimeseriesQuerySchema), async (c) => {
|
|
1551
|
+
const auth = await options.authenticate(c);
|
|
1552
|
+
if (!auth) {
|
|
1553
|
+
return unauthorizedResponse(c);
|
|
1554
|
+
}
|
|
1555
|
+
const query = c.req.valid('query');
|
|
1556
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
1557
|
+
if (selectedInstances.length === 0) {
|
|
1558
|
+
return c.json({
|
|
1559
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
1560
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
1561
|
+
}, 400);
|
|
1562
|
+
}
|
|
1563
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1564
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
|
|
1565
|
+
c,
|
|
1566
|
+
instance,
|
|
1567
|
+
path: '/stats/timeseries',
|
|
1568
|
+
query: forwardQuery,
|
|
1569
|
+
schema: TimeseriesStatsResponseSchema,
|
|
1570
|
+
fetchImpl,
|
|
1571
|
+
})));
|
|
1572
|
+
const failedInstances = results
|
|
1573
|
+
.filter((result) => !result.ok)
|
|
1574
|
+
.map((result) => result.failure);
|
|
1575
|
+
const successfulResults = results.filter((result) => result.ok);
|
|
1576
|
+
if (successfulResults.length === 0) {
|
|
1577
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
1578
|
+
}
|
|
1579
|
+
return c.json({
|
|
1580
|
+
buckets: mergeTimeseriesBuckets(successfulResults.map((result) => result.data)),
|
|
1581
|
+
interval: query.interval,
|
|
1582
|
+
range: query.range,
|
|
1583
|
+
partial: failedInstances.length > 0,
|
|
1584
|
+
failedInstances,
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1587
|
+
routes.get('/stats/latency', describeRoute({
|
|
1588
|
+
tags: ['console-gateway'],
|
|
1589
|
+
summary: 'Get merged latency stats across instances',
|
|
1590
|
+
responses: {
|
|
1591
|
+
200: {
|
|
1592
|
+
description: 'Merged latency stats',
|
|
1593
|
+
content: {
|
|
1594
|
+
'application/json': {
|
|
1595
|
+
schema: resolver(GatewayLatencyResponseSchema),
|
|
1596
|
+
},
|
|
1597
|
+
},
|
|
1598
|
+
},
|
|
1599
|
+
},
|
|
1600
|
+
}), zValidator('query', GatewayLatencyQuerySchema), async (c) => {
|
|
1601
|
+
const auth = await options.authenticate(c);
|
|
1602
|
+
if (!auth) {
|
|
1603
|
+
return unauthorizedResponse(c);
|
|
1604
|
+
}
|
|
1605
|
+
const query = c.req.valid('query');
|
|
1606
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
1607
|
+
if (selectedInstances.length === 0) {
|
|
1608
|
+
return c.json({
|
|
1609
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
1610
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
1611
|
+
}, 400);
|
|
1612
|
+
}
|
|
1613
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1614
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
|
|
1615
|
+
c,
|
|
1616
|
+
instance,
|
|
1617
|
+
path: '/stats/latency',
|
|
1618
|
+
query: forwardQuery,
|
|
1619
|
+
schema: LatencyStatsResponseSchema,
|
|
1620
|
+
fetchImpl,
|
|
1621
|
+
})));
|
|
1622
|
+
const failedInstances = results
|
|
1623
|
+
.filter((result) => !result.ok)
|
|
1624
|
+
.map((result) => result.failure);
|
|
1625
|
+
const successfulResults = results.filter((result) => result.ok);
|
|
1626
|
+
if (successfulResults.length === 0) {
|
|
1627
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
1628
|
+
}
|
|
1629
|
+
return c.json({
|
|
1630
|
+
push: averagePercentiles(successfulResults.map((result) => result.data.push)),
|
|
1631
|
+
pull: averagePercentiles(successfulResults.map((result) => result.data.pull)),
|
|
1632
|
+
range: query.range,
|
|
1633
|
+
partial: failedInstances.length > 0,
|
|
1634
|
+
failedInstances,
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
routes.get('/commits', describeRoute({
|
|
1638
|
+
tags: ['console-gateway'],
|
|
1639
|
+
summary: 'List merged commits across instances',
|
|
1640
|
+
responses: {
|
|
1641
|
+
200: {
|
|
1642
|
+
description: 'Merged commits',
|
|
1643
|
+
content: {
|
|
1644
|
+
'application/json': {
|
|
1645
|
+
schema: resolver(GatewayPaginatedResponseSchema(GatewayCommitItemSchema)),
|
|
1646
|
+
},
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
}), zValidator('query', GatewayPaginatedQuerySchema), async (c) => {
|
|
1651
|
+
const auth = await options.authenticate(c);
|
|
1652
|
+
if (!auth) {
|
|
1653
|
+
return unauthorizedResponse(c);
|
|
1654
|
+
}
|
|
1655
|
+
const query = c.req.valid('query');
|
|
1656
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
1657
|
+
if (selectedInstances.length === 0) {
|
|
1658
|
+
return c.json({
|
|
1659
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
1660
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
1661
|
+
}, 400);
|
|
1662
|
+
}
|
|
1663
|
+
const targetCount = query.offset + query.limit;
|
|
1664
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1665
|
+
forwardQuery.delete('limit');
|
|
1666
|
+
forwardQuery.delete('offset');
|
|
1667
|
+
const pageSchema = ConsolePaginatedResponseSchema(ConsoleCommitListItemSchema);
|
|
1668
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
|
|
1669
|
+
c,
|
|
1670
|
+
instance,
|
|
1671
|
+
path: '/commits',
|
|
1672
|
+
query: forwardQuery,
|
|
1673
|
+
targetCount,
|
|
1674
|
+
schema: pageSchema,
|
|
1675
|
+
fetchImpl,
|
|
1676
|
+
})));
|
|
1677
|
+
const failedInstances = results
|
|
1678
|
+
.filter((result) => !result.ok)
|
|
1679
|
+
.map((result) => result.failure);
|
|
1680
|
+
const successful = results
|
|
1681
|
+
.map((result, index) => ({
|
|
1682
|
+
result,
|
|
1683
|
+
instance: selectedInstances[index],
|
|
1684
|
+
}))
|
|
1685
|
+
.filter((entry) => Boolean(entry.instance) && entry.result.ok);
|
|
1686
|
+
if (successful.length === 0) {
|
|
1687
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
1688
|
+
}
|
|
1689
|
+
const merged = successful
|
|
1690
|
+
.flatMap(({ result, instance }) => result.items.map((commit) => ({
|
|
1691
|
+
...commit,
|
|
1692
|
+
instanceId: instance.instanceId,
|
|
1693
|
+
federatedCommitId: `${instance.instanceId}:${commit.commitSeq}`,
|
|
1694
|
+
})))
|
|
1695
|
+
.sort((a, b) => {
|
|
1696
|
+
const byTime = compareIsoDesc(a.createdAt, b.createdAt);
|
|
1697
|
+
if (byTime !== 0)
|
|
1698
|
+
return byTime;
|
|
1699
|
+
const byInstance = a.instanceId.localeCompare(b.instanceId);
|
|
1700
|
+
if (byInstance !== 0)
|
|
1701
|
+
return byInstance;
|
|
1702
|
+
return b.commitSeq - a.commitSeq;
|
|
1703
|
+
});
|
|
1704
|
+
return c.json({
|
|
1705
|
+
items: merged.slice(query.offset, query.offset + query.limit),
|
|
1706
|
+
total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
|
|
1707
|
+
offset: query.offset,
|
|
1708
|
+
limit: query.limit,
|
|
1709
|
+
partial: failedInstances.length > 0,
|
|
1710
|
+
failedInstances,
|
|
1711
|
+
});
|
|
1712
|
+
});
|
|
1713
|
+
routes.get('/commits/:seq', describeRoute({
|
|
1714
|
+
tags: ['console-gateway'],
|
|
1715
|
+
summary: 'Get merged commit detail by federated id',
|
|
1716
|
+
responses: {
|
|
1717
|
+
200: {
|
|
1718
|
+
description: 'Commit detail',
|
|
1719
|
+
content: {
|
|
1720
|
+
'application/json': {
|
|
1721
|
+
schema: resolver(GatewayCommitDetailSchema),
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
},
|
|
1725
|
+
},
|
|
1726
|
+
}), zValidator('param', GatewayCommitPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
|
|
1727
|
+
const auth = await options.authenticate(c);
|
|
1728
|
+
if (!auth) {
|
|
1729
|
+
return unauthorizedResponse(c);
|
|
1730
|
+
}
|
|
1731
|
+
const { seq } = c.req.valid('param');
|
|
1732
|
+
const query = c.req.valid('query');
|
|
1733
|
+
const target = resolveCommitTarget({ seq, instances, query });
|
|
1734
|
+
if (!target.ok) {
|
|
1735
|
+
return c.json({
|
|
1736
|
+
error: target.error,
|
|
1737
|
+
...(target.message ? { message: target.message } : {}),
|
|
1738
|
+
}, target.status);
|
|
1739
|
+
}
|
|
1740
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1741
|
+
const result = await fetchDownstreamJson({
|
|
1742
|
+
c,
|
|
1743
|
+
instance: target.instance,
|
|
1744
|
+
path: `/commits/${target.localCommitSeq}`,
|
|
1745
|
+
query: forwardQuery,
|
|
1746
|
+
schema: ConsoleCommitDetailSchema,
|
|
1747
|
+
fetchImpl,
|
|
1748
|
+
});
|
|
1749
|
+
if (!result.ok) {
|
|
1750
|
+
if (result.failure.status === 404) {
|
|
1751
|
+
return c.json({ error: 'NOT_FOUND' }, 404);
|
|
1752
|
+
}
|
|
1753
|
+
return c.json({
|
|
1754
|
+
error: 'DOWNSTREAM_UNAVAILABLE',
|
|
1755
|
+
failedInstances: [result.failure],
|
|
1756
|
+
}, 502);
|
|
1757
|
+
}
|
|
1758
|
+
return c.json({
|
|
1759
|
+
...result.data,
|
|
1760
|
+
instanceId: target.instance.instanceId,
|
|
1761
|
+
federatedCommitId: `${target.instance.instanceId}:${result.data.commitSeq}`,
|
|
1762
|
+
localCommitSeq: result.data.commitSeq,
|
|
1763
|
+
});
|
|
1764
|
+
});
|
|
1765
|
+
routes.get('/clients', describeRoute({
|
|
1766
|
+
tags: ['console-gateway'],
|
|
1767
|
+
summary: 'List merged clients across instances',
|
|
1768
|
+
responses: {
|
|
1769
|
+
200: {
|
|
1770
|
+
description: 'Merged clients',
|
|
1771
|
+
content: {
|
|
1772
|
+
'application/json': {
|
|
1773
|
+
schema: resolver(GatewayPaginatedResponseSchema(GatewayClientItemSchema)),
|
|
1774
|
+
},
|
|
1775
|
+
},
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
}), zValidator('query', GatewayPaginatedQuerySchema), async (c) => {
|
|
1779
|
+
const auth = await options.authenticate(c);
|
|
1780
|
+
if (!auth) {
|
|
1781
|
+
return unauthorizedResponse(c);
|
|
1782
|
+
}
|
|
1783
|
+
const query = c.req.valid('query');
|
|
1784
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
1785
|
+
if (selectedInstances.length === 0) {
|
|
1786
|
+
return c.json({
|
|
1787
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
1788
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
1789
|
+
}, 400);
|
|
1790
|
+
}
|
|
1791
|
+
const targetCount = query.offset + query.limit;
|
|
1792
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1793
|
+
forwardQuery.delete('limit');
|
|
1794
|
+
forwardQuery.delete('offset');
|
|
1795
|
+
const pageSchema = ConsolePaginatedResponseSchema(ConsoleClientSchema);
|
|
1796
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
|
|
1797
|
+
c,
|
|
1798
|
+
instance,
|
|
1799
|
+
path: '/clients',
|
|
1800
|
+
query: forwardQuery,
|
|
1801
|
+
targetCount,
|
|
1802
|
+
schema: pageSchema,
|
|
1803
|
+
fetchImpl,
|
|
1804
|
+
})));
|
|
1805
|
+
const failedInstances = results
|
|
1806
|
+
.filter((result) => !result.ok)
|
|
1807
|
+
.map((result) => result.failure);
|
|
1808
|
+
const successful = results
|
|
1809
|
+
.map((result, index) => ({
|
|
1810
|
+
result,
|
|
1811
|
+
instance: selectedInstances[index],
|
|
1812
|
+
}))
|
|
1813
|
+
.filter((entry) => Boolean(entry.instance) && entry.result.ok);
|
|
1814
|
+
if (successful.length === 0) {
|
|
1815
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
1816
|
+
}
|
|
1817
|
+
const merged = successful
|
|
1818
|
+
.flatMap(({ result, instance }) => result.items.map((client) => ({
|
|
1819
|
+
...client,
|
|
1820
|
+
instanceId: instance.instanceId,
|
|
1821
|
+
federatedClientId: `${instance.instanceId}:${client.clientId}`,
|
|
1822
|
+
})))
|
|
1823
|
+
.sort((a, b) => {
|
|
1824
|
+
const byTime = compareIsoDesc(a.updatedAt, b.updatedAt);
|
|
1825
|
+
if (byTime !== 0)
|
|
1826
|
+
return byTime;
|
|
1827
|
+
const byInstance = a.instanceId.localeCompare(b.instanceId);
|
|
1828
|
+
if (byInstance !== 0)
|
|
1829
|
+
return byInstance;
|
|
1830
|
+
return a.clientId.localeCompare(b.clientId);
|
|
1831
|
+
});
|
|
1832
|
+
return c.json({
|
|
1833
|
+
items: merged.slice(query.offset, query.offset + query.limit),
|
|
1834
|
+
total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
|
|
1835
|
+
offset: query.offset,
|
|
1836
|
+
limit: query.limit,
|
|
1837
|
+
partial: failedInstances.length > 0,
|
|
1838
|
+
failedInstances,
|
|
1839
|
+
});
|
|
1840
|
+
});
|
|
1841
|
+
routes.get('/timeline', describeRoute({
|
|
1842
|
+
tags: ['console-gateway'],
|
|
1843
|
+
summary: 'List merged timeline items across instances',
|
|
1844
|
+
responses: {
|
|
1845
|
+
200: {
|
|
1846
|
+
description: 'Merged timeline',
|
|
1847
|
+
content: {
|
|
1848
|
+
'application/json': {
|
|
1849
|
+
schema: resolver(GatewayPaginatedResponseSchema(GatewayTimelineItemSchema)),
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
},
|
|
1854
|
+
}), zValidator('query', GatewayTimelineQuerySchema), async (c) => {
|
|
1855
|
+
const auth = await options.authenticate(c);
|
|
1856
|
+
if (!auth) {
|
|
1857
|
+
return unauthorizedResponse(c);
|
|
1858
|
+
}
|
|
1859
|
+
const query = c.req.valid('query');
|
|
1860
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
1861
|
+
if (selectedInstances.length === 0) {
|
|
1862
|
+
return c.json({
|
|
1863
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
1864
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
1865
|
+
}, 400);
|
|
1866
|
+
}
|
|
1867
|
+
const targetCount = query.offset + query.limit;
|
|
1868
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1869
|
+
forwardQuery.delete('limit');
|
|
1870
|
+
forwardQuery.delete('offset');
|
|
1871
|
+
const pageSchema = ConsolePaginatedResponseSchema(ConsoleTimelineItemSchema);
|
|
1872
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
|
|
1873
|
+
c,
|
|
1874
|
+
instance,
|
|
1875
|
+
path: '/timeline',
|
|
1876
|
+
query: forwardQuery,
|
|
1877
|
+
targetCount,
|
|
1878
|
+
schema: pageSchema,
|
|
1879
|
+
fetchImpl,
|
|
1880
|
+
})));
|
|
1881
|
+
const failedInstances = results
|
|
1882
|
+
.filter((result) => !result.ok)
|
|
1883
|
+
.map((result) => result.failure);
|
|
1884
|
+
const successful = results
|
|
1885
|
+
.map((result, index) => ({
|
|
1886
|
+
result,
|
|
1887
|
+
instance: selectedInstances[index],
|
|
1888
|
+
}))
|
|
1889
|
+
.filter((entry) => Boolean(entry.instance) && entry.result.ok);
|
|
1890
|
+
if (successful.length === 0) {
|
|
1891
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
1892
|
+
}
|
|
1893
|
+
const merged = successful
|
|
1894
|
+
.flatMap(({ result, instance }) => result.items.map((item) => {
|
|
1895
|
+
const localCommitSeq = item.type === 'commit' ? (item.commit?.commitSeq ?? null) : null;
|
|
1896
|
+
const localEventId = item.type === 'event' ? (item.event?.eventId ?? null) : null;
|
|
1897
|
+
const localIdSegment = item.type === 'commit'
|
|
1898
|
+
? String(localCommitSeq ?? 'unknown')
|
|
1899
|
+
: String(localEventId ?? 'unknown');
|
|
1900
|
+
return {
|
|
1901
|
+
...item,
|
|
1902
|
+
instanceId: instance.instanceId,
|
|
1903
|
+
federatedTimelineId: `${instance.instanceId}:${item.type}:${localIdSegment}`,
|
|
1904
|
+
localCommitSeq,
|
|
1905
|
+
localEventId,
|
|
1906
|
+
};
|
|
1907
|
+
}))
|
|
1908
|
+
.sort((a, b) => {
|
|
1909
|
+
const byTime = compareIsoDesc(a.timestamp, b.timestamp);
|
|
1910
|
+
if (byTime !== 0)
|
|
1911
|
+
return byTime;
|
|
1912
|
+
const byInstance = a.instanceId.localeCompare(b.instanceId);
|
|
1913
|
+
if (byInstance !== 0)
|
|
1914
|
+
return byInstance;
|
|
1915
|
+
const aLocalId = a.localCommitSeq ?? a.localEventId ?? 0;
|
|
1916
|
+
const bLocalId = b.localCommitSeq ?? b.localEventId ?? 0;
|
|
1917
|
+
return bLocalId - aLocalId;
|
|
1918
|
+
});
|
|
1919
|
+
return c.json({
|
|
1920
|
+
items: merged.slice(query.offset, query.offset + query.limit),
|
|
1921
|
+
total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
|
|
1922
|
+
offset: query.offset,
|
|
1923
|
+
limit: query.limit,
|
|
1924
|
+
partial: failedInstances.length > 0,
|
|
1925
|
+
failedInstances,
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1928
|
+
routes.get('/operations', describeRoute({
|
|
1929
|
+
tags: ['console-gateway'],
|
|
1930
|
+
summary: 'List merged operation events across instances',
|
|
1931
|
+
responses: {
|
|
1932
|
+
200: {
|
|
1933
|
+
description: 'Merged operations',
|
|
1934
|
+
content: {
|
|
1935
|
+
'application/json': {
|
|
1936
|
+
schema: resolver(GatewayPaginatedResponseSchema(GatewayOperationItemSchema)),
|
|
1937
|
+
},
|
|
1938
|
+
},
|
|
1939
|
+
},
|
|
1940
|
+
},
|
|
1941
|
+
}), zValidator('query', GatewayOperationsQuerySchema), async (c) => {
|
|
1942
|
+
const auth = await options.authenticate(c);
|
|
1943
|
+
if (!auth) {
|
|
1944
|
+
return unauthorizedResponse(c);
|
|
1945
|
+
}
|
|
1946
|
+
const query = c.req.valid('query');
|
|
1947
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
1948
|
+
if (selectedInstances.length === 0) {
|
|
1949
|
+
return c.json({
|
|
1950
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
1951
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
1952
|
+
}, 400);
|
|
1953
|
+
}
|
|
1954
|
+
const targetCount = query.offset + query.limit;
|
|
1955
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
1956
|
+
forwardQuery.delete('limit');
|
|
1957
|
+
forwardQuery.delete('offset');
|
|
1958
|
+
const pageSchema = ConsolePaginatedResponseSchema(ConsoleOperationEventSchema);
|
|
1959
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
|
|
1960
|
+
c,
|
|
1961
|
+
instance,
|
|
1962
|
+
path: '/operations',
|
|
1963
|
+
query: forwardQuery,
|
|
1964
|
+
targetCount,
|
|
1965
|
+
schema: pageSchema,
|
|
1966
|
+
fetchImpl,
|
|
1967
|
+
})));
|
|
1968
|
+
const failedInstances = results
|
|
1969
|
+
.filter((result) => !result.ok)
|
|
1970
|
+
.map((result) => result.failure);
|
|
1971
|
+
const successful = results
|
|
1972
|
+
.map((result, index) => ({
|
|
1973
|
+
result,
|
|
1974
|
+
instance: selectedInstances[index],
|
|
1975
|
+
}))
|
|
1976
|
+
.filter((entry) => Boolean(entry.instance) && entry.result.ok);
|
|
1977
|
+
if (successful.length === 0) {
|
|
1978
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
1979
|
+
}
|
|
1980
|
+
const merged = successful
|
|
1981
|
+
.flatMap(({ result, instance }) => result.items.map((operation) => ({
|
|
1982
|
+
...operation,
|
|
1983
|
+
instanceId: instance.instanceId,
|
|
1984
|
+
federatedOperationId: `${instance.instanceId}:${operation.operationId}`,
|
|
1985
|
+
localOperationId: operation.operationId,
|
|
1986
|
+
})))
|
|
1987
|
+
.sort((a, b) => {
|
|
1988
|
+
const byTime = compareIsoDesc(a.createdAt, b.createdAt);
|
|
1989
|
+
if (byTime !== 0)
|
|
1990
|
+
return byTime;
|
|
1991
|
+
const byInstance = a.instanceId.localeCompare(b.instanceId);
|
|
1992
|
+
if (byInstance !== 0)
|
|
1993
|
+
return byInstance;
|
|
1994
|
+
return b.localOperationId - a.localOperationId;
|
|
1995
|
+
});
|
|
1996
|
+
return c.json({
|
|
1997
|
+
items: merged.slice(query.offset, query.offset + query.limit),
|
|
1998
|
+
total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
|
|
1999
|
+
offset: query.offset,
|
|
2000
|
+
limit: query.limit,
|
|
2001
|
+
partial: failedInstances.length > 0,
|
|
2002
|
+
failedInstances,
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
routes.get('/events', describeRoute({
|
|
2006
|
+
tags: ['console-gateway'],
|
|
2007
|
+
summary: 'List merged request events across instances',
|
|
2008
|
+
responses: {
|
|
2009
|
+
200: {
|
|
2010
|
+
description: 'Merged events',
|
|
2011
|
+
content: {
|
|
2012
|
+
'application/json': {
|
|
2013
|
+
schema: resolver(GatewayPaginatedResponseSchema(GatewayEventItemSchema)),
|
|
2014
|
+
},
|
|
2015
|
+
},
|
|
2016
|
+
},
|
|
2017
|
+
},
|
|
2018
|
+
}), zValidator('query', GatewayEventsQuerySchema), async (c) => {
|
|
2019
|
+
const auth = await options.authenticate(c);
|
|
2020
|
+
if (!auth) {
|
|
2021
|
+
return unauthorizedResponse(c);
|
|
2022
|
+
}
|
|
2023
|
+
const query = c.req.valid('query');
|
|
2024
|
+
const selectedInstances = selectInstances({ instances, query });
|
|
2025
|
+
if (selectedInstances.length === 0) {
|
|
2026
|
+
return c.json({
|
|
2027
|
+
error: 'NO_INSTANCES_SELECTED',
|
|
2028
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
2029
|
+
}, 400);
|
|
2030
|
+
}
|
|
2031
|
+
const targetCount = query.offset + query.limit;
|
|
2032
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
2033
|
+
forwardQuery.delete('limit');
|
|
2034
|
+
forwardQuery.delete('offset');
|
|
2035
|
+
const pageSchema = ConsolePaginatedResponseSchema(ConsoleRequestEventSchema);
|
|
2036
|
+
const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
|
|
2037
|
+
c,
|
|
2038
|
+
instance,
|
|
2039
|
+
path: '/events',
|
|
2040
|
+
query: forwardQuery,
|
|
2041
|
+
targetCount,
|
|
2042
|
+
schema: pageSchema,
|
|
2043
|
+
fetchImpl,
|
|
2044
|
+
})));
|
|
2045
|
+
const failedInstances = results
|
|
2046
|
+
.filter((result) => !result.ok)
|
|
2047
|
+
.map((result) => result.failure);
|
|
2048
|
+
const successful = results
|
|
2049
|
+
.map((result, index) => ({
|
|
2050
|
+
result,
|
|
2051
|
+
instance: selectedInstances[index],
|
|
2052
|
+
}))
|
|
2053
|
+
.filter((entry) => Boolean(entry.instance) && entry.result.ok);
|
|
2054
|
+
if (successful.length === 0) {
|
|
2055
|
+
return allInstancesFailedResponse(c, failedInstances);
|
|
2056
|
+
}
|
|
2057
|
+
const merged = successful
|
|
2058
|
+
.flatMap(({ result, instance }) => result.items.map((event) => ({
|
|
2059
|
+
...event,
|
|
2060
|
+
instanceId: instance.instanceId,
|
|
2061
|
+
federatedEventId: `${instance.instanceId}:${event.eventId}`,
|
|
2062
|
+
localEventId: event.eventId,
|
|
2063
|
+
})))
|
|
2064
|
+
.sort((a, b) => {
|
|
2065
|
+
const byTime = compareIsoDesc(a.createdAt, b.createdAt);
|
|
2066
|
+
if (byTime !== 0)
|
|
2067
|
+
return byTime;
|
|
2068
|
+
const byInstance = a.instanceId.localeCompare(b.instanceId);
|
|
2069
|
+
if (byInstance !== 0)
|
|
2070
|
+
return byInstance;
|
|
2071
|
+
return b.localEventId - a.localEventId;
|
|
2072
|
+
});
|
|
2073
|
+
return c.json({
|
|
2074
|
+
items: merged.slice(query.offset, query.offset + query.limit),
|
|
2075
|
+
total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
|
|
2076
|
+
offset: query.offset,
|
|
2077
|
+
limit: query.limit,
|
|
2078
|
+
partial: failedInstances.length > 0,
|
|
2079
|
+
failedInstances,
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
2082
|
+
if (options.websocket?.enabled &&
|
|
2083
|
+
options.websocket?.upgradeWebSocket !== undefined) {
|
|
2084
|
+
const upgradeWebSocket = options.websocket.upgradeWebSocket;
|
|
2085
|
+
const heartbeatIntervalMs = options.websocket.heartbeatIntervalMs ?? 30000;
|
|
2086
|
+
const createDownstreamSocket = options.websocket.createWebSocket ??
|
|
2087
|
+
((url) => new WebSocket(url));
|
|
2088
|
+
const liveState = new WeakMap();
|
|
2089
|
+
routes.get('/events/live', upgradeWebSocket(async (c) => {
|
|
2090
|
+
const auth = await options.authenticate(c);
|
|
2091
|
+
const partitionId = c.req.query('partitionId')?.trim() || undefined;
|
|
2092
|
+
const replaySince = c.req.query('since')?.trim() || undefined;
|
|
2093
|
+
const replayLimitRaw = c.req.query('replayLimit');
|
|
2094
|
+
const replayLimitNumber = replayLimitRaw
|
|
2095
|
+
? Number.parseInt(replayLimitRaw, 10)
|
|
2096
|
+
: Number.NaN;
|
|
2097
|
+
const replayLimit = Number.isFinite(replayLimitNumber)
|
|
2098
|
+
? Math.max(1, Math.min(500, replayLimitNumber))
|
|
2099
|
+
: 100;
|
|
2100
|
+
const selectedInstances = selectInstances({
|
|
2101
|
+
instances,
|
|
2102
|
+
query: {
|
|
2103
|
+
instanceId: c.req.query('instanceId') ?? undefined,
|
|
2104
|
+
instanceIds: c.req.query('instanceIds') ?? undefined,
|
|
2105
|
+
},
|
|
2106
|
+
});
|
|
2107
|
+
const cleanup = (ws) => {
|
|
2108
|
+
const state = liveState.get(ws);
|
|
2109
|
+
if (!state)
|
|
2110
|
+
return;
|
|
2111
|
+
clearInterval(state.heartbeatInterval);
|
|
2112
|
+
for (const downstream of state.downstreamSockets) {
|
|
2113
|
+
try {
|
|
2114
|
+
downstream.close();
|
|
2115
|
+
}
|
|
2116
|
+
catch {
|
|
2117
|
+
// no-op
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
liveState.delete(ws);
|
|
2121
|
+
};
|
|
2122
|
+
return {
|
|
2123
|
+
onOpen(_event, ws) {
|
|
2124
|
+
if (!auth) {
|
|
2125
|
+
ws.send(JSON.stringify({ type: 'error', message: 'UNAUTHENTICATED' }));
|
|
2126
|
+
ws.close(4001, 'Unauthenticated');
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
if (selectedInstances.length === 0) {
|
|
2130
|
+
ws.send(JSON.stringify({
|
|
2131
|
+
type: 'error',
|
|
2132
|
+
message: 'No enabled instances matched the provided instance filter.',
|
|
2133
|
+
}));
|
|
2134
|
+
ws.close(4004, 'No instances selected');
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const downstreamSockets = [];
|
|
2138
|
+
for (const instance of selectedInstances) {
|
|
2139
|
+
const downstreamQuery = new URLSearchParams();
|
|
2140
|
+
const downstreamToken = resolveForwardBearerToken({
|
|
2141
|
+
c,
|
|
2142
|
+
instance,
|
|
2143
|
+
});
|
|
2144
|
+
if (downstreamToken) {
|
|
2145
|
+
downstreamQuery.set('token', downstreamToken);
|
|
2146
|
+
}
|
|
2147
|
+
if (partitionId) {
|
|
2148
|
+
downstreamQuery.set('partitionId', partitionId);
|
|
2149
|
+
}
|
|
2150
|
+
if (replaySince) {
|
|
2151
|
+
downstreamQuery.set('since', replaySince);
|
|
2152
|
+
}
|
|
2153
|
+
downstreamQuery.set('replayLimit', String(replayLimit));
|
|
2154
|
+
const downstreamUrl = buildConsoleEndpointUrl({
|
|
2155
|
+
instance,
|
|
2156
|
+
requestUrl: c.req.url,
|
|
2157
|
+
path: '/events/live',
|
|
2158
|
+
query: downstreamQuery,
|
|
2159
|
+
});
|
|
2160
|
+
const downstreamSocket = createDownstreamSocket(downstreamUrl);
|
|
2161
|
+
downstreamSocket.onmessage = (message) => {
|
|
2162
|
+
if (typeof message.data !== 'string') {
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
const payload = JSON.parse(message.data);
|
|
2167
|
+
if (typeof payload.type === 'string' &&
|
|
2168
|
+
(payload.type === 'connected' ||
|
|
2169
|
+
payload.type === 'heartbeat')) {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
const payloadData = payload.data &&
|
|
2173
|
+
typeof payload.data === 'object' &&
|
|
2174
|
+
!Array.isArray(payload.data)
|
|
2175
|
+
? { ...payload.data, instanceId: instance.instanceId }
|
|
2176
|
+
: { instanceId: instance.instanceId };
|
|
2177
|
+
const event = {
|
|
2178
|
+
...payload,
|
|
2179
|
+
data: payloadData,
|
|
2180
|
+
instanceId: instance.instanceId,
|
|
2181
|
+
timestamp: typeof payload.timestamp === 'string'
|
|
2182
|
+
? payload.timestamp
|
|
2183
|
+
: new Date().toISOString(),
|
|
2184
|
+
};
|
|
2185
|
+
ws.send(JSON.stringify(event));
|
|
2186
|
+
}
|
|
2187
|
+
catch {
|
|
2188
|
+
// Ignore malformed downstream events
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
downstreamSocket.onerror = () => {
|
|
2192
|
+
try {
|
|
2193
|
+
ws.send(JSON.stringify({
|
|
2194
|
+
type: 'instance_error',
|
|
2195
|
+
instanceId: instance.instanceId,
|
|
2196
|
+
timestamp: new Date().toISOString(),
|
|
2197
|
+
}));
|
|
2198
|
+
}
|
|
2199
|
+
catch {
|
|
2200
|
+
// ignore send errors
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
downstreamSockets.push(downstreamSocket);
|
|
2204
|
+
}
|
|
2205
|
+
ws.send(JSON.stringify({
|
|
2206
|
+
type: 'connected',
|
|
2207
|
+
timestamp: new Date().toISOString(),
|
|
2208
|
+
instanceCount: selectedInstances.length,
|
|
2209
|
+
}));
|
|
2210
|
+
const heartbeatInterval = setInterval(() => {
|
|
2211
|
+
try {
|
|
2212
|
+
ws.send(JSON.stringify({
|
|
2213
|
+
type: 'heartbeat',
|
|
2214
|
+
timestamp: new Date().toISOString(),
|
|
2215
|
+
}));
|
|
2216
|
+
}
|
|
2217
|
+
catch {
|
|
2218
|
+
clearInterval(heartbeatInterval);
|
|
2219
|
+
}
|
|
2220
|
+
}, heartbeatIntervalMs);
|
|
2221
|
+
liveState.set(ws, {
|
|
2222
|
+
downstreamSockets,
|
|
2223
|
+
heartbeatInterval,
|
|
2224
|
+
});
|
|
2225
|
+
},
|
|
2226
|
+
onClose(_event, ws) {
|
|
2227
|
+
cleanup(ws);
|
|
2228
|
+
},
|
|
2229
|
+
onError(_event, ws) {
|
|
2230
|
+
cleanup(ws);
|
|
2231
|
+
},
|
|
2232
|
+
};
|
|
2233
|
+
}));
|
|
2234
|
+
}
|
|
2235
|
+
routes.get('/events/:id', describeRoute({
|
|
2236
|
+
tags: ['console-gateway'],
|
|
2237
|
+
summary: 'Get merged event detail by federated id',
|
|
2238
|
+
responses: {
|
|
2239
|
+
200: {
|
|
2240
|
+
description: 'Event detail',
|
|
2241
|
+
content: {
|
|
2242
|
+
'application/json': {
|
|
2243
|
+
schema: resolver(GatewayEventItemSchema),
|
|
2244
|
+
},
|
|
2245
|
+
},
|
|
2246
|
+
},
|
|
2247
|
+
},
|
|
2248
|
+
}), zValidator('param', GatewayEventPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
|
|
2249
|
+
const auth = await options.authenticate(c);
|
|
2250
|
+
if (!auth) {
|
|
2251
|
+
return unauthorizedResponse(c);
|
|
2252
|
+
}
|
|
2253
|
+
const { id } = c.req.valid('param');
|
|
2254
|
+
const query = c.req.valid('query');
|
|
2255
|
+
const target = resolveEventTarget({
|
|
2256
|
+
id,
|
|
2257
|
+
instances,
|
|
2258
|
+
query,
|
|
2259
|
+
});
|
|
2260
|
+
if (!target.ok) {
|
|
2261
|
+
return c.json({
|
|
2262
|
+
error: target.error,
|
|
2263
|
+
...(target.message ? { message: target.message } : {}),
|
|
2264
|
+
}, target.status);
|
|
2265
|
+
}
|
|
2266
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
2267
|
+
const result = await fetchDownstreamJson({
|
|
2268
|
+
c,
|
|
2269
|
+
instance: target.instance,
|
|
2270
|
+
path: `/events/${target.localEventId}`,
|
|
2271
|
+
query: forwardQuery,
|
|
2272
|
+
schema: ConsoleRequestEventSchema,
|
|
2273
|
+
fetchImpl,
|
|
2274
|
+
});
|
|
2275
|
+
if (!result.ok) {
|
|
2276
|
+
if (result.failure.status === 404) {
|
|
2277
|
+
return c.json({ error: 'NOT_FOUND' }, 404);
|
|
2278
|
+
}
|
|
2279
|
+
return c.json({
|
|
2280
|
+
error: 'DOWNSTREAM_UNAVAILABLE',
|
|
2281
|
+
failedInstances: [result.failure],
|
|
2282
|
+
}, 502);
|
|
2283
|
+
}
|
|
2284
|
+
return c.json({
|
|
2285
|
+
...result.data,
|
|
2286
|
+
instanceId: target.instance.instanceId,
|
|
2287
|
+
federatedEventId: `${target.instance.instanceId}:${result.data.eventId}`,
|
|
2288
|
+
localEventId: result.data.eventId,
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
routes.get('/events/:id/payload', describeRoute({
|
|
2292
|
+
tags: ['console-gateway'],
|
|
2293
|
+
summary: 'Get merged event payload by federated id',
|
|
2294
|
+
responses: {
|
|
2295
|
+
200: {
|
|
2296
|
+
description: 'Event payload',
|
|
2297
|
+
content: {
|
|
2298
|
+
'application/json': {
|
|
2299
|
+
schema: resolver(GatewayEventPayloadSchema),
|
|
2300
|
+
},
|
|
2301
|
+
},
|
|
2302
|
+
},
|
|
2303
|
+
},
|
|
2304
|
+
}), zValidator('param', GatewayEventPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
|
|
2305
|
+
const auth = await options.authenticate(c);
|
|
2306
|
+
if (!auth) {
|
|
2307
|
+
return unauthorizedResponse(c);
|
|
2308
|
+
}
|
|
2309
|
+
const { id } = c.req.valid('param');
|
|
2310
|
+
const query = c.req.valid('query');
|
|
2311
|
+
const target = resolveEventTarget({
|
|
2312
|
+
id,
|
|
2313
|
+
instances,
|
|
2314
|
+
query,
|
|
2315
|
+
});
|
|
2316
|
+
if (!target.ok) {
|
|
2317
|
+
return c.json({
|
|
2318
|
+
error: target.error,
|
|
2319
|
+
...(target.message ? { message: target.message } : {}),
|
|
2320
|
+
}, target.status);
|
|
2321
|
+
}
|
|
2322
|
+
const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
|
|
2323
|
+
const result = await fetchDownstreamJson({
|
|
2324
|
+
c,
|
|
2325
|
+
instance: target.instance,
|
|
2326
|
+
path: `/events/${target.localEventId}/payload`,
|
|
2327
|
+
query: forwardQuery,
|
|
2328
|
+
schema: ConsoleRequestPayloadSchema,
|
|
2329
|
+
fetchImpl,
|
|
2330
|
+
});
|
|
2331
|
+
if (!result.ok) {
|
|
2332
|
+
if (result.failure.status === 404) {
|
|
2333
|
+
return c.json({ error: 'NOT_FOUND' }, 404);
|
|
2334
|
+
}
|
|
2335
|
+
return c.json({
|
|
2336
|
+
error: 'DOWNSTREAM_UNAVAILABLE',
|
|
2337
|
+
failedInstances: [result.failure],
|
|
2338
|
+
}, 502);
|
|
2339
|
+
}
|
|
2340
|
+
return c.json({
|
|
2341
|
+
...result.data,
|
|
2342
|
+
instanceId: target.instance.instanceId,
|
|
2343
|
+
federatedEventId: `${target.instance.instanceId}:${target.localEventId}`,
|
|
2344
|
+
localEventId: target.localEventId,
|
|
2345
|
+
});
|
|
2346
|
+
});
|
|
2347
|
+
return routes;
|
|
2348
|
+
}
|
|
2349
|
+
//# sourceMappingURL=gateway.js.map
|