@syncular/server-hono 0.0.6-158 → 0.0.6-165
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/blobs.d.ts +10 -4
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +260 -26
- package/dist/blobs.js.map +1 -1
- package/dist/console/gateway.d.ts +4 -0
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +97 -60
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/route-descriptor.d.ts +6 -0
- package/dist/console/route-descriptor.d.ts.map +1 -0
- package/dist/console/route-descriptor.js +16 -0
- package/dist/console/route-descriptor.js.map +1 -0
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +153 -108
- package/dist/console/routes.js.map +1 -1
- package/dist/console/schema-errors.d.ts +2 -0
- package/dist/console/schema-errors.d.ts.map +1 -0
- package/dist/console/schema-errors.js +17 -0
- package/dist/console/schema-errors.js.map +1 -0
- package/dist/console/schemas.js +1 -1
- package/dist/console/schemas.js.map +1 -1
- package/dist/console/types.d.ts +32 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +13 -10
- package/dist/create-server.js.map +1 -1
- package/dist/proxy/routes.d.ts +10 -0
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +57 -6
- package/dist/proxy/routes.js.map +1 -1
- package/dist/routes.d.ts +21 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +338 -352
- package/dist/routes.js.map +1 -1
- package/package.json +7 -6
- package/src/__tests__/blob-routes.test.ts +286 -18
- package/src/__tests__/console-gateway-live-routes.test.ts +61 -1
- package/src/__tests__/console-routes.test.ts +30 -1
- package/src/__tests__/create-server.test.ts +237 -1
- package/src/__tests__/pull-chunk-storage.test.ts +98 -0
- package/src/blobs.ts +360 -34
- package/src/console/gateway.ts +335 -288
- package/src/console/route-descriptor.ts +22 -0
- package/src/console/routes.ts +327 -248
- package/src/console/schema-errors.ts +23 -0
- package/src/console/schemas.ts +1 -1
- package/src/console/types.ts +32 -0
- package/src/create-server.ts +13 -10
- package/src/proxy/routes.ts +73 -9
- package/src/routes.ts +449 -396
package/dist/console/routes.js
CHANGED
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
* - DELETE /clients/:id - Evict client
|
|
16
16
|
*/
|
|
17
17
|
import { logSyncEvent } from '@syncular/core';
|
|
18
|
-
import { compactChanges, computePruneWatermarkCommitSeq, notifyExternalDataChange, pruneSync, readSyncStats, } from '@syncular/server';
|
|
18
|
+
import { coerceNumber, compactChanges, computePruneWatermarkCommitSeq, notifyExternalDataChange, parseJsonValue, pruneSync, readSyncStats, } from '@syncular/server';
|
|
19
19
|
import { Hono } from 'hono';
|
|
20
20
|
import { cors } from 'hono/cors';
|
|
21
|
-
import {
|
|
21
|
+
import { resolver, validator as zValidator } from 'hono-openapi';
|
|
22
22
|
import { sql } from 'kysely';
|
|
23
23
|
import { z } from 'zod';
|
|
24
24
|
import { closeUnauthenticatedSocket, parseBearerToken, parseWebSocketAuthToken, } from './live-auth.js';
|
|
25
|
+
import { describeConsoleRoute } from './route-descriptor.js';
|
|
26
|
+
import { isBenignConsoleSchemaError } from './schema-errors.js';
|
|
25
27
|
import { ApiKeyTypeSchema, ConsoleApiKeyBulkRevokeRequestSchema, ConsoleApiKeyBulkRevokeResponseSchema, ConsoleApiKeyCreateRequestSchema, ConsoleApiKeyCreateResponseSchema, ConsoleApiKeyRevokeResponseSchema, ConsoleApiKeySchema, ConsoleBlobDeleteResponseSchema, ConsoleBlobListQuerySchema, ConsoleBlobListResponseSchema, 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';
|
|
26
28
|
/**
|
|
27
29
|
* Create a simple console event emitter for broadcasting live events.
|
|
@@ -79,19 +81,6 @@ export function createConsoleEventEmitter(options) {
|
|
|
79
81
|
},
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
|
-
function coerceNumber(value) {
|
|
83
|
-
if (value === null || value === undefined)
|
|
84
|
-
return null;
|
|
85
|
-
if (typeof value === 'number')
|
|
86
|
-
return Number.isFinite(value) ? value : null;
|
|
87
|
-
if (typeof value === 'bigint')
|
|
88
|
-
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
89
|
-
if (typeof value === 'string') {
|
|
90
|
-
const n = Number(value);
|
|
91
|
-
return Number.isFinite(n) ? n : null;
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
84
|
function parseDate(value) {
|
|
96
85
|
if (!value)
|
|
97
86
|
return null;
|
|
@@ -105,17 +94,18 @@ function includesSearchTerm(value, searchTerm) {
|
|
|
105
94
|
return false;
|
|
106
95
|
return value.toLowerCase().includes(searchTerm);
|
|
107
96
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return value;
|
|
113
|
-
try {
|
|
114
|
-
return JSON.parse(value);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return value;
|
|
97
|
+
function parseJsonRecord(value) {
|
|
98
|
+
const parsed = parseJsonValue(value);
|
|
99
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
100
|
+
return {};
|
|
118
101
|
}
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
function parseJsonStringArray(value) {
|
|
105
|
+
const parsed = parseJsonValue(value);
|
|
106
|
+
if (!Array.isArray(parsed))
|
|
107
|
+
return [];
|
|
108
|
+
return parsed.filter((entry) => typeof entry === 'string');
|
|
119
109
|
}
|
|
120
110
|
function parseScopesSummary(value) {
|
|
121
111
|
const parsed = parseJsonValue(value);
|
|
@@ -267,6 +257,7 @@ const DEFAULT_REQUEST_EVENTS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
267
257
|
const DEFAULT_REQUEST_EVENTS_MAX_ROWS = 10_000;
|
|
268
258
|
const DEFAULT_OPERATION_EVENTS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
269
259
|
const DEFAULT_OPERATION_EVENTS_MAX_ROWS = 5_000;
|
|
260
|
+
const DEFAULT_TIMELINE_SCAN_MAX_ROWS = 10_000;
|
|
270
261
|
const DEFAULT_AUTO_EVENTS_PRUNE_INTERVAL_MS = 5 * 60 * 1000;
|
|
271
262
|
function readNonNegativeInteger(value, fallback) {
|
|
272
263
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
@@ -294,12 +285,16 @@ export function createConsoleRoutes(options) {
|
|
|
294
285
|
const requestEventsMaxRows = readNonNegativeInteger(options.maintenance?.requestEventsMaxRows, DEFAULT_REQUEST_EVENTS_MAX_ROWS);
|
|
295
286
|
const operationEventsMaxAgeMs = readNonNegativeInteger(options.maintenance?.operationEventsMaxAgeMs, DEFAULT_OPERATION_EVENTS_MAX_AGE_MS);
|
|
296
287
|
const operationEventsMaxRows = readNonNegativeInteger(options.maintenance?.operationEventsMaxRows, DEFAULT_OPERATION_EVENTS_MAX_ROWS);
|
|
288
|
+
const timelineScanMaxRows = readNonNegativeInteger(options.maintenance?.timelineScanMaxRows, DEFAULT_TIMELINE_SCAN_MAX_ROWS);
|
|
297
289
|
const autoEventsPruneIntervalMs = readNonNegativeInteger(options.maintenance?.autoPruneIntervalMs, DEFAULT_AUTO_EVENTS_PRUNE_INTERVAL_MS);
|
|
298
290
|
let lastEventsPruneRunAt = 0;
|
|
299
291
|
// Ensure console schema exists before handlers query console tables.
|
|
300
292
|
const consoleSchemaReadyPromise = (options.consoleSchemaReady ??
|
|
301
293
|
options.dialect.ensureConsoleSchema?.(options.db) ??
|
|
302
294
|
Promise.resolve()).catch((err) => {
|
|
295
|
+
if (isBenignConsoleSchemaError(err)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
303
298
|
console.error('[console] Failed to ensure console schema:', err);
|
|
304
299
|
throw err;
|
|
305
300
|
});
|
|
@@ -630,8 +625,7 @@ export function createConsoleRoutes(options) {
|
|
|
630
625
|
// -------------------------------------------------------------------------
|
|
631
626
|
// GET /stats
|
|
632
627
|
// -------------------------------------------------------------------------
|
|
633
|
-
routes.get('/stats',
|
|
634
|
-
tags: ['console'],
|
|
628
|
+
routes.get('/stats', describeConsoleRoute({
|
|
635
629
|
summary: 'Get sync statistics',
|
|
636
630
|
responses: {
|
|
637
631
|
200: {
|
|
@@ -661,8 +655,7 @@ export function createConsoleRoutes(options) {
|
|
|
661
655
|
// -------------------------------------------------------------------------
|
|
662
656
|
// GET /stats/timeseries
|
|
663
657
|
// -------------------------------------------------------------------------
|
|
664
|
-
routes.get('/stats/timeseries',
|
|
665
|
-
tags: ['console'],
|
|
658
|
+
routes.get('/stats/timeseries', describeConsoleRoute({
|
|
666
659
|
summary: 'Get time-series statistics',
|
|
667
660
|
responses: {
|
|
668
661
|
200: {
|
|
@@ -819,8 +812,7 @@ export function createConsoleRoutes(options) {
|
|
|
819
812
|
// -------------------------------------------------------------------------
|
|
820
813
|
// GET /stats/latency
|
|
821
814
|
// -------------------------------------------------------------------------
|
|
822
|
-
routes.get('/stats/latency',
|
|
823
|
-
tags: ['console'],
|
|
815
|
+
routes.get('/stats/latency', describeConsoleRoute({
|
|
824
816
|
summary: 'Get latency percentiles',
|
|
825
817
|
responses: {
|
|
826
818
|
200: {
|
|
@@ -907,8 +899,7 @@ export function createConsoleRoutes(options) {
|
|
|
907
899
|
// -------------------------------------------------------------------------
|
|
908
900
|
// GET /timeline
|
|
909
901
|
// -------------------------------------------------------------------------
|
|
910
|
-
routes.get('/timeline',
|
|
911
|
-
tags: ['console'],
|
|
902
|
+
routes.get('/timeline', describeConsoleRoute({
|
|
912
903
|
summary: 'List timeline items',
|
|
913
904
|
responses: {
|
|
914
905
|
200: {
|
|
@@ -931,6 +922,8 @@ export function createConsoleRoutes(options) {
|
|
|
931
922
|
const items = [];
|
|
932
923
|
const normalizedSearchTerm = search?.trim().toLowerCase() || null;
|
|
933
924
|
const normalizedTable = table?.trim() || null;
|
|
925
|
+
const timelineSourceScanLimit = timelineScanMaxRows > 0 ? timelineScanMaxRows : null;
|
|
926
|
+
let timelineTruncated = false;
|
|
934
927
|
if (view !== 'events' &&
|
|
935
928
|
!eventType &&
|
|
936
929
|
!outcome &&
|
|
@@ -962,8 +955,19 @@ export function createConsoleRoutes(options) {
|
|
|
962
955
|
if (to) {
|
|
963
956
|
commitsQuery = commitsQuery.where('created_at', '<=', to);
|
|
964
957
|
}
|
|
965
|
-
|
|
966
|
-
|
|
958
|
+
let commitsQueryWithOrdering = commitsQuery.orderBy('created_at', 'desc');
|
|
959
|
+
if (timelineSourceScanLimit !== null) {
|
|
960
|
+
commitsQueryWithOrdering = commitsQueryWithOrdering.limit(timelineSourceScanLimit + 1);
|
|
961
|
+
}
|
|
962
|
+
const commitRows = await commitsQueryWithOrdering.execute();
|
|
963
|
+
const scannedCommitRows = timelineSourceScanLimit === null
|
|
964
|
+
? commitRows
|
|
965
|
+
: commitRows.slice(0, timelineSourceScanLimit);
|
|
966
|
+
if (timelineSourceScanLimit !== null &&
|
|
967
|
+
commitRows.length > timelineSourceScanLimit) {
|
|
968
|
+
timelineTruncated = true;
|
|
969
|
+
}
|
|
970
|
+
for (const row of scannedCommitRows) {
|
|
967
971
|
const commit = {
|
|
968
972
|
commitSeq: coerceNumber(row.commit_seq) ?? 0,
|
|
969
973
|
actorId: row.actor_id ?? '',
|
|
@@ -1012,8 +1016,19 @@ export function createConsoleRoutes(options) {
|
|
|
1012
1016
|
if (to) {
|
|
1013
1017
|
eventsQuery = eventsQuery.where('created_at', '<=', to);
|
|
1014
1018
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1019
|
+
let eventsQueryWithOrdering = eventsQuery.orderBy('created_at', 'desc');
|
|
1020
|
+
if (timelineSourceScanLimit !== null) {
|
|
1021
|
+
eventsQueryWithOrdering = eventsQueryWithOrdering.limit(timelineSourceScanLimit + 1);
|
|
1022
|
+
}
|
|
1023
|
+
const eventRows = await eventsQueryWithOrdering.execute();
|
|
1024
|
+
const scannedEventRows = timelineSourceScanLimit === null
|
|
1025
|
+
? eventRows
|
|
1026
|
+
: eventRows.slice(0, timelineSourceScanLimit);
|
|
1027
|
+
if (timelineSourceScanLimit !== null &&
|
|
1028
|
+
eventRows.length > timelineSourceScanLimit) {
|
|
1029
|
+
timelineTruncated = true;
|
|
1030
|
+
}
|
|
1031
|
+
for (const row of scannedEventRows) {
|
|
1017
1032
|
const event = mapRequestEvent(row);
|
|
1018
1033
|
items.push({
|
|
1019
1034
|
type: 'event',
|
|
@@ -1076,13 +1091,18 @@ export function createConsoleRoutes(options) {
|
|
|
1076
1091
|
limit,
|
|
1077
1092
|
};
|
|
1078
1093
|
c.header('X-Total-Count', String(total));
|
|
1094
|
+
if (timelineTruncated) {
|
|
1095
|
+
c.header('X-Timeline-Truncated', 'true');
|
|
1096
|
+
if (timelineSourceScanLimit !== null) {
|
|
1097
|
+
c.header('X-Timeline-Scan-Limit', String(timelineSourceScanLimit));
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1079
1100
|
return c.json(response, 200);
|
|
1080
1101
|
});
|
|
1081
1102
|
// -------------------------------------------------------------------------
|
|
1082
1103
|
// GET /commits
|
|
1083
1104
|
// -------------------------------------------------------------------------
|
|
1084
|
-
routes.get('/commits',
|
|
1085
|
-
tags: ['console'],
|
|
1105
|
+
routes.get('/commits', describeConsoleRoute({
|
|
1086
1106
|
summary: 'List commits',
|
|
1087
1107
|
responses: {
|
|
1088
1108
|
200: {
|
|
@@ -1150,8 +1170,7 @@ export function createConsoleRoutes(options) {
|
|
|
1150
1170
|
// -------------------------------------------------------------------------
|
|
1151
1171
|
// GET /commits/:seq
|
|
1152
1172
|
// -------------------------------------------------------------------------
|
|
1153
|
-
routes.get('/commits/:seq',
|
|
1154
|
-
tags: ['console'],
|
|
1173
|
+
routes.get('/commits/:seq', describeConsoleRoute({
|
|
1155
1174
|
summary: 'Get commit details',
|
|
1156
1175
|
responses: {
|
|
1157
1176
|
200: {
|
|
@@ -1224,20 +1243,9 @@ export function createConsoleRoutes(options) {
|
|
|
1224
1243
|
table: row.table ?? '',
|
|
1225
1244
|
rowId: row.row_id ?? '',
|
|
1226
1245
|
op: row.op === 'delete' ? 'delete' : 'upsert',
|
|
1227
|
-
rowJson:
|
|
1228
|
-
? (() => {
|
|
1229
|
-
try {
|
|
1230
|
-
return JSON.parse(row.row_json);
|
|
1231
|
-
}
|
|
1232
|
-
catch {
|
|
1233
|
-
return row.row_json;
|
|
1234
|
-
}
|
|
1235
|
-
})()
|
|
1236
|
-
: row.row_json,
|
|
1246
|
+
rowJson: parseJsonValue(row.row_json),
|
|
1237
1247
|
rowVersion: coerceNumber(row.row_version),
|
|
1238
|
-
scopes:
|
|
1239
|
-
? JSON.parse(row.scopes || '{}')
|
|
1240
|
-
: (row.scopes ?? {}),
|
|
1248
|
+
scopes: parseJsonRecord(row.scopes),
|
|
1241
1249
|
}));
|
|
1242
1250
|
const commit = {
|
|
1243
1251
|
commitSeq: coerceNumber(commitRow.commit_seq) ?? 0,
|
|
@@ -1246,11 +1254,7 @@ export function createConsoleRoutes(options) {
|
|
|
1246
1254
|
clientCommitId: commitRow.client_commit_id ?? '',
|
|
1247
1255
|
createdAt: commitRow.created_at ?? '',
|
|
1248
1256
|
changeCount: coerceNumber(commitRow.change_count) ?? 0,
|
|
1249
|
-
affectedTables:
|
|
1250
|
-
? commitRow.affected_tables
|
|
1251
|
-
: typeof commitRow.affected_tables === 'string'
|
|
1252
|
-
? JSON.parse(commitRow.affected_tables || '[]')
|
|
1253
|
-
: [],
|
|
1257
|
+
affectedTables: parseJsonStringArray(commitRow.affected_tables),
|
|
1254
1258
|
changes,
|
|
1255
1259
|
};
|
|
1256
1260
|
return c.json(commit, 200);
|
|
@@ -1258,8 +1262,7 @@ export function createConsoleRoutes(options) {
|
|
|
1258
1262
|
// -------------------------------------------------------------------------
|
|
1259
1263
|
// GET /clients
|
|
1260
1264
|
// -------------------------------------------------------------------------
|
|
1261
|
-
routes.get('/clients',
|
|
1262
|
-
tags: ['console'],
|
|
1265
|
+
routes.get('/clients', describeConsoleRoute({
|
|
1263
1266
|
summary: 'List clients',
|
|
1264
1267
|
responses: {
|
|
1265
1268
|
200: {
|
|
@@ -1385,8 +1388,7 @@ export function createConsoleRoutes(options) {
|
|
|
1385
1388
|
// -------------------------------------------------------------------------
|
|
1386
1389
|
// GET /handlers
|
|
1387
1390
|
// -------------------------------------------------------------------------
|
|
1388
|
-
routes.get('/handlers',
|
|
1389
|
-
tags: ['console'],
|
|
1391
|
+
routes.get('/handlers', describeConsoleRoute({
|
|
1390
1392
|
summary: 'List registered handlers',
|
|
1391
1393
|
responses: {
|
|
1392
1394
|
200: {
|
|
@@ -1413,8 +1415,7 @@ export function createConsoleRoutes(options) {
|
|
|
1413
1415
|
// -------------------------------------------------------------------------
|
|
1414
1416
|
// GET /operations - Operation audit log
|
|
1415
1417
|
// -------------------------------------------------------------------------
|
|
1416
|
-
routes.get('/operations',
|
|
1417
|
-
tags: ['console'],
|
|
1418
|
+
routes.get('/operations', describeConsoleRoute({
|
|
1418
1419
|
summary: 'List operation audit events',
|
|
1419
1420
|
responses: {
|
|
1420
1421
|
200: {
|
|
@@ -1470,8 +1471,7 @@ export function createConsoleRoutes(options) {
|
|
|
1470
1471
|
// -------------------------------------------------------------------------
|
|
1471
1472
|
// POST /prune/preview
|
|
1472
1473
|
// -------------------------------------------------------------------------
|
|
1473
|
-
routes.post('/prune/preview',
|
|
1474
|
-
tags: ['console'],
|
|
1474
|
+
routes.post('/prune/preview', describeConsoleRoute({
|
|
1475
1475
|
summary: 'Preview pruning',
|
|
1476
1476
|
responses: {
|
|
1477
1477
|
200: {
|
|
@@ -1505,8 +1505,7 @@ export function createConsoleRoutes(options) {
|
|
|
1505
1505
|
// -------------------------------------------------------------------------
|
|
1506
1506
|
// POST /prune
|
|
1507
1507
|
// -------------------------------------------------------------------------
|
|
1508
|
-
routes.post('/prune',
|
|
1509
|
-
tags: ['console'],
|
|
1508
|
+
routes.post('/prune', describeConsoleRoute({
|
|
1510
1509
|
summary: 'Trigger pruning',
|
|
1511
1510
|
responses: {
|
|
1512
1511
|
200: {
|
|
@@ -1549,8 +1548,7 @@ export function createConsoleRoutes(options) {
|
|
|
1549
1548
|
// -------------------------------------------------------------------------
|
|
1550
1549
|
// POST /compact
|
|
1551
1550
|
// -------------------------------------------------------------------------
|
|
1552
|
-
routes.post('/compact',
|
|
1553
|
-
tags: ['console'],
|
|
1551
|
+
routes.post('/compact', describeConsoleRoute({
|
|
1554
1552
|
summary: 'Trigger compaction',
|
|
1555
1553
|
responses: {
|
|
1556
1554
|
200: {
|
|
@@ -1601,8 +1599,7 @@ export function createConsoleRoutes(options) {
|
|
|
1601
1599
|
tables: z.array(z.string()),
|
|
1602
1600
|
deletedChunks: z.number(),
|
|
1603
1601
|
});
|
|
1604
|
-
routes.post('/notify-data-change',
|
|
1605
|
-
tags: ['console'],
|
|
1602
|
+
routes.post('/notify-data-change', describeConsoleRoute({
|
|
1606
1603
|
summary: 'Notify external data change',
|
|
1607
1604
|
description: 'Creates a synthetic commit to force re-bootstrap for affected tables. ' +
|
|
1608
1605
|
'Use after pipeline imports or direct DB writes to notify connected clients.',
|
|
@@ -1662,8 +1659,7 @@ export function createConsoleRoutes(options) {
|
|
|
1662
1659
|
// -------------------------------------------------------------------------
|
|
1663
1660
|
// DELETE /clients/:id
|
|
1664
1661
|
// -------------------------------------------------------------------------
|
|
1665
|
-
routes.delete('/clients/:id',
|
|
1666
|
-
tags: ['console'],
|
|
1662
|
+
routes.delete('/clients/:id', describeConsoleRoute({
|
|
1667
1663
|
summary: 'Evict client',
|
|
1668
1664
|
responses: {
|
|
1669
1665
|
200: {
|
|
@@ -1716,8 +1712,7 @@ export function createConsoleRoutes(options) {
|
|
|
1716
1712
|
// -------------------------------------------------------------------------
|
|
1717
1713
|
// GET /events - Paginated request events list
|
|
1718
1714
|
// -------------------------------------------------------------------------
|
|
1719
|
-
routes.get('/events',
|
|
1720
|
-
tags: ['console'],
|
|
1715
|
+
routes.get('/events', describeConsoleRoute({
|
|
1721
1716
|
summary: 'List request events',
|
|
1722
1717
|
responses: {
|
|
1723
1718
|
200: {
|
|
@@ -1800,6 +1795,9 @@ export function createConsoleRoutes(options) {
|
|
|
1800
1795
|
const emitter = options.eventEmitter;
|
|
1801
1796
|
const upgradeWebSocket = options.websocket.upgradeWebSocket;
|
|
1802
1797
|
const heartbeatIntervalMs = options.websocket.heartbeatIntervalMs ?? 30000;
|
|
1798
|
+
const maxMessageBytes = options.websocket.maxMessageBytes ?? 1024 * 1024;
|
|
1799
|
+
const maxMessagesPerWindow = options.websocket.maxMessagesPerWindow ?? 120;
|
|
1800
|
+
const messageRateWindowMs = options.websocket.messageRateWindowMs ?? 10000;
|
|
1803
1801
|
const wsState = new WeakMap();
|
|
1804
1802
|
const cleanup = (ws) => {
|
|
1805
1803
|
const state = wsState.get(ws);
|
|
@@ -1816,7 +1814,7 @@ export function createConsoleRoutes(options) {
|
|
|
1816
1814
|
}
|
|
1817
1815
|
wsState.delete(ws);
|
|
1818
1816
|
};
|
|
1819
|
-
|
|
1817
|
+
const liveEventsWebSocketRoute = upgradeWebSocket(async (c) => {
|
|
1820
1818
|
const authHeader = c.req.header('Authorization');
|
|
1821
1819
|
const partitionId = c.req.query('partitionId')?.trim() || undefined;
|
|
1822
1820
|
const replaySince = c.req.query('since');
|
|
@@ -1854,6 +1852,8 @@ export function createConsoleRoutes(options) {
|
|
|
1854
1852
|
authTimeout: null,
|
|
1855
1853
|
isAuthenticated: false,
|
|
1856
1854
|
startAuthenticatedSession: null,
|
|
1855
|
+
messageRateWindowStart: Date.now(),
|
|
1856
|
+
messageRateWindowCount: 0,
|
|
1857
1857
|
};
|
|
1858
1858
|
wsState.set(ws, state);
|
|
1859
1859
|
const startAuthenticatedSession = () => {
|
|
@@ -1928,7 +1928,29 @@ export function createConsoleRoutes(options) {
|
|
|
1928
1928
|
},
|
|
1929
1929
|
async onMessage(event, ws) {
|
|
1930
1930
|
const state = wsState.get(ws);
|
|
1931
|
-
if (!state
|
|
1931
|
+
if (!state) {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const messageBytes = measureWebSocketMessageBytes(event.data);
|
|
1935
|
+
if (messageBytes > maxMessageBytes) {
|
|
1936
|
+
ws.close(1009, 'message too large');
|
|
1937
|
+
cleanup(ws);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (maxMessagesPerWindow > 0 && messageRateWindowMs > 0) {
|
|
1941
|
+
const nowMs = Date.now();
|
|
1942
|
+
if (nowMs - state.messageRateWindowStart >= messageRateWindowMs) {
|
|
1943
|
+
state.messageRateWindowStart = nowMs;
|
|
1944
|
+
state.messageRateWindowCount = 0;
|
|
1945
|
+
}
|
|
1946
|
+
state.messageRateWindowCount += 1;
|
|
1947
|
+
if (state.messageRateWindowCount > maxMessagesPerWindow) {
|
|
1948
|
+
ws.close(1008, 'message rate exceeded');
|
|
1949
|
+
cleanup(ws);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
if (state.isAuthenticated) {
|
|
1932
1954
|
return;
|
|
1933
1955
|
}
|
|
1934
1956
|
if (typeof event.data !== 'string') {
|
|
@@ -1961,13 +1983,18 @@ export function createConsoleRoutes(options) {
|
|
|
1961
1983
|
cleanup(ws);
|
|
1962
1984
|
},
|
|
1963
1985
|
};
|
|
1964
|
-
})
|
|
1986
|
+
});
|
|
1987
|
+
routes.get('/events/live', async (c, next) => {
|
|
1988
|
+
if (!isWebSocketOriginAllowed(c, options.websocket?.allowedOrigins)) {
|
|
1989
|
+
return c.json({ error: 'FORBIDDEN_ORIGIN' }, 403);
|
|
1990
|
+
}
|
|
1991
|
+
return liveEventsWebSocketRoute(c, next);
|
|
1992
|
+
});
|
|
1965
1993
|
}
|
|
1966
1994
|
// -------------------------------------------------------------------------
|
|
1967
1995
|
// GET /events/:id - Single event detail
|
|
1968
1996
|
// -------------------------------------------------------------------------
|
|
1969
|
-
routes.get('/events/:id',
|
|
1970
|
-
tags: ['console'],
|
|
1997
|
+
routes.get('/events/:id', describeConsoleRoute({
|
|
1971
1998
|
summary: 'Get event details',
|
|
1972
1999
|
responses: {
|
|
1973
2000
|
200: {
|
|
@@ -2016,8 +2043,7 @@ export function createConsoleRoutes(options) {
|
|
|
2016
2043
|
// -------------------------------------------------------------------------
|
|
2017
2044
|
// GET /events/:id/payload - payload snapshot detail (if retained)
|
|
2018
2045
|
// -------------------------------------------------------------------------
|
|
2019
|
-
routes.get('/events/:id/payload',
|
|
2020
|
-
tags: ['console'],
|
|
2046
|
+
routes.get('/events/:id/payload', describeConsoleRoute({
|
|
2021
2047
|
summary: 'Get event payload snapshot',
|
|
2022
2048
|
responses: {
|
|
2023
2049
|
200: {
|
|
@@ -2092,8 +2118,7 @@ export function createConsoleRoutes(options) {
|
|
|
2092
2118
|
// -------------------------------------------------------------------------
|
|
2093
2119
|
// DELETE /events - Clear all events
|
|
2094
2120
|
// -------------------------------------------------------------------------
|
|
2095
|
-
routes.delete('/events',
|
|
2096
|
-
tags: ['console'],
|
|
2121
|
+
routes.delete('/events', describeConsoleRoute({
|
|
2097
2122
|
summary: 'Clear all events',
|
|
2098
2123
|
responses: {
|
|
2099
2124
|
200: {
|
|
@@ -2127,8 +2152,7 @@ export function createConsoleRoutes(options) {
|
|
|
2127
2152
|
// -------------------------------------------------------------------------
|
|
2128
2153
|
// POST /events/prune - Prune old events
|
|
2129
2154
|
// -------------------------------------------------------------------------
|
|
2130
|
-
routes.post('/events/prune',
|
|
2131
|
-
tags: ['console'],
|
|
2155
|
+
routes.post('/events/prune', describeConsoleRoute({
|
|
2132
2156
|
summary: 'Prune old events',
|
|
2133
2157
|
responses: {
|
|
2134
2158
|
200: {
|
|
@@ -2163,8 +2187,7 @@ export function createConsoleRoutes(options) {
|
|
|
2163
2187
|
// -------------------------------------------------------------------------
|
|
2164
2188
|
// GET /api-keys - List all API keys
|
|
2165
2189
|
// -------------------------------------------------------------------------
|
|
2166
|
-
routes.get('/api-keys',
|
|
2167
|
-
tags: ['console'],
|
|
2190
|
+
routes.get('/api-keys', describeConsoleRoute({
|
|
2168
2191
|
summary: 'List API keys',
|
|
2169
2192
|
responses: {
|
|
2170
2193
|
200: {
|
|
@@ -2263,8 +2286,7 @@ export function createConsoleRoutes(options) {
|
|
|
2263
2286
|
// -------------------------------------------------------------------------
|
|
2264
2287
|
// POST /api-keys - Create new API key
|
|
2265
2288
|
// -------------------------------------------------------------------------
|
|
2266
|
-
routes.post('/api-keys',
|
|
2267
|
-
tags: ['console'],
|
|
2289
|
+
routes.post('/api-keys', describeConsoleRoute({
|
|
2268
2290
|
summary: 'Create API key',
|
|
2269
2291
|
responses: {
|
|
2270
2292
|
201: {
|
|
@@ -2346,8 +2368,7 @@ export function createConsoleRoutes(options) {
|
|
|
2346
2368
|
// -------------------------------------------------------------------------
|
|
2347
2369
|
// GET /api-keys/:id - Get single API key
|
|
2348
2370
|
// -------------------------------------------------------------------------
|
|
2349
|
-
routes.get('/api-keys/:id',
|
|
2350
|
-
tags: ['console'],
|
|
2371
|
+
routes.get('/api-keys/:id', describeConsoleRoute({
|
|
2351
2372
|
summary: 'Get API key',
|
|
2352
2373
|
responses: {
|
|
2353
2374
|
200: {
|
|
@@ -2407,8 +2428,7 @@ export function createConsoleRoutes(options) {
|
|
|
2407
2428
|
// -------------------------------------------------------------------------
|
|
2408
2429
|
// DELETE /api-keys/:id - Revoke API key (soft delete)
|
|
2409
2430
|
// -------------------------------------------------------------------------
|
|
2410
|
-
routes.delete('/api-keys/:id',
|
|
2411
|
-
tags: ['console'],
|
|
2431
|
+
routes.delete('/api-keys/:id', describeConsoleRoute({
|
|
2412
2432
|
summary: 'Revoke API key',
|
|
2413
2433
|
responses: {
|
|
2414
2434
|
200: {
|
|
@@ -2447,8 +2467,7 @@ export function createConsoleRoutes(options) {
|
|
|
2447
2467
|
// -------------------------------------------------------------------------
|
|
2448
2468
|
// POST /api-keys/bulk-revoke - Revoke multiple API keys
|
|
2449
2469
|
// -------------------------------------------------------------------------
|
|
2450
|
-
routes.post('/api-keys/bulk-revoke',
|
|
2451
|
-
tags: ['console'],
|
|
2470
|
+
routes.post('/api-keys/bulk-revoke', describeConsoleRoute({
|
|
2452
2471
|
summary: 'Bulk revoke API keys',
|
|
2453
2472
|
responses: {
|
|
2454
2473
|
200: {
|
|
@@ -2534,8 +2553,7 @@ export function createConsoleRoutes(options) {
|
|
|
2534
2553
|
// -------------------------------------------------------------------------
|
|
2535
2554
|
// POST /api-keys/:id/rotate/stage - Stage rotate API key (keep old active)
|
|
2536
2555
|
// -------------------------------------------------------------------------
|
|
2537
|
-
routes.post('/api-keys/:id/rotate/stage',
|
|
2538
|
-
tags: ['console'],
|
|
2556
|
+
routes.post('/api-keys/:id/rotate/stage', describeConsoleRoute({
|
|
2539
2557
|
summary: 'Stage rotate API key',
|
|
2540
2558
|
responses: {
|
|
2541
2559
|
200: {
|
|
@@ -2627,8 +2645,7 @@ export function createConsoleRoutes(options) {
|
|
|
2627
2645
|
// -------------------------------------------------------------------------
|
|
2628
2646
|
// POST /api-keys/:id/rotate - Rotate API key
|
|
2629
2647
|
// -------------------------------------------------------------------------
|
|
2630
|
-
routes.post('/api-keys/:id/rotate',
|
|
2631
|
-
tags: ['console'],
|
|
2648
|
+
routes.post('/api-keys/:id/rotate', describeConsoleRoute({
|
|
2632
2649
|
summary: 'Rotate API key',
|
|
2633
2650
|
responses: {
|
|
2634
2651
|
200: {
|
|
@@ -2729,8 +2746,7 @@ export function createConsoleRoutes(options) {
|
|
|
2729
2746
|
// Storage endpoints
|
|
2730
2747
|
// -----------------------------------------------------------------------
|
|
2731
2748
|
const bucket = options.blobBucket;
|
|
2732
|
-
routes.get('/storage',
|
|
2733
|
-
tags: ['console'],
|
|
2749
|
+
routes.get('/storage', describeConsoleRoute({
|
|
2734
2750
|
summary: 'List storage items',
|
|
2735
2751
|
responses: {
|
|
2736
2752
|
200: {
|
|
@@ -2771,8 +2787,7 @@ export function createConsoleRoutes(options) {
|
|
|
2771
2787
|
cursor: listed.cursor ?? null,
|
|
2772
2788
|
}, 200);
|
|
2773
2789
|
});
|
|
2774
|
-
routes.get('/storage/:key{.+}/download',
|
|
2775
|
-
tags: ['console'],
|
|
2790
|
+
routes.get('/storage/:key{.+}/download', describeConsoleRoute({
|
|
2776
2791
|
summary: 'Download a storage item',
|
|
2777
2792
|
responses: {
|
|
2778
2793
|
200: { description: 'Storage item contents' },
|
|
@@ -2808,8 +2823,7 @@ export function createConsoleRoutes(options) {
|
|
|
2808
2823
|
headers,
|
|
2809
2824
|
});
|
|
2810
2825
|
});
|
|
2811
|
-
routes.delete('/storage/:key{.+}',
|
|
2812
|
-
tags: ['console'],
|
|
2826
|
+
routes.delete('/storage/:key{.+}', describeConsoleRoute({
|
|
2813
2827
|
summary: 'Delete a storage item',
|
|
2814
2828
|
responses: {
|
|
2815
2829
|
200: {
|
|
@@ -2837,6 +2851,37 @@ export function createConsoleRoutes(options) {
|
|
|
2837
2851
|
});
|
|
2838
2852
|
return routes;
|
|
2839
2853
|
}
|
|
2854
|
+
function isWebSocketOriginAllowed(c, allowedOrigins) {
|
|
2855
|
+
if (!allowedOrigins)
|
|
2856
|
+
return true;
|
|
2857
|
+
if (allowedOrigins === '*')
|
|
2858
|
+
return true;
|
|
2859
|
+
const origin = c.req.header('origin');
|
|
2860
|
+
if (!origin)
|
|
2861
|
+
return false;
|
|
2862
|
+
try {
|
|
2863
|
+
const normalizedOrigin = new URL(origin).origin;
|
|
2864
|
+
return allowedOrigins.includes(normalizedOrigin);
|
|
2865
|
+
}
|
|
2866
|
+
catch {
|
|
2867
|
+
return false;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
function measureWebSocketMessageBytes(data) {
|
|
2871
|
+
if (typeof data === 'string') {
|
|
2872
|
+
return new TextEncoder().encode(data).byteLength;
|
|
2873
|
+
}
|
|
2874
|
+
if (data instanceof ArrayBuffer) {
|
|
2875
|
+
return data.byteLength;
|
|
2876
|
+
}
|
|
2877
|
+
if (ArrayBuffer.isView(data)) {
|
|
2878
|
+
return data.byteLength;
|
|
2879
|
+
}
|
|
2880
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
2881
|
+
return data.size;
|
|
2882
|
+
}
|
|
2883
|
+
return new TextEncoder().encode(String(data)).byteLength;
|
|
2884
|
+
}
|
|
2840
2885
|
// ===========================================================================
|
|
2841
2886
|
// API Key Utilities
|
|
2842
2887
|
// ===========================================================================
|