@syncular/server 0.0.6-95 → 0.1.0

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