@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.
Files changed (50) hide show
  1. package/dist/blobs.d.ts +10 -4
  2. package/dist/blobs.d.ts.map +1 -1
  3. package/dist/blobs.js +260 -26
  4. package/dist/blobs.js.map +1 -1
  5. package/dist/console/gateway.d.ts +4 -0
  6. package/dist/console/gateway.d.ts.map +1 -1
  7. package/dist/console/gateway.js +97 -60
  8. package/dist/console/gateway.js.map +1 -1
  9. package/dist/console/route-descriptor.d.ts +6 -0
  10. package/dist/console/route-descriptor.d.ts.map +1 -0
  11. package/dist/console/route-descriptor.js +16 -0
  12. package/dist/console/route-descriptor.js.map +1 -0
  13. package/dist/console/routes.d.ts.map +1 -1
  14. package/dist/console/routes.js +153 -108
  15. package/dist/console/routes.js.map +1 -1
  16. package/dist/console/schema-errors.d.ts +2 -0
  17. package/dist/console/schema-errors.d.ts.map +1 -0
  18. package/dist/console/schema-errors.js +17 -0
  19. package/dist/console/schema-errors.js.map +1 -0
  20. package/dist/console/schemas.js +1 -1
  21. package/dist/console/schemas.js.map +1 -1
  22. package/dist/console/types.d.ts +32 -0
  23. package/dist/console/types.d.ts.map +1 -1
  24. package/dist/create-server.d.ts.map +1 -1
  25. package/dist/create-server.js +13 -10
  26. package/dist/create-server.js.map +1 -1
  27. package/dist/proxy/routes.d.ts +10 -0
  28. package/dist/proxy/routes.d.ts.map +1 -1
  29. package/dist/proxy/routes.js +57 -6
  30. package/dist/proxy/routes.js.map +1 -1
  31. package/dist/routes.d.ts +21 -0
  32. package/dist/routes.d.ts.map +1 -1
  33. package/dist/routes.js +338 -352
  34. package/dist/routes.js.map +1 -1
  35. package/package.json +7 -6
  36. package/src/__tests__/blob-routes.test.ts +286 -18
  37. package/src/__tests__/console-gateway-live-routes.test.ts +61 -1
  38. package/src/__tests__/console-routes.test.ts +30 -1
  39. package/src/__tests__/create-server.test.ts +237 -1
  40. package/src/__tests__/pull-chunk-storage.test.ts +98 -0
  41. package/src/blobs.ts +360 -34
  42. package/src/console/gateway.ts +335 -288
  43. package/src/console/route-descriptor.ts +22 -0
  44. package/src/console/routes.ts +327 -248
  45. package/src/console/schema-errors.ts +23 -0
  46. package/src/console/schemas.ts +1 -1
  47. package/src/console/types.ts +32 -0
  48. package/src/create-server.ts +13 -10
  49. package/src/proxy/routes.ts +73 -9
  50. package/src/routes.ts +449 -396
@@ -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 { describeRoute, resolver, validator as zValidator } from 'hono-openapi';
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 parseJsonValue(value) {
109
- if (value === null || value === undefined)
110
- return null;
111
- if (typeof value !== 'string')
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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
- const commitRows = await commitsQuery.execute();
966
- for (const row of commitRows) {
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
- const eventRows = await eventsQuery.execute();
1016
- for (const row of eventRows) {
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', describeRoute({
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', describeRoute({
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: typeof row.row_json === 'string'
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: typeof row.scopes === 'string'
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: Array.isArray(commitRow.affected_tables)
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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
- routes.get('/events/live', upgradeWebSocket(async (c) => {
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 || state.isAuthenticated) {
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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', describeRoute({
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{.+}', describeRoute({
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
  // ===========================================================================