@edge-base/server 0.1.1

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 (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Room WebSocket route — /api/room
3
+ *
4
+ * Handles WebSocket upgrade requests and routes to Room DO.
5
+ * v2: namespace + roomId identification.
6
+ *
7
+ * URL: /api/room?namespace={ns}&id={roomId}
8
+ * DO name: namespace::roomId
9
+ *
10
+ * DDoS defense (same pattern as Database Live,):
11
+ * - IP-based pending connection limit (5 max)
12
+ * - KV counter with expirationTtl: 60s
13
+ */
14
+ import { OpenAPIHono, createRoute, z, type HonoEnv } from '../lib/hono.js';
15
+ import type { RoomNamespaceConfig } from '@edge-base/shared';
16
+ import type { Context } from 'hono';
17
+ import { parseConfig } from '../lib/do-router.js';
18
+ import { resolveRoomRuntime } from '../lib/room-runtime.js';
19
+ import { zodDefaultHook, jsonResponseSchema, errorResponseSchema } from '../lib/schemas.js';
20
+ import {
21
+ acquirePendingWebSocketSlot,
22
+ getPendingWebSocketCount,
23
+ releasePendingWebSocketSlot,
24
+ } from '../lib/websocket-pending.js';
25
+ import { getTrustedClientIp } from '../lib/client-ip.js';
26
+
27
+
28
+ export const roomRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
29
+
30
+ const MAX_PENDING_PER_IP = 5;
31
+ const PENDING_TTL_SECONDS = 60;
32
+ const roomFallbackWarnings = new Set<string>();
33
+ const roomQuerySchema = z.object({
34
+ namespace: z.string().openapi({ description: 'Room namespace' }),
35
+ id: z.string().openapi({ description: 'Room ID' }),
36
+ });
37
+ const roomConnectDiagnosticSchema = z.object({
38
+ ok: z.boolean(),
39
+ type: z.string(),
40
+ category: z.string(),
41
+ message: z.string(),
42
+ namespace: z.string().optional(),
43
+ roomId: z.string().optional(),
44
+ runtime: z.string().optional(),
45
+ pendingCount: z.number().optional(),
46
+ maxPending: z.number().optional(),
47
+ });
48
+ const roomRealtimeSessionDescriptionSchema = z.object({
49
+ sdp: z.string().openapi({ description: 'WebRTC session description payload' }),
50
+ type: z.enum(['offer', 'answer']).openapi({ description: 'Session description type' }),
51
+ });
52
+ const roomRealtimeTrackSchema = z.object({
53
+ location: z.enum(['local', 'remote']).openapi({ description: 'Track direction relative to the caller' }),
54
+ mid: z.string().optional().openapi({ description: 'WebRTC media ID' }),
55
+ sessionId: z.string().optional().openapi({ description: 'Provider session ID associated with this track' }),
56
+ trackName: z.string().optional().openapi({ description: 'Track name used by the provider' }),
57
+ bidirectionalMediaStream: z.boolean().optional().openapi({ description: 'Whether the track should be bidirectional' }),
58
+ kind: z.string().optional().openapi({ description: 'Track kind reported by the provider' }),
59
+ simulcast: z.object({
60
+ preferredRid: z.string().optional(),
61
+ priorityOrdering: z.enum(['none', 'asciibetical']).optional(),
62
+ ridNotAvailable: z.enum(['none', 'asciibetical']).optional(),
63
+ }).optional().openapi({ description: 'Optional simulcast preferences' }),
64
+ errorCode: z.string().optional().openapi({ description: 'Provider-level error code for this track' }),
65
+ errorDescription: z.string().optional().openapi({ description: 'Provider-level error description for this track' }),
66
+ });
67
+ const roomRealtimeCreateSessionBodySchema = z.object({
68
+ connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the realtime session to' }),
69
+ correlationId: z.string().optional().openapi({ description: 'Optional provider correlation ID' }),
70
+ thirdparty: z.boolean().optional().openapi({ description: 'Forward Cloudflare Realtime thirdparty mode' }),
71
+ sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
72
+ });
73
+ const roomRealtimeCreateSessionResponseSchema = z.object({
74
+ sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
75
+ sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
76
+ errorCode: z.string().optional(),
77
+ errorDescription: z.string().optional(),
78
+ connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
79
+ reused: z.boolean().optional().openapi({ description: 'Whether an existing provider session was reused' }),
80
+ });
81
+ const roomRealtimeSessionStateSchema = z.object({
82
+ sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
83
+ connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
84
+ createdAt: z.number().openapi({ description: 'Unix epoch milliseconds when the session was created' }),
85
+ updatedAt: z.number().openapi({ description: 'Unix epoch milliseconds when the session was last updated' }),
86
+ });
87
+ const roomRealtimeIceServerSchema = z.object({
88
+ urls: z.union([z.array(z.string()), z.string()]).openapi({ description: 'ICE server URL or URL list' }),
89
+ username: z.string().optional(),
90
+ credential: z.string().optional(),
91
+ });
92
+ const roomRealtimeIceServersBodySchema = z.object({
93
+ ttl: z.number().optional().openapi({ description: 'Requested TURN credential TTL in seconds' }),
94
+ });
95
+ const roomRealtimeIceServersResponseSchema = z.object({
96
+ iceServers: z.array(roomRealtimeIceServerSchema).openapi({ description: 'ICE servers returned by Cloudflare TURN' }),
97
+ });
98
+ const roomRealtimeTracksResponseSchema = z.object({
99
+ errorCode: z.string().optional(),
100
+ errorDescription: z.string().optional(),
101
+ requiresImmediateRenegotiation: z.boolean().optional(),
102
+ sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
103
+ tracks: z.array(roomRealtimeTrackSchema).optional(),
104
+ });
105
+ const roomRealtimeTracksBodySchema = z.object({
106
+ sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
107
+ connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the track operation to' }),
108
+ sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
109
+ tracks: z.array(roomRealtimeTrackSchema).min(1).openapi({ description: 'Tracks to create or subscribe to' }),
110
+ autoDiscover: z.boolean().optional().openapi({ description: 'Ask the provider to auto-discover remote tracks' }),
111
+ publish: z.object({
112
+ kind: z.enum(['audio', 'video', 'screen']).optional(),
113
+ trackId: z.string().optional(),
114
+ deviceId: z.string().optional(),
115
+ muted: z.boolean().optional(),
116
+ }).optional().openapi({ description: 'Optional room media state updates to apply after track creation' }),
117
+ });
118
+ const roomRealtimeRenegotiateBodySchema = z.object({
119
+ sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
120
+ connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the renegotiation to' }),
121
+ sessionDescription: roomRealtimeSessionDescriptionSchema,
122
+ });
123
+ const roomRealtimeCloseTracksBodySchema = z.object({
124
+ sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
125
+ connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the close operation to' }),
126
+ sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
127
+ tracks: z.array(z.object({
128
+ mid: z.string().openapi({ description: 'Track MID to close' }),
129
+ })).min(1).openapi({ description: 'Tracks to close' }),
130
+ force: z.boolean().optional().openapi({ description: 'Force close even if the provider reports the track as active' }),
131
+ unpublish: z.object({
132
+ kind: z.enum(['audio', 'video', 'screen']).optional(),
133
+ }).optional().openapi({ description: 'Optional room media state cleanup after closing tracks' }),
134
+ });
135
+
136
+ function isRoomOperationPublic(
137
+ namespaceConfig: RoomNamespaceConfig | null | undefined,
138
+ operation: 'metadata' | 'join' | 'action',
139
+ ): boolean {
140
+ if (!namespaceConfig?.public) return false;
141
+ if (namespaceConfig.public === true) return true;
142
+ return !!namespaceConfig.public[operation];
143
+ }
144
+
145
+ function getRoomAuthContext(
146
+ auth: {
147
+ id?: string;
148
+ role?: string;
149
+ email?: string | null;
150
+ custom?: Record<string, unknown> | null;
151
+ isAnonymous?: boolean;
152
+ meta?: Record<string, unknown>;
153
+ } | null | undefined,
154
+ ) {
155
+ if (!auth?.id) return null;
156
+ return {
157
+ id: auth.id,
158
+ role: auth.role,
159
+ email: auth.email ?? undefined,
160
+ custom: auth.custom ?? undefined,
161
+ isAnonymous: auth.isAnonymous,
162
+ meta: auth.meta,
163
+ };
164
+ }
165
+
166
+ export async function proxyRoomDoRequest(
167
+ c: Context<HonoEnv>,
168
+ path: string,
169
+ method: string,
170
+ options?: { requireAuth?: boolean; validatedJson?: unknown },
171
+ ): Promise<Response> {
172
+ const namespace = c.req.query('namespace');
173
+ const roomId = c.req.query('id');
174
+
175
+ if (!namespace || !roomId) {
176
+ return c.json({ code: 400, message: 'namespace and id query parameters required' }, 400);
177
+ }
178
+
179
+ if (options?.requireAuth && !c.get('auth')) {
180
+ return c.json({ code: 401, message: 'Authentication required' }, 401);
181
+ }
182
+
183
+ const config = parseConfig(c.env);
184
+ if (config.release && !config.rooms?.[namespace]) {
185
+ return c.json({ code: 403, message: `Room namespace '${namespace}' not configured` }, 403);
186
+ }
187
+
188
+ const runtime = resolveRoomRuntime(c.env);
189
+ if (!runtime.binding) {
190
+ return c.json({ code: 404, message: `Room runtime '${runtime.target}' not configured` }, 404);
191
+ }
192
+
193
+ const doName = `${namespace}::${roomId}`;
194
+ const doId = runtime.binding.idFromName(doName);
195
+ const doStub = runtime.binding.get(doId);
196
+
197
+ const url = new URL(c.req.url);
198
+ url.pathname = path;
199
+ url.searchParams.set('room', doName);
200
+ let body: ArrayBuffer | undefined;
201
+ if (method !== 'GET' && method !== 'HEAD') {
202
+ if (!c.req.raw.bodyUsed) {
203
+ body = await c.req.raw.clone().arrayBuffer().catch(() => undefined);
204
+ }
205
+
206
+ if ((!body || body.byteLength === 0) && options?.validatedJson !== undefined) {
207
+ body = new TextEncoder().encode(JSON.stringify(options.validatedJson)).buffer;
208
+ }
209
+ }
210
+
211
+ const doRequest = new Request(url.toString(), {
212
+ method,
213
+ headers: c.req.raw.headers,
214
+ body: body && body.byteLength > 0 ? body : undefined,
215
+ });
216
+
217
+ return doStub.fetch(doRequest);
218
+ }
219
+
220
+ const connectRoom = createRoute({
221
+ operationId: 'connectRoom',
222
+ method: 'get',
223
+ path: '/',
224
+ tags: ['client'],
225
+ summary: 'Connect to room WebSocket',
226
+ description: 'WebSocket upgrade endpoint for Room connections. Requires Upgrade: websocket header.',
227
+ request: {
228
+ query: roomQuerySchema,
229
+ },
230
+ responses: {
231
+ 101: { description: 'WebSocket upgrade successful' },
232
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
233
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
234
+ 429: { description: 'Rate limited', content: { 'application/json': { schema: errorResponseSchema } } },
235
+ },
236
+ });
237
+
238
+ const roomConnectCheck = createRoute({
239
+ operationId: 'checkRoomConnection',
240
+ method: 'get',
241
+ path: '/connect-check',
242
+ tags: ['client'],
243
+ summary: 'Check room WebSocket connection prerequisites',
244
+ request: {
245
+ query: roomQuerySchema,
246
+ },
247
+ responses: {
248
+ 200: { description: 'Room connection looks ready', content: { 'application/json': { schema: roomConnectDiagnosticSchema } } },
249
+ 400: { description: 'Bad request', content: { 'application/json': { schema: roomConnectDiagnosticSchema } } },
250
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: roomConnectDiagnosticSchema } } },
251
+ 404: { description: 'Room feature not configured', content: { 'application/json': { schema: roomConnectDiagnosticSchema } } },
252
+ 429: { description: 'Rate limited', content: { 'application/json': { schema: roomConnectDiagnosticSchema } } },
253
+ },
254
+ });
255
+
256
+ roomRoute.openapi(roomConnectCheck, async (c) => {
257
+ const namespace = c.req.query('namespace');
258
+ const roomId = c.req.query('id');
259
+ if (!namespace || !roomId) {
260
+ return c.json({
261
+ ok: false,
262
+ type: 'room_connect_invalid_request',
263
+ category: 'request',
264
+ message: 'namespace and id query parameters required',
265
+ }, 400);
266
+ }
267
+
268
+ const config = parseConfig(c.env);
269
+ if (config.release && !config.rooms?.[namespace]) {
270
+ return c.json({
271
+ ok: false,
272
+ type: 'room_namespace_not_configured',
273
+ category: 'config',
274
+ message: `Room namespace '${namespace}' not configured`,
275
+ namespace,
276
+ roomId,
277
+ }, 403);
278
+ }
279
+
280
+ const runtime = resolveRoomRuntime(c.env);
281
+ if (!runtime.binding) {
282
+ return c.json({
283
+ ok: false,
284
+ type: 'room_runtime_unconfigured',
285
+ category: 'config',
286
+ message: `Room runtime '${runtime.target}' not configured`,
287
+ namespace,
288
+ roomId,
289
+ runtime: runtime.target,
290
+ }, 404);
291
+ }
292
+
293
+ const ip = getTrustedClientIp(c.env, c.req) ?? 'unknown';
294
+ const kvKey = `ws:room:pending:${ip}`;
295
+ const pendingCount = await getPendingWebSocketCount(c.env.KV, kvKey).catch(() => 0);
296
+ if (pendingCount >= MAX_PENDING_PER_IP) {
297
+ return c.json({
298
+ ok: false,
299
+ type: 'room_connect_rate_limited',
300
+ category: 'rate_limit',
301
+ message: 'Too many pending Room connections',
302
+ namespace,
303
+ roomId,
304
+ runtime: runtime.target,
305
+ pendingCount,
306
+ maxPending: MAX_PENDING_PER_IP,
307
+ }, 429);
308
+ }
309
+
310
+ return c.json({
311
+ ok: true,
312
+ type: 'room_connect_ready',
313
+ category: 'ready',
314
+ message: 'Room WebSocket preflight passed',
315
+ namespace,
316
+ roomId,
317
+ runtime: runtime.target,
318
+ pendingCount,
319
+ maxPending: MAX_PENDING_PER_IP,
320
+ }, 200);
321
+ });
322
+
323
+ roomRoute.openapi(connectRoom, async (c) => {
324
+ const upgradeHeader = c.req.header('Upgrade');
325
+ if (upgradeHeader !== 'websocket') {
326
+ return c.json({ code: 400, message: 'Expected WebSocket upgrade' }, 400);
327
+ }
328
+
329
+ const namespace = c.req.query('namespace');
330
+ const roomId = c.req.query('id');
331
+
332
+ if (!namespace || !roomId) {
333
+ return c.json({ code: 400, message: 'namespace and id query parameters required' }, 400);
334
+ }
335
+
336
+ // ─── Validate namespace against config ───
337
+ const config = parseConfig(c.env);
338
+ if (config.release) {
339
+ if (!config.rooms?.[namespace]) {
340
+ return c.json({
341
+ code: 403,
342
+ message: `Room namespace '${namespace}' not configured`,
343
+ }, 403);
344
+ }
345
+ }
346
+
347
+ const runtime = resolveRoomRuntime(c.env);
348
+ if (!runtime.binding) {
349
+ return c.json({ code: 404, message: `Room runtime '${runtime.target}' not configured` }, 404);
350
+ }
351
+
352
+ // ─── DDoS Defense: IP-based pending connection limit ───
353
+ const ip = getTrustedClientIp(c.env, c.req) ?? 'unknown';
354
+ const kvKey = `ws:room:pending:${ip}`;
355
+ let pendingTracked = false;
356
+
357
+ try {
358
+ pendingTracked = await acquirePendingWebSocketSlot(
359
+ c.env.KV,
360
+ kvKey,
361
+ MAX_PENDING_PER_IP,
362
+ PENDING_TTL_SECONDS,
363
+ );
364
+ if (!pendingTracked) {
365
+ return c.json({
366
+ code: 429,
367
+ message: 'Too many pending Room connections',
368
+ }, 429);
369
+ }
370
+ } catch {
371
+ // KV failure shouldn't block legitimate connections
372
+ }
373
+
374
+ // ─── Route to Room DO ───
375
+ const doName = `${namespace}::${roomId}`;
376
+ const doId = runtime.binding.idFromName(doName);
377
+ const doStub = runtime.binding.get(doId);
378
+
379
+ const url = new URL(c.req.url);
380
+ url.pathname = '/websocket';
381
+ url.searchParams.set('room', doName);
382
+ const doRequest = new Request(url.toString(), {
383
+ headers: c.req.raw.headers,
384
+ });
385
+
386
+ try {
387
+ return await doStub.fetch(doRequest);
388
+ } finally {
389
+ if (pendingTracked) {
390
+ try {
391
+ await releasePendingWebSocketSlot(c.env.KV, kvKey, PENDING_TTL_SECONDS);
392
+ } catch {
393
+ // KV failure shouldn't break an already accepted connection.
394
+ }
395
+ }
396
+ }
397
+ });
398
+
399
+ /**
400
+ * GET /room/metadata?namespace={ns}&id={roomId}
401
+ * HTTP endpoint to query room metadata without joining.
402
+ * In release mode, metadata requires either an explicit access rule or public.metadata opt-in.
403
+ */
404
+ const getRoomMetadata = createRoute({
405
+ operationId: 'getRoomMetadata',
406
+ method: 'get',
407
+ path: '/metadata',
408
+ tags: ['client'],
409
+ summary: 'Get room metadata',
410
+ request: {
411
+ query: roomQuerySchema,
412
+ },
413
+ responses: {
414
+ 200: { description: 'Room metadata', content: { 'application/json': { schema: jsonResponseSchema } } },
415
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
416
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
417
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
418
+ },
419
+ });
420
+
421
+ roomRoute.openapi(getRoomMetadata, async (c) => {
422
+ const namespace = c.req.query('namespace');
423
+ const roomId = c.req.query('id');
424
+
425
+ if (!namespace || !roomId) {
426
+ return c.json({ code: 400, message: 'namespace and id query parameters required' }, 400);
427
+ }
428
+
429
+ // Validate namespace against config
430
+ const config = parseConfig(c.env);
431
+ const namespaceConfig = config.rooms?.[namespace];
432
+ if (config.release) {
433
+ if (!config.rooms?.[namespace]) {
434
+ return c.json({
435
+ code: 403,
436
+ message: `Room namespace '${namespace}' not configured`,
437
+ }, 403);
438
+ }
439
+ }
440
+
441
+ const metadataAccess = namespaceConfig?.access?.metadata;
442
+ if (!metadataAccess) {
443
+ if (config.release && !isRoomOperationPublic(namespaceConfig, 'metadata')) {
444
+ return c.json({ code: 403, message: 'Room metadata requires access.metadata or public.metadata in release mode' }, 403);
445
+ }
446
+ if (!config.release) {
447
+ const warningKey = `${namespace}:metadata`;
448
+ if (!roomFallbackWarnings.has(warningKey)) {
449
+ roomFallbackWarnings.add(warningKey);
450
+ console.warn(`[Room] ${warningKey} is using development-mode allow-by-default. Add rooms.${namespace}.access.metadata or public.metadata to make this explicit.`);
451
+ }
452
+ }
453
+ }
454
+ if (metadataAccess) {
455
+ const auth = (c.get('auth') as { id?: string; role?: string; email?: string | null; custom?: Record<string, unknown> | null; isAnonymous?: boolean; meta?: Record<string, unknown> } | null | undefined) ?? null;
456
+ const allowed = await Promise.resolve(metadataAccess(
457
+ getRoomAuthContext(auth),
458
+ roomId,
459
+ )).catch(() => false);
460
+ if (!allowed) {
461
+ return c.json({ code: 403, message: 'Denied by room metadata access rule' }, 403);
462
+ }
463
+ }
464
+
465
+ const runtime = resolveRoomRuntime(c.env);
466
+ if (!runtime.binding) {
467
+ return c.json({ code: 404, message: `Room runtime '${runtime.target}' not configured` }, 404);
468
+ }
469
+
470
+ // Route to Room DO
471
+ const doName = `${namespace}::${roomId}`;
472
+ const doId = runtime.binding.idFromName(doName);
473
+ const doStub = runtime.binding.get(doId);
474
+
475
+ const url = new URL(c.req.url);
476
+ url.pathname = '/metadata';
477
+ url.searchParams.set('room', doName);
478
+ const doRequest = new Request(url.toString(), {
479
+ method: 'GET',
480
+ headers: c.req.raw.headers,
481
+ });
482
+
483
+ return doStub.fetch(doRequest);
484
+ });
485
+
486
+ const getRoomRealtimeSession = createRoute({
487
+ operationId: 'getRoomRealtimeSession',
488
+ method: 'get',
489
+ path: '/media/realtime/session',
490
+ tags: ['client'],
491
+ summary: 'Get the active room realtime media session',
492
+ description: 'Returns the provider session currently bound to the authenticated room member.',
493
+ request: {
494
+ query: roomQuerySchema.extend({
495
+ connectionId: z.string().optional().openapi({ description: 'Optional room connection ID override' }),
496
+ }),
497
+ },
498
+ responses: {
499
+ 200: { description: 'Active room realtime session', content: { 'application/json': { schema: roomRealtimeSessionStateSchema } } },
500
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
501
+ 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
502
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
503
+ 404: { description: 'No active session or runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
504
+ },
505
+ });
506
+
507
+ const createRoomRealtimeSession = createRoute({
508
+ operationId: 'createRoomRealtimeSession',
509
+ method: 'post',
510
+ path: '/media/realtime/session',
511
+ tags: ['client'],
512
+ summary: 'Create a room realtime media session',
513
+ description: 'Creates a Cloudflare Realtime session for the authenticated room member.',
514
+ request: {
515
+ query: roomQuerySchema,
516
+ body: { content: { 'application/json': { schema: roomRealtimeCreateSessionBodySchema } }, required: false },
517
+ },
518
+ responses: {
519
+ 200: { description: 'Realtime session created', content: { 'application/json': { schema: roomRealtimeCreateSessionResponseSchema } } },
520
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
521
+ 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
522
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
523
+ 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
524
+ 409: { description: 'Conflicting existing published media', content: { 'application/json': { schema: errorResponseSchema } } },
525
+ },
526
+ });
527
+
528
+ const createRoomRealtimeIceServers = createRoute({
529
+ operationId: 'createRoomRealtimeIceServers',
530
+ method: 'post',
531
+ path: '/media/realtime/turn',
532
+ tags: ['client'],
533
+ summary: 'Generate TURN / ICE credentials for room realtime media',
534
+ description: 'Generates ICE server credentials for the authenticated room member.',
535
+ request: {
536
+ query: roomQuerySchema,
537
+ body: { content: { 'application/json': { schema: roomRealtimeIceServersBodySchema } }, required: false },
538
+ },
539
+ responses: {
540
+ 200: { description: 'ICE servers generated', content: { 'application/json': { schema: roomRealtimeIceServersResponseSchema } } },
541
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
542
+ 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
543
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
544
+ 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
545
+ },
546
+ });
547
+
548
+ const addRoomRealtimeTracks = createRoute({
549
+ operationId: 'addRoomRealtimeTracks',
550
+ method: 'post',
551
+ path: '/media/realtime/tracks/new',
552
+ tags: ['client'],
553
+ summary: 'Add realtime media tracks to a room session',
554
+ description: 'Creates or subscribes realtime tracks for the authenticated room member.',
555
+ request: {
556
+ query: roomQuerySchema,
557
+ body: { content: { 'application/json': { schema: roomRealtimeTracksBodySchema } }, required: true },
558
+ },
559
+ responses: {
560
+ 200: { description: 'Realtime tracks updated', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
561
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
562
+ 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
563
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
564
+ 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
565
+ },
566
+ });
567
+
568
+ const renegotiateRoomRealtimeSession = createRoute({
569
+ operationId: 'renegotiateRoomRealtimeSession',
570
+ method: 'put',
571
+ path: '/media/realtime/renegotiate',
572
+ tags: ['client'],
573
+ summary: 'Renegotiate a room realtime media session',
574
+ description: 'Submits a new session description for an existing room realtime media session.',
575
+ request: {
576
+ query: roomQuerySchema,
577
+ body: { content: { 'application/json': { schema: roomRealtimeRenegotiateBodySchema } }, required: true },
578
+ },
579
+ responses: {
580
+ 200: { description: 'Realtime session renegotiated', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
581
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
582
+ 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
583
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
584
+ 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
585
+ },
586
+ });
587
+
588
+ const closeRoomRealtimeTracks = createRoute({
589
+ operationId: 'closeRoomRealtimeTracks',
590
+ method: 'put',
591
+ path: '/media/realtime/tracks/close',
592
+ tags: ['client'],
593
+ summary: 'Close room realtime media tracks',
594
+ description: 'Closes provider tracks for the authenticated room member and optionally unpublishes room media state.',
595
+ request: {
596
+ query: roomQuerySchema,
597
+ body: { content: { 'application/json': { schema: roomRealtimeCloseTracksBodySchema } }, required: true },
598
+ },
599
+ responses: {
600
+ 200: { description: 'Realtime tracks closed', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
601
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
602
+ 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
603
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
604
+ 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
605
+ },
606
+ });
607
+
608
+ roomRoute.openapi(getRoomRealtimeSession, async (c) =>
609
+ proxyRoomDoRequest(c, '/media/realtime/session', 'GET', { requireAuth: true }));
610
+
611
+ roomRoute.openapi(createRoomRealtimeSession, async (c) =>
612
+ proxyRoomDoRequest(c, '/media/realtime/session', 'POST', {
613
+ requireAuth: true,
614
+ validatedJson: c.req.valid('json'),
615
+ }));
616
+
617
+ roomRoute.openapi(createRoomRealtimeIceServers, async (c) =>
618
+ proxyRoomDoRequest(c, '/media/realtime/turn', 'POST', {
619
+ requireAuth: true,
620
+ validatedJson: c.req.valid('json'),
621
+ }));
622
+
623
+ roomRoute.openapi(addRoomRealtimeTracks, async (c) =>
624
+ proxyRoomDoRequest(c, '/media/realtime/tracks/new', 'POST', {
625
+ requireAuth: true,
626
+ validatedJson: c.req.valid('json'),
627
+ }));
628
+
629
+ roomRoute.openapi(renegotiateRoomRealtimeSession, async (c) =>
630
+ proxyRoomDoRequest(c, '/media/realtime/renegotiate', 'PUT', {
631
+ requireAuth: true,
632
+ validatedJson: c.req.valid('json'),
633
+ }));
634
+
635
+ roomRoute.openapi(closeRoomRealtimeTracks, async (c) =>
636
+ proxyRoomDoRequest(c, '/media/realtime/tracks/close', 'PUT', {
637
+ requireAuth: true,
638
+ validatedJson: c.req.valid('json'),
639
+ }));