@edge-base/server 0.2.5 → 0.2.7

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 (135) hide show
  1. package/admin-build/_app/immutable/chunks/{DILS_-VJ.js → B3CvhH3c.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/BDYewzou.js +1 -0
  3. package/admin-build/_app/immutable/chunks/{Cdm5zBRA.js → BEM1BeVF.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{Dt4vL4Df.js → BYL_uBga.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{B94PilAN.js → BYyykAbh.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/BaUG2TJ-.js +1 -0
  7. package/admin-build/_app/immutable/chunks/{C72lTcG0.js → Bcs4KYNp.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{D2j3I1VQ.js → BfpUQYr3.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/BhCO1Fpt.js +1 -0
  10. package/admin-build/_app/immutable/chunks/{B8s_s9QY.js → BkZCgsc3.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/CIOC1v_q.js +128 -0
  12. package/admin-build/_app/immutable/chunks/CjcrXziO.js +2 -0
  13. package/admin-build/_app/immutable/chunks/CvczjTXx.js +1 -0
  14. package/admin-build/_app/immutable/chunks/D1u3u7xu.js +1 -0
  15. package/admin-build/_app/immutable/chunks/{B0HRJ657.js → DOOPbWwG.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{BqTb6Mxk.js → DaXO-sFP.js} +1 -1
  17. package/admin-build/_app/immutable/chunks/DnpbvAPi.js +1 -0
  18. package/admin-build/_app/immutable/chunks/{B6MschND.js → Dz9cUCuv.js} +1 -1
  19. package/admin-build/_app/immutable/chunks/{CaVKAiCe.js → Tea2dBJ8.js} +1 -1
  20. package/admin-build/_app/immutable/chunks/{Z41NK6i6.js → bguI1TeA.js} +1 -1
  21. package/admin-build/_app/immutable/chunks/{J2Gw0SMu.js → ejoEf2I5.js} +1 -1
  22. package/admin-build/_app/immutable/chunks/{B2TnDKF7.js → iEyeblJR.js} +1 -1
  23. package/admin-build/_app/immutable/chunks/{_teD5ji5.js → nlAMTi52.js} +1 -1
  24. package/admin-build/_app/immutable/chunks/qKdzaeX3.js +1 -0
  25. package/admin-build/_app/immutable/entry/{app.D3flihMw.js → app.DoUaxnew.js} +2 -2
  26. package/admin-build/_app/immutable/entry/start.MmZh8oBH.js +1 -0
  27. package/admin-build/_app/immutable/nodes/{0.CdczqZLK.js → 0.Dsxi8s7i.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/1.Cp2l-hol.js +1 -0
  29. package/admin-build/_app/immutable/nodes/10.4oY6m8Nz.js +1 -0
  30. package/admin-build/_app/immutable/nodes/11.DfcozD4J.js +1 -0
  31. package/admin-build/_app/immutable/nodes/12.uJgZdCIA.js +1 -0
  32. package/admin-build/_app/immutable/nodes/13.CaN1kRev.js +110 -0
  33. package/admin-build/_app/immutable/nodes/14.DQ5xIi3s.js +3 -0
  34. package/admin-build/_app/immutable/nodes/15.B_EkebTJ.js +1 -0
  35. package/admin-build/_app/immutable/nodes/{16.BR7WwQrS.js → 16.Tko1ZX8-.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{17.Cm57KKXV.js → 17.BCmWMJX9.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/18.hmGhl1O2.js +1 -0
  38. package/admin-build/_app/immutable/nodes/19.D-1infOo.js +2 -0
  39. package/admin-build/_app/immutable/nodes/{20.DnHeFlTv.js → 20.CY4KKcBL.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/21.B9lbNUQr.js +1 -0
  41. package/admin-build/_app/immutable/nodes/22.14Vd7bnt.js +1 -0
  42. package/admin-build/_app/immutable/nodes/{23.CWSGMcKJ.js → 23.Be6jK77o.js} +2 -2
  43. package/admin-build/_app/immutable/nodes/24.CSTFkr6R.js +2 -0
  44. package/admin-build/_app/immutable/nodes/25.DRTg8fHc.js +2 -0
  45. package/admin-build/_app/immutable/nodes/26.DKt-9lwQ.js +1 -0
  46. package/admin-build/_app/immutable/nodes/27.D5caPu0F.js +1 -0
  47. package/admin-build/_app/immutable/nodes/28.hJhlnlyY.js +1 -0
  48. package/admin-build/_app/immutable/nodes/29.CDYBzFyT.js +1 -0
  49. package/admin-build/_app/immutable/nodes/{3.B6q-7qr8.js → 3.DMyKwkGn.js} +1 -1
  50. package/admin-build/_app/immutable/nodes/30.BaHNeEmc.js +1 -0
  51. package/admin-build/_app/immutable/nodes/31.C6PV5L-2.js +1 -0
  52. package/admin-build/_app/immutable/nodes/4.9E118Ftm.js +1 -0
  53. package/admin-build/_app/immutable/nodes/5.D8guAl3v.js +1 -0
  54. package/admin-build/_app/immutable/nodes/6.D1u__DtT.js +1 -0
  55. package/admin-build/_app/immutable/nodes/7.DWXHnRFf.js +1 -0
  56. package/admin-build/_app/immutable/nodes/8.Dojd8krc.js +1 -0
  57. package/admin-build/_app/immutable/nodes/9.CLtrr0K_.js +1 -0
  58. package/admin-build/_app/version.json +1 -1
  59. package/admin-build/index.html +7 -7
  60. package/openapi.json +6 -1941
  61. package/package.json +3 -3
  62. package/src/__tests__/openapi-coverage.test.ts +0 -6
  63. package/src/__tests__/push-handlers.test.ts +1 -1
  64. package/src/__tests__/room-auth-state-loss.test.ts +6 -0
  65. package/src/__tests__/room-handler-context.test.ts +0 -31
  66. package/src/__tests__/room-rate-limit-scopes.test.ts +1 -5
  67. package/src/__tests__/room-runtime-routing.test.ts +24 -111
  68. package/src/__tests__/route-parser.test.ts +6 -0
  69. package/src/__tests__/schema.test.ts +15 -6
  70. package/src/__tests__/smoke-skip-report.test.ts +1 -1
  71. package/src/durable-objects/database-do.ts +7 -1
  72. package/src/durable-objects/room-runtime-base.ts +290 -57
  73. package/src/durable-objects/rooms-do.ts +212 -1336
  74. package/src/index.ts +23 -9
  75. package/src/lib/d1-handler.ts +32 -17
  76. package/src/lib/openapi.ts +1 -4
  77. package/src/lib/postgres-handler.ts +24 -12
  78. package/src/lib/route-parser.ts +3 -0
  79. package/src/lib/schemas.ts +12 -2
  80. package/src/middleware/captcha-verify.ts +16 -3
  81. package/src/middleware/error-handler.ts +1 -1
  82. package/src/middleware/rules.ts +28 -9
  83. package/src/routes/admin-auth.ts +3 -3
  84. package/src/routes/admin.ts +13 -8
  85. package/src/routes/analytics-api.ts +3 -3
  86. package/src/routes/auth.ts +1 -1
  87. package/src/routes/backup.ts +1 -1
  88. package/src/routes/d1.ts +14 -7
  89. package/src/routes/database-live.ts +13 -6
  90. package/src/routes/kv.ts +21 -10
  91. package/src/routes/oauth.ts +1 -1
  92. package/src/routes/push.ts +119 -77
  93. package/src/routes/room.ts +203 -280
  94. package/src/routes/schema-endpoint.ts +2 -2
  95. package/src/routes/sql.ts +10 -6
  96. package/src/routes/storage.ts +4 -2
  97. package/src/routes/vectorize.ts +16 -4
  98. package/src/types.ts +1 -14
  99. package/admin-build/_app/immutable/chunks/6oMK_164.js +0 -1
  100. package/admin-build/_app/immutable/chunks/BEW7Ez_g.js +0 -1
  101. package/admin-build/_app/immutable/chunks/BoOooyH6.js +0 -1
  102. package/admin-build/_app/immutable/chunks/BvHnF5tV.js +0 -1
  103. package/admin-build/_app/immutable/chunks/CoI6jjbg.js +0 -2
  104. package/admin-build/_app/immutable/chunks/CrOZMmdF.js +0 -1
  105. package/admin-build/_app/immutable/chunks/Cw6OYcq-.js +0 -1
  106. package/admin-build/_app/immutable/chunks/DPdQ7z0T.js +0 -128
  107. package/admin-build/_app/immutable/chunks/pUxw8jfq.js +0 -1
  108. package/admin-build/_app/immutable/entry/start.Cl6sLxnz.js +0 -1
  109. package/admin-build/_app/immutable/nodes/1.DxcSsEqS.js +0 -1
  110. package/admin-build/_app/immutable/nodes/10.DuAd4aIm.js +0 -1
  111. package/admin-build/_app/immutable/nodes/11.0jgHQL92.js +0 -1
  112. package/admin-build/_app/immutable/nodes/12.CKNPqmyy.js +0 -1
  113. package/admin-build/_app/immutable/nodes/13.B1p2POXS.js +0 -110
  114. package/admin-build/_app/immutable/nodes/14.Bb-REBND.js +0 -3
  115. package/admin-build/_app/immutable/nodes/15.1uBFCX0X.js +0 -1
  116. package/admin-build/_app/immutable/nodes/18.CoiwfAuQ.js +0 -1
  117. package/admin-build/_app/immutable/nodes/19.B8ZdLlXj.js +0 -2
  118. package/admin-build/_app/immutable/nodes/21.CJFaf0Ia.js +0 -1
  119. package/admin-build/_app/immutable/nodes/22.CItETFzy.js +0 -1
  120. package/admin-build/_app/immutable/nodes/24.CWbEqNMB.js +0 -2
  121. package/admin-build/_app/immutable/nodes/25.DRkLEhKi.js +0 -2
  122. package/admin-build/_app/immutable/nodes/26.BRxO8AYH.js +0 -1
  123. package/admin-build/_app/immutable/nodes/27.BLs-nVHz.js +0 -1
  124. package/admin-build/_app/immutable/nodes/28.G79qkdBK.js +0 -1
  125. package/admin-build/_app/immutable/nodes/29.BOcI6g0N.js +0 -1
  126. package/admin-build/_app/immutable/nodes/30.DAIC7dKd.js +0 -1
  127. package/admin-build/_app/immutable/nodes/31.pl0XXjXF.js +0 -1
  128. package/admin-build/_app/immutable/nodes/4.DOdvVlZj.js +0 -1
  129. package/admin-build/_app/immutable/nodes/5.BW_zlgye.js +0 -1
  130. package/admin-build/_app/immutable/nodes/6.Dxy1CAI2.js +0 -1
  131. package/admin-build/_app/immutable/nodes/7.BG98w_o7.js +0 -1
  132. package/admin-build/_app/immutable/nodes/8.DoG5R2rG.js +0 -1
  133. package/admin-build/_app/immutable/nodes/9.Dmxf6zAC.js +0 -1
  134. package/src/__tests__/cloudflare-realtime.test.ts +0 -113
  135. package/src/lib/cloudflare-realtime.ts +0 -251
@@ -45,109 +45,26 @@ const roomConnectDiagnosticSchema = z.object({
45
45
  pendingCount: z.number().optional(),
46
46
  maxPending: z.number().optional(),
47
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' }),
48
+ const roomSummarySchema = z.object({
49
+ namespace: z.string(),
50
+ roomId: z.string(),
51
+ metadata: z.record(z.string(), z.unknown()),
52
+ occupancy: z.object({
53
+ activeMembers: z.number(),
54
+ activeConnections: z.number(),
55
+ }),
56
+ updatedAt: z.string(),
51
57
  });
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' }),
58
+ const roomSummaryBatchBodySchema = z.object({
59
+ namespace: z.string().openapi({ description: 'Room namespace shared by the requested room IDs' }),
60
+ ids: z.array(z.string()).min(1).max(100).openapi({ description: 'Room IDs to summarize' }),
66
61
  });
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(),
62
+ const roomSummaryCollectionSchema = z.object({
63
+ namespace: z.string(),
64
+ items: z.array(roomSummarySchema),
65
+ deniedIds: z.array(z.string()),
66
+ updatedAt: z.string(),
72
67
  });
73
- const roomCloudflareRealtimeKitCreateSessionBodySchema = z.object({
74
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the Cloudflare RealtimeKit participant to' }),
75
- customParticipantId: z.string().optional().openapi({ description: 'Optional custom participant identifier for the provisioned RealtimeKit participant' }),
76
- name: z.string().optional().openapi({ description: 'Optional display name for the provisioned RealtimeKit participant' }),
77
- picture: z.string().optional().openapi({ description: 'Optional avatar URL for the provisioned RealtimeKit participant' }),
78
- });
79
- const roomRealtimeCreateSessionResponseSchema = z.object({
80
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
81
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
82
- errorCode: z.string().optional(),
83
- errorDescription: z.string().optional(),
84
- connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
85
- reused: z.boolean().optional().openapi({ description: 'Whether an existing provider session was reused' }),
86
- });
87
- const roomCloudflareRealtimeKitCreateSessionResponseSchema = z.object({
88
- sessionId: z.string().openapi({ description: 'Cloudflare RealtimeKit participant ID' }),
89
- meetingId: z.string().openapi({ description: 'Cloudflare RealtimeKit meeting ID backing the room session' }),
90
- participantId: z.string().openapi({ description: 'Cloudflare RealtimeKit participant ID' }),
91
- authToken: z.string().openapi({ description: 'RealtimeKit auth token for the provisioned participant' }),
92
- presetName: z.string().optional().openapi({ description: 'RealtimeKit preset used for the provisioned participant' }),
93
- connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
94
- reused: z.boolean().optional().openapi({ description: 'Whether an existing provider participant was reused' }),
95
- });
96
- const roomRealtimeSessionStateSchema = z.object({
97
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
98
- connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
99
- createdAt: z.number().openapi({ description: 'Unix epoch milliseconds when the session was created' }),
100
- updatedAt: z.number().openapi({ description: 'Unix epoch milliseconds when the session was last updated' }),
101
- });
102
- const roomRealtimeIceServerSchema = z.object({
103
- urls: z.union([z.array(z.string()), z.string()]).openapi({ description: 'ICE server URL or URL list' }),
104
- username: z.string().optional(),
105
- credential: z.string().optional(),
106
- });
107
- const roomRealtimeIceServersBodySchema = z.object({
108
- ttl: z.number().optional().openapi({ description: 'Requested TURN credential TTL in seconds' }),
109
- });
110
- const roomRealtimeIceServersResponseSchema = z.object({
111
- iceServers: z.array(roomRealtimeIceServerSchema).openapi({ description: 'ICE servers returned by Cloudflare TURN' }),
112
- });
113
- const roomRealtimeTracksResponseSchema = z.object({
114
- errorCode: z.string().optional(),
115
- errorDescription: z.string().optional(),
116
- requiresImmediateRenegotiation: z.boolean().optional(),
117
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
118
- tracks: z.array(roomRealtimeTrackSchema).optional(),
119
- });
120
- const roomRealtimeTracksBodySchema = z.object({
121
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
122
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the track operation to' }),
123
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
124
- tracks: z.array(roomRealtimeTrackSchema).min(1).openapi({ description: 'Tracks to create or subscribe to' }),
125
- autoDiscover: z.boolean().optional().openapi({ description: 'Ask the provider to auto-discover remote tracks' }),
126
- publish: z.object({
127
- kind: z.enum(['audio', 'video', 'screen']).optional(),
128
- trackId: z.string().optional(),
129
- deviceId: z.string().optional(),
130
- muted: z.boolean().optional(),
131
- }).optional().openapi({ description: 'Optional room media state updates to apply after track creation' }),
132
- });
133
- const roomRealtimeRenegotiateBodySchema = z.object({
134
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
135
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the renegotiation to' }),
136
- sessionDescription: roomRealtimeSessionDescriptionSchema,
137
- });
138
- const roomRealtimeCloseTracksBodySchema = z.object({
139
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
140
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the close operation to' }),
141
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
142
- tracks: z.array(z.object({
143
- mid: z.string().openapi({ description: 'Track MID to close' }),
144
- })).min(1).openapi({ description: 'Tracks to close' }),
145
- force: z.boolean().optional().openapi({ description: 'Force close even if the provider reports the track as active' }),
146
- unpublish: z.object({
147
- kind: z.enum(['audio', 'video', 'screen']).optional(),
148
- }).optional().openapi({ description: 'Optional room media state cleanup after closing tracks' }),
149
- });
150
-
151
68
  function isRoomOperationPublic(
152
69
  namespaceConfig: RoomNamespaceConfig | null | undefined,
153
70
  operation: 'metadata' | 'join' | 'action',
@@ -157,6 +74,22 @@ function isRoomOperationPublic(
157
74
  return !!namespaceConfig.public[operation];
158
75
  }
159
76
 
77
+ function warnRoomDevelopmentFallback(
78
+ namespace: string,
79
+ operation: 'metadata' | 'join' | 'action',
80
+ ): void {
81
+ const warningKey = `${namespace}:${operation}`;
82
+ if (roomFallbackWarnings.has(warningKey)) {
83
+ return;
84
+ }
85
+ roomFallbackWarnings.add(warningKey);
86
+ console.warn(
87
+ `[Room] ${warningKey} is allowed because release=false and no explicit room rule was found. `
88
+ + `This fallback is local-dev only. Add rooms.${namespace}.access.${operation} or set `
89
+ + `rooms.${namespace}.public.${operation}=true to make the behavior explicit.`,
90
+ );
91
+ }
92
+
160
93
  function getRoomAuthContext(
161
94
  auth: {
162
95
  id?: string;
@@ -188,11 +121,11 @@ export async function proxyRoomDoRequest(
188
121
  const roomId = c.req.query('id');
189
122
 
190
123
  if (!namespace || !roomId) {
191
- return c.json({ code: 400, message: 'namespace and id query parameters required' }, 400);
124
+ return c.json({ code: 400, message: "Missing required query parameters 'namespace' and 'id' for room requests." }, 400);
192
125
  }
193
126
 
194
127
  if (options?.requireAuth && !c.get('auth')) {
195
- return c.json({ code: 401, message: 'Authentication required' }, 401);
128
+ return c.json({ code: 401, message: 'Authentication required. Sign in before trying to access this room.' }, 401);
196
129
  }
197
130
 
198
131
  const config = parseConfig(c.env);
@@ -433,6 +366,42 @@ const getRoomMetadata = createRoute({
433
366
  },
434
367
  });
435
368
 
369
+ const getRoomSummary = createRoute({
370
+ operationId: 'getRoomSummary',
371
+ method: 'get',
372
+ path: '/summary',
373
+ tags: ['client'],
374
+ summary: 'Get room summary',
375
+ description: 'Returns lobby-safe room metadata plus current occupancy without joining the room.',
376
+ request: {
377
+ query: roomQuerySchema,
378
+ },
379
+ responses: {
380
+ 200: { description: 'Room summary', content: { 'application/json': { schema: roomSummarySchema } } },
381
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
382
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
383
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
384
+ },
385
+ });
386
+
387
+ const getRoomSummaries = createRoute({
388
+ operationId: 'getRoomSummaries',
389
+ method: 'post',
390
+ path: '/summaries',
391
+ tags: ['client'],
392
+ summary: 'Get summaries for multiple rooms',
393
+ description: 'Returns lobby-safe room metadata plus current occupancy for multiple room IDs in the same namespace.',
394
+ request: {
395
+ body: { content: { 'application/json': { schema: roomSummaryBatchBodySchema } }, required: true },
396
+ },
397
+ responses: {
398
+ 200: { description: 'Room summaries', content: { 'application/json': { schema: roomSummaryCollectionSchema } } },
399
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
400
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
401
+ 404: { description: 'Room feature not configured', content: { 'application/json': { schema: errorResponseSchema } } },
402
+ },
403
+ });
404
+
436
405
  roomRoute.openapi(getRoomMetadata, async (c) => {
437
406
  const namespace = c.req.query('namespace');
438
407
  const roomId = c.req.query('id');
@@ -459,11 +428,7 @@ roomRoute.openapi(getRoomMetadata, async (c) => {
459
428
  return c.json({ code: 403, message: 'Room metadata requires access.metadata or public.metadata in release mode' }, 403);
460
429
  }
461
430
  if (!config.release) {
462
- const warningKey = `${namespace}:metadata`;
463
- if (!roomFallbackWarnings.has(warningKey)) {
464
- roomFallbackWarnings.add(warningKey);
465
- console.warn(`[Room] ${warningKey} is using development-mode allow-by-default. Add rooms.${namespace}.access.metadata or public.metadata to make this explicit.`);
466
- }
431
+ warnRoomDevelopmentFallback(namespace, 'metadata');
467
432
  }
468
433
  }
469
434
  if (metadataAccess) {
@@ -498,184 +463,142 @@ roomRoute.openapi(getRoomMetadata, async (c) => {
498
463
  return doStub.fetch(doRequest);
499
464
  });
500
465
 
501
- const getRoomRealtimeSession = createRoute({
502
- operationId: 'getRoomRealtimeSession',
503
- method: 'get',
504
- path: '/media/realtime/session',
505
- tags: ['client'],
506
- summary: 'Get the active room realtime media session',
507
- description: 'Returns the provider session currently bound to the authenticated room member.',
508
- request: {
509
- query: roomQuerySchema.extend({
510
- connectionId: z.string().optional().openapi({ description: 'Optional room connection ID override' }),
511
- }),
512
- },
513
- responses: {
514
- 200: { description: 'Active room realtime session', content: { 'application/json': { schema: roomRealtimeSessionStateSchema } } },
515
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
516
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
517
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
518
- 404: { description: 'No active session or runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
519
- },
520
- });
466
+ roomRoute.openapi(getRoomSummary, async (c) => {
467
+ const namespace = c.req.query('namespace');
468
+ const roomId = c.req.query('id');
521
469
 
522
- const createRoomRealtimeSession = createRoute({
523
- operationId: 'createRoomRealtimeSession',
524
- method: 'post',
525
- path: '/media/realtime/session',
526
- tags: ['client'],
527
- summary: 'Create a room realtime media session',
528
- description: 'Creates a Cloudflare Realtime session for the authenticated room member.',
529
- request: {
530
- query: roomQuerySchema,
531
- body: { content: { 'application/json': { schema: roomRealtimeCreateSessionBodySchema } }, required: false },
532
- },
533
- responses: {
534
- 200: { description: 'Realtime session created', content: { 'application/json': { schema: roomRealtimeCreateSessionResponseSchema } } },
535
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
536
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
537
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
538
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
539
- 409: { description: 'Conflicting existing published media', content: { 'application/json': { schema: errorResponseSchema } } },
540
- },
541
- });
470
+ if (!namespace || !roomId) {
471
+ return c.json({ code: 400, message: 'namespace and id query parameters required' }, 400);
472
+ }
542
473
 
543
- const createRoomCloudflareRealtimeKitSession = createRoute({
544
- operationId: 'createRoomCloudflareRealtimeKitSession',
545
- method: 'post',
546
- path: '/media/cloudflare_realtimekit/session',
547
- tags: ['client'],
548
- summary: 'Create a room Cloudflare RealtimeKit session',
549
- description: 'Creates a Cloudflare RealtimeKit session for the authenticated room member.',
550
- request: {
551
- query: roomQuerySchema,
552
- body: { content: { 'application/json': { schema: roomCloudflareRealtimeKitCreateSessionBodySchema } }, required: false },
553
- },
554
- responses: {
555
- 200: { description: 'Cloudflare RealtimeKit session created', content: { 'application/json': { schema: roomCloudflareRealtimeKitCreateSessionResponseSchema } } },
556
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
557
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
558
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
559
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
560
- 409: { description: 'Conflicting existing published media', content: { 'application/json': { schema: errorResponseSchema } } },
561
- },
562
- });
474
+ const config = parseConfig(c.env);
475
+ const namespaceConfig = config.rooms?.[namespace];
476
+ if (config.release) {
477
+ if (!config.rooms?.[namespace]) {
478
+ return c.json({
479
+ code: 403,
480
+ message: `Room namespace '${namespace}' not configured`,
481
+ }, 403);
482
+ }
483
+ }
563
484
 
564
- const createRoomRealtimeIceServers = createRoute({
565
- operationId: 'createRoomRealtimeIceServers',
566
- method: 'post',
567
- path: '/media/realtime/turn',
568
- tags: ['client'],
569
- summary: 'Generate TURN / ICE credentials for room realtime media',
570
- description: 'Generates ICE server credentials for the authenticated room member.',
571
- request: {
572
- query: roomQuerySchema,
573
- body: { content: { 'application/json': { schema: roomRealtimeIceServersBodySchema } }, required: false },
574
- },
575
- responses: {
576
- 200: { description: 'ICE servers generated', content: { 'application/json': { schema: roomRealtimeIceServersResponseSchema } } },
577
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
578
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
579
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
580
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
581
- },
582
- });
485
+ const metadataAccess = namespaceConfig?.access?.metadata;
486
+ if (!metadataAccess) {
487
+ if (config.release && !isRoomOperationPublic(namespaceConfig, 'metadata')) {
488
+ return c.json({ code: 403, message: 'Room summary requires access.metadata or public.metadata in release mode' }, 403);
489
+ }
490
+ if (!config.release) {
491
+ warnRoomDevelopmentFallback(namespace, 'metadata');
492
+ }
493
+ }
494
+ if (metadataAccess) {
495
+ 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;
496
+ const allowed = await Promise.resolve(metadataAccess(
497
+ getRoomAuthContext(auth),
498
+ roomId,
499
+ )).catch(() => false);
500
+ if (!allowed) {
501
+ return c.json({ code: 403, message: 'Denied by room metadata access rule' }, 403);
502
+ }
503
+ }
583
504
 
584
- const addRoomRealtimeTracks = createRoute({
585
- operationId: 'addRoomRealtimeTracks',
586
- method: 'post',
587
- path: '/media/realtime/tracks/new',
588
- tags: ['client'],
589
- summary: 'Add realtime media tracks to a room session',
590
- description: 'Creates or subscribes realtime tracks for the authenticated room member.',
591
- request: {
592
- query: roomQuerySchema,
593
- body: { content: { 'application/json': { schema: roomRealtimeTracksBodySchema } }, required: true },
594
- },
595
- responses: {
596
- 200: { description: 'Realtime tracks updated', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
597
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
598
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
599
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
600
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
601
- },
602
- });
505
+ const runtime = resolveRoomRuntime(c.env);
506
+ if (!runtime.binding) {
507
+ return c.json({ code: 404, message: `Room runtime '${runtime.target}' not configured` }, 404);
508
+ }
603
509
 
604
- const renegotiateRoomRealtimeSession = createRoute({
605
- operationId: 'renegotiateRoomRealtimeSession',
606
- method: 'put',
607
- path: '/media/realtime/renegotiate',
608
- tags: ['client'],
609
- summary: 'Renegotiate a room realtime media session',
610
- description: 'Submits a new session description for an existing room realtime media session.',
611
- request: {
612
- query: roomQuerySchema,
613
- body: { content: { 'application/json': { schema: roomRealtimeRenegotiateBodySchema } }, required: true },
614
- },
615
- responses: {
616
- 200: { description: 'Realtime session renegotiated', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
617
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
618
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
619
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
620
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
621
- },
622
- });
510
+ const doName = `${namespace}::${roomId}`;
511
+ const doId = runtime.binding.idFromName(doName);
512
+ const doStub = runtime.binding.get(doId);
623
513
 
624
- const closeRoomRealtimeTracks = createRoute({
625
- operationId: 'closeRoomRealtimeTracks',
626
- method: 'put',
627
- path: '/media/realtime/tracks/close',
628
- tags: ['client'],
629
- summary: 'Close room realtime media tracks',
630
- description: 'Closes provider tracks for the authenticated room member and optionally unpublishes room media state.',
631
- request: {
632
- query: roomQuerySchema,
633
- body: { content: { 'application/json': { schema: roomRealtimeCloseTracksBodySchema } }, required: true },
634
- },
635
- responses: {
636
- 200: { description: 'Realtime tracks closed', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
637
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
638
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
639
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
640
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
641
- },
514
+ const url = new URL(c.req.url);
515
+ url.pathname = '/summary';
516
+ url.searchParams.set('room', doName);
517
+ const doRequest = new Request(url.toString(), {
518
+ method: 'GET',
519
+ headers: c.req.raw.headers,
520
+ });
521
+
522
+ return doStub.fetch(doRequest);
642
523
  });
643
524
 
644
- roomRoute.openapi(getRoomRealtimeSession, async (c) =>
645
- proxyRoomDoRequest(c, '/media/realtime/session', 'GET', { requireAuth: true }));
646
-
647
- roomRoute.openapi(createRoomRealtimeSession, async (c) =>
648
- proxyRoomDoRequest(c, '/media/realtime/session', 'POST', {
649
- requireAuth: true,
650
- validatedJson: c.req.valid('json'),
651
- }));
652
-
653
- roomRoute.openapi(createRoomRealtimeIceServers, async (c) =>
654
- proxyRoomDoRequest(c, '/media/realtime/turn', 'POST', {
655
- requireAuth: true,
656
- validatedJson: c.req.valid('json'),
657
- }));
658
-
659
- roomRoute.openapi(addRoomRealtimeTracks, async (c) =>
660
- proxyRoomDoRequest(c, '/media/realtime/tracks/new', 'POST', {
661
- requireAuth: true,
662
- validatedJson: c.req.valid('json'),
663
- }));
664
-
665
- roomRoute.openapi(renegotiateRoomRealtimeSession, async (c) =>
666
- proxyRoomDoRequest(c, '/media/realtime/renegotiate', 'PUT', {
667
- requireAuth: true,
668
- validatedJson: c.req.valid('json'),
669
- }));
670
-
671
- roomRoute.openapi(closeRoomRealtimeTracks, async (c) =>
672
- proxyRoomDoRequest(c, '/media/realtime/tracks/close', 'PUT', {
673
- requireAuth: true,
674
- validatedJson: c.req.valid('json'),
675
- }));
676
-
677
- roomRoute.openapi(createRoomCloudflareRealtimeKitSession, async (c) =>
678
- proxyRoomDoRequest(c, '/media/cloudflare_realtimekit/session', 'POST', {
679
- requireAuth: true,
680
- validatedJson: c.req.valid('json'),
681
- }));
525
+ roomRoute.openapi(getRoomSummaries, async (c) => {
526
+ const body = c.req.valid('json') as z.infer<typeof roomSummaryBatchBodySchema>;
527
+ const namespace = body.namespace;
528
+ const roomIds = [...new Set(body.ids)];
529
+
530
+ const config = parseConfig(c.env);
531
+ const namespaceConfig = config.rooms?.[namespace];
532
+ if (config.release && !namespaceConfig) {
533
+ return c.json({
534
+ code: 403,
535
+ message: `Room namespace '${namespace}' not configured`,
536
+ }, 403);
537
+ }
538
+
539
+ const metadataAccess = namespaceConfig?.access?.metadata;
540
+ if (!metadataAccess) {
541
+ if (config.release && !isRoomOperationPublic(namespaceConfig, 'metadata')) {
542
+ return c.json({ code: 403, message: 'Room summaries require access.metadata or public.metadata in release mode' }, 403);
543
+ }
544
+ if (!config.release) {
545
+ warnRoomDevelopmentFallback(namespace, 'metadata');
546
+ }
547
+ }
548
+
549
+ const runtime = resolveRoomRuntime(c.env);
550
+ if (!runtime.binding) {
551
+ return c.json({ code: 404, message: `Room runtime '${runtime.target}' not configured` }, 404);
552
+ }
553
+
554
+ 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;
555
+ const authContext = getRoomAuthContext(auth);
556
+ const allowedRoomIds: string[] = [];
557
+ const deniedIds: string[] = [];
558
+
559
+ if (metadataAccess) {
560
+ for (const roomId of roomIds) {
561
+ const allowed = await Promise.resolve(metadataAccess(authContext, roomId)).catch(() => false);
562
+ if (allowed) {
563
+ allowedRoomIds.push(roomId);
564
+ } else {
565
+ deniedIds.push(roomId);
566
+ }
567
+ }
568
+ } else {
569
+ allowedRoomIds.push(...roomIds);
570
+ }
571
+
572
+ const items = await Promise.all(
573
+ allowedRoomIds.map(async (roomId) => {
574
+ const doName = `${namespace}::${roomId}`;
575
+ const doId = runtime.binding!.idFromName(doName);
576
+ const doStub = runtime.binding!.get(doId);
577
+
578
+ const url = new URL(c.req.url);
579
+ url.pathname = '/summary';
580
+ url.searchParams.set('room', doName);
581
+ const doResponse = await doStub.fetch(new Request(url.toString(), {
582
+ method: 'GET',
583
+ headers: c.req.raw.headers,
584
+ }));
585
+
586
+ if (!doResponse.ok) {
587
+ const errorPayload = await doResponse.json().catch(() => null) as { message?: string } | null;
588
+ throw new Error(
589
+ errorPayload?.message
590
+ ?? `Failed to load room summary for '${roomId}' in namespace '${namespace}'.`,
591
+ );
592
+ }
593
+
594
+ return doResponse.json() as Promise<z.infer<typeof roomSummarySchema>>;
595
+ }),
596
+ );
597
+
598
+ return c.json({
599
+ namespace,
600
+ items,
601
+ deniedIds,
602
+ updatedAt: new Date().toISOString(),
603
+ });
604
+ });
@@ -45,14 +45,14 @@ schemaRoute.openapi(getSchema, async (c) => {
45
45
  buildConstraintCtx(c.env, c.req),
46
46
  );
47
47
  if (skResult === 'invalid') {
48
- throw new EdgeBaseError(401, 'Unauthorized. Invalid Service Key.');
48
+ throw new EdgeBaseError(401, 'Invalid X-EdgeBase-Service-Key for schema reads.');
49
49
  }
50
50
  const serviceKeyBypass = skResult === 'valid';
51
51
 
52
52
  if (!serviceKeyBypass) {
53
53
  const auth = c.get('auth');
54
54
  if (!auth) {
55
- throw new EdgeBaseError(401, 'Authentication required.');
55
+ throw new EdgeBaseError(401, 'Authentication required to read the schema endpoint.');
56
56
  }
57
57
  }
58
58
  }
package/src/routes/sql.ts CHANGED
@@ -39,6 +39,10 @@ import { executeProviderAwareSql } from '../lib/provider-aware-sql.js';
39
39
 
40
40
  export const sqlRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
41
41
 
42
+ function invalidSqlJsonMessage(): string {
43
+ return 'Invalid JSON body for SQL execution. Send application/json with { namespace, sql, params? }.';
44
+ }
45
+
42
46
  /**
43
47
  * POST /api/sql
44
48
  * Body: { namespace: string, id?: string, sql: string, params?: unknown[] }
@@ -81,19 +85,19 @@ sqlRoute.openapi(executeSql, async (c) => {
81
85
  try {
82
86
  body = await c.req.json();
83
87
  } catch {
84
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
88
+ return c.json({ code: 400, message: invalidSqlJsonMessage() }, 400);
85
89
  }
86
90
 
87
91
  const { namespace, id, sql, params } = body;
88
92
 
89
93
  if (!namespace || typeof namespace !== 'string') {
90
- return c.json({ code: 400, message: 'namespace is required' }, 400);
94
+ return c.json({ code: 400, message: "Missing required field 'namespace' for SQL execution." }, 400);
91
95
  }
92
96
  if (id !== undefined && id !== null && typeof id !== 'string') {
93
- return c.json({ code: 400, message: 'id must be a string' }, 400);
97
+ return c.json({ code: 400, message: "Field 'id' must be a string when provided for SQL execution." }, 400);
94
98
  }
95
99
  if (!sql || typeof sql !== 'string') {
96
- return c.json({ code: 400, message: 'sql is required' }, 400);
100
+ return c.json({ code: 400, message: "Missing required field 'sql' for SQL execution." }, 400);
97
101
  }
98
102
 
99
103
  // Validate namespace is declared in databases config (§1)
@@ -124,10 +128,10 @@ sqlRoute.openapi(executeSql, async (c) => {
124
128
  buildConstraintCtx(c.env, c.req),
125
129
  );
126
130
  if (skResult === 'missing') {
127
- return c.json({ code: 403, message: 'Service Key required to execute SQL' }, 403);
131
+ return c.json({ code: 403, message: `X-EdgeBase-Service-Key is required to execute SQL for database namespace '${namespace}'.` }, 403);
128
132
  }
129
133
  if (skResult === 'invalid') {
130
- return c.json({ code: 401, message: 'Unauthorized. Invalid Service Key.' }, 401);
134
+ return c.json({ code: 401, message: `Invalid X-EdgeBase-Service-Key for SQL execution in database namespace '${namespace}'.` }, 401);
131
135
  }
132
136
 
133
137
  try {
@@ -297,7 +297,7 @@ function checkServiceKey(env: Env, header: string | undefined, scope: string, re
297
297
  const { result } = validateKey(header, scope, config, env, undefined, constraintCtx);
298
298
  if (result === 'valid') return true;
299
299
  if (result === 'invalid') {
300
- throw new EdgeBaseError(401, 'Unauthorized. Invalid Service Key.', undefined, 'unauthenticated');
300
+ throw new EdgeBaseError(401, `Invalid X-EdgeBase-Service-Key for storage scope '${scope}'.`, undefined, 'unauthenticated');
301
301
  }
302
302
  return false; // 'missing' → continue to normal rules
303
303
  }
@@ -1236,7 +1236,9 @@ storage.openapi(deleteBatch, async (c) => {
1236
1236
  // afterDelete — plugin-registered storage hooks (per-file, non-blocking)
1237
1237
  executeStorageHooks('afterDelete', { ...fileMeta, bucket: bucketName }, auth, c.executionCtx, c.env, getWorkerUrl(c.req.url, c.env));
1238
1238
  } catch (e) {
1239
- const msg = e instanceof EdgeBaseError ? e.message : 'Unknown error.';
1239
+ const msg = e instanceof EdgeBaseError
1240
+ ? e.message
1241
+ : 'Delete failed with an unexpected storage error. Check worker logs for details.';
1240
1242
  failed.push({ key, error: msg });
1241
1243
  }
1242
1244
  }