@edge-base/server 0.2.6 → 0.2.8

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 (78) hide show
  1. package/admin-build/_app/immutable/chunks/{CbfX3ELZ.js → B9efkx2V.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{CLHN9MVr.js → BMXWUTG-.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{DemDWbs-.js → Bt4AyT3o.js} +3 -3
  4. package/admin-build/_app/immutable/chunks/CKVjMXZi.js +1 -0
  5. package/admin-build/_app/immutable/chunks/{BvoGcDFV.js → CMYgGhZR.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{LL3ulaxa.js → CTRjWhGs.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{BN_-k-Ck.js → CwyE59Yt.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{DQVP4KC-.js → D8aeTKry.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/{Ff90owjx.js → DGAHkap7.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{CR37B8DX.js → DPgR4-0v.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{DdvsFblq.js → DYtrHeVQ.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{CrwlCAM0.js → DcVb45Ds.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/Djnkhy-S.js +1 -0
  14. package/admin-build/_app/immutable/chunks/{DmDTovpg.js → fPy6xmgG.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{CCUxCptE.js → j4jxnAKj.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{qBm6xof8.js → zl2AUKMP.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.CP83Ni80.js → app.Cmz0WjMl.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.JE7dcbK1.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.DiRq7puO.js → 0.y6D_QyUb.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.BFeyKLGT.js → 1.CndRxhbH.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.zcee7hJx.js → 10.CdA5FmXy.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.BW7wLs2Y.js → 11.DG8SzMp_.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.CxJRlYSd.js → 12.CvmQqpFa.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.pp0F_5hn.js → 13.BbGNdswT.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.t3AfGiGo.js → 14.CZKsN7-O.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.B3agc7NX.js → 15.A7-CYgkG.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.C4uG2-i8.js → 16.hgJT9H-x.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.CwGxi1Bn.js → 17.DkWZbcN2.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.CrQyN_gU.js → 18.sX3Fb5gh.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.NEPUOXl7.js → 19.VAZUW-1K.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.DGHO8ipr.js → 20.DkIKxacG.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.DOjJlQKc.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.Dri5It7a.js → 22.BDaHvtaw.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.BPQP_Zte.js → 23.BVRzw_pD.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.D580FdSS.js → 24.CVhSJyG0.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.BMNPOZwF.js → 25.Bme-9bZn.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.XcpEcbiz.js → 26.Dsx7RIIs.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.C1zHHcYv.js → 27.DMGQnzFM.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.CuKzzrY8.js → 28.GGwFmEhZ.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.nLpBMXnM.js → 29.Dnghr0nk.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.5G_aseoL.js → 3.Cg7zZJP1.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.CQC4nLoU.js → 30.C0J24z3I.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.Bet8kxOK.js → 31.MdxFI8v6.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.nmJDYJpC.js → 4.DCAOVzGE.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.CnbYLG4E.js → 5.DzUQ-cTc.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.KA01b-3y.js → 6.CptBYTVj.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.CP9fkn1L.js → 7.DfeeQ0Rg.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.BTzDb---.js → 8.CIcvctW7.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.DkNJg_J6.js → 9.QKrvq4RA.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/openapi.json +6 -1941
  53. package/package.json +3 -3
  54. package/src/__tests__/admin-assets.test.ts +7 -7
  55. package/src/__tests__/frontend-assets.test.ts +75 -0
  56. package/src/__tests__/frontend-config.test.ts +16 -0
  57. package/src/__tests__/frontend-routing.test.ts +200 -0
  58. package/src/__tests__/openapi-coverage.test.ts +0 -6
  59. package/src/__tests__/room-auth-state-loss.test.ts +6 -0
  60. package/src/__tests__/room-handler-context.test.ts +0 -31
  61. package/src/__tests__/room-rate-limit-scopes.test.ts +1 -5
  62. package/src/__tests__/room-runtime-routing.test.ts +1 -111
  63. package/src/__tests__/smoke-skip-report.test.ts +1 -1
  64. package/src/durable-objects/room-runtime-base.ts +243 -17
  65. package/src/durable-objects/rooms-do.ts +190 -1345
  66. package/src/index.ts +97 -3
  67. package/src/lib/admin-assets.ts +5 -5
  68. package/src/lib/frontend-assets.ts +129 -0
  69. package/src/lib/frontend-config.ts +11 -0
  70. package/src/lib/openapi.ts +1 -4
  71. package/src/routes/room.ts +0 -285
  72. package/src/types.ts +1 -14
  73. package/admin-build/_app/immutable/chunks/Q3vAxeY-.js +0 -1
  74. package/admin-build/_app/immutable/chunks/SQVAC3Cv.js +0 -1
  75. package/admin-build/_app/immutable/entry/start.DY6YakU0.js +0 -1
  76. package/admin-build/_app/immutable/nodes/21.UVKBDvp4.js +0 -1
  77. package/src/__tests__/cloudflare-realtime.test.ts +0 -113
  78. package/src/lib/cloudflare-realtime.ts +0 -251
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { HonoEnv } from './lib/hono.js';
2
2
  import type { OpenApiSpec } from './lib/openapi.js';
3
3
  import type { Env } from './types.js';
4
+ import type { FrontendConfigLike } from './lib/frontend-config.js';
4
5
  import { ensureServerStartup } from './lib/runtime-startup.js';
5
6
 
6
7
  // ─── DO Re-exports (wrangler needs exports from main entry) ───
@@ -11,12 +12,14 @@ export { RoomsDO } from './durable-objects/rooms-do.js';
11
12
  export { LogsDO } from './durable-objects/logs-do.js';
12
13
 
13
14
  let appPromise: Promise<Awaited<ReturnType<typeof buildApp>>> | null = null;
15
+ const FRONTEND_ASSET_REDIRECT_STATUSES = new Set([301, 302, 307, 308]);
16
+ const FRONTEND_ASSET_REDIRECT_LIMIT = 4;
14
17
 
15
18
  function assetUnavailableMessage(
16
- assetName: 'admin dashboard' | 'harness assets',
19
+ assetName: 'admin dashboard' | 'frontend bundle' | 'harness assets',
17
20
  ): string {
18
21
  const label = `${assetName[0].toUpperCase()}${assetName.slice(1)}`;
19
- const verb = assetName === 'admin dashboard' ? 'is' : 'are';
22
+ const verb = assetName === 'harness assets' ? 'are' : 'is';
20
23
  return `${label} ${verb} not deployed for this worker. Deploy the assets bundle or configure ADMIN_ORIGIN if they are hosted elsewhere.`;
21
24
  }
22
25
 
@@ -56,6 +59,7 @@ async function buildApp() {
56
59
  analyticsRouteModule,
57
60
  adminAssetsModule,
58
61
  adminRoutingModule,
62
+ frontendAssetsModule,
59
63
  schemasModule,
60
64
  pluginMigrationsModule,
61
65
  pluginMigrationRoutingModule,
@@ -95,6 +99,7 @@ async function buildApp() {
95
99
  import('./routes/analytics-api.js'),
96
100
  import('./lib/admin-assets.js'),
97
101
  import('./lib/admin-routing.js'),
102
+ import('./lib/frontend-assets.js'),
98
103
  import('./lib/schemas.js'),
99
104
  import('./lib/plugin-migrations.js'),
100
105
  import('./lib/plugin-migration-routing.js'),
@@ -116,6 +121,7 @@ async function buildApp() {
116
121
  const { SERVER_VERSION } = versionModule;
117
122
  const { createAdminAssetRequest } = adminAssetsModule;
118
123
  const { resolveAdminFaviconTarget, resolveAdminRedirectTarget } = adminRoutingModule;
124
+ const { applyFrontendAssetHeaders, createFrontendAssetRequest } = frontendAssetsModule;
119
125
  const { zodDefaultHook } = schemasModule;
120
126
  const { executePluginMigrations } = pluginMigrationsModule;
121
127
  const { shouldRunPluginMigrationsForRequestPath } = pluginMigrationRoutingModule;
@@ -163,8 +169,75 @@ async function buildApp() {
163
169
  app.route('/admin/api', adminRouteModule.adminRoute);
164
170
  app.route('/admin/api/backup', backupRouteModule.backupRoute);
165
171
 
166
- app.get('/', (c) => {
172
+ function getFrontendConfig(env: Env): FrontendConfigLike | undefined {
173
+ return (doRouterModule.parseConfig(env) as { frontend?: FrontendConfigLike } | undefined)?.frontend;
174
+ }
175
+
176
+ async function fetchFrontendAssetResponse(
177
+ assetsBinding: { fetch(request: Request): Promise<Response> },
178
+ assetRequest: Request,
179
+ ): Promise<Response> {
180
+ let currentRequest = assetRequest;
181
+ const visitedUrls = new Set<string>();
182
+
183
+ for (let attempt = 0; attempt <= FRONTEND_ASSET_REDIRECT_LIMIT; attempt += 1) {
184
+ visitedUrls.add(currentRequest.url);
185
+ const assetResponse = await assetsBinding.fetch(currentRequest);
186
+ if (!FRONTEND_ASSET_REDIRECT_STATUSES.has(assetResponse.status)) {
187
+ return assetResponse;
188
+ }
189
+
190
+ const location = assetResponse.headers.get('location');
191
+ if (!location) {
192
+ return assetResponse;
193
+ }
194
+
195
+ const nextUrl = new URL(location, currentRequest.url);
196
+ if (nextUrl.origin !== new URL(currentRequest.url).origin) {
197
+ return assetResponse;
198
+ }
199
+
200
+ if (visitedUrls.has(nextUrl.toString())) {
201
+ return assetResponse;
202
+ }
203
+
204
+ currentRequest = new Request(nextUrl.toString(), currentRequest);
205
+ }
206
+
207
+ return assetsBinding.fetch(currentRequest);
208
+ }
209
+
210
+ async function serveFrontendAsset(c: { env: Env; req: { raw: Request } }): Promise<Response | null> {
211
+ const frontend = getFrontendConfig(c.env);
212
+ if (!frontend) {
213
+ return null;
214
+ }
215
+
216
+ if (!c.env.ASSETS) {
217
+ return new Response(
218
+ JSON.stringify({ code: 404, message: assetUnavailableMessage('frontend bundle') }),
219
+ {
220
+ status: 404,
221
+ headers: { 'content-type': 'application/json; charset=UTF-8' },
222
+ },
223
+ );
224
+ }
225
+
226
+ const assetRequest = createFrontendAssetRequest(c.req.raw, frontend);
227
+ if (!assetRequest) {
228
+ return null;
229
+ }
230
+
231
+ const assetResponse = await fetchFrontendAssetResponse(c.env.ASSETS, assetRequest);
232
+ return applyFrontendAssetHeaders(assetResponse, new URL(assetRequest.url).pathname);
233
+ }
234
+
235
+ app.get('/', async (c) => {
167
236
  const env = c.env as Env;
237
+ const frontendResponse = await serveFrontendAsset({ env, req: c.req });
238
+ if (frontendResponse) {
239
+ return frontendResponse;
240
+ }
168
241
  const externalAdminUrl = resolveAdminRedirectTarget(c.req.url, env.ADMIN_ORIGIN);
169
242
  if (externalAdminUrl) {
170
243
  return c.redirect(externalAdminUrl, 302);
@@ -181,6 +254,10 @@ async function buildApp() {
181
254
 
182
255
  app.get('/favicon.ico', async (c) => {
183
256
  const env = c.env as Env;
257
+ const frontendResponse = await serveFrontendAsset({ env, req: c.req });
258
+ if (frontendResponse) {
259
+ return frontendResponse;
260
+ }
184
261
  const externalFaviconUrl = resolveAdminFaviconTarget(env.ADMIN_ORIGIN);
185
262
  if (externalFaviconUrl) {
186
263
  return c.redirect(externalFaviconUrl, 302);
@@ -197,6 +274,10 @@ async function buildApp() {
197
274
 
198
275
  app.get('/favicon.svg', async (c) => {
199
276
  const env = c.env as Env;
277
+ const frontendResponse = await serveFrontendAsset({ env, req: c.req });
278
+ if (frontendResponse) {
279
+ return frontendResponse;
280
+ }
200
281
  const externalFaviconUrl = resolveAdminFaviconTarget(env.ADMIN_ORIGIN);
201
282
  if (externalFaviconUrl) {
202
283
  return c.redirect(externalFaviconUrl, 302);
@@ -275,6 +356,19 @@ async function buildApp() {
275
356
  return c.json(normalizeOpenApiDocument(spec as OpenApiSpec, new URL(c.req.url).origin));
276
357
  });
277
358
 
359
+ app.on(['GET', 'HEAD'], '*', async (c) => {
360
+ const env = c.env as Env;
361
+ const frontendResponse = await serveFrontendAsset({ env, req: c.req });
362
+ if (frontendResponse) {
363
+ return frontendResponse;
364
+ }
365
+
366
+ return c.json({
367
+ code: 404,
368
+ message: `Path '${new URL(c.req.url).pathname}' was not found on this EdgeBase server.`,
369
+ }, 404);
370
+ });
371
+
278
372
  app.notFound((c) => {
279
373
  return c.json({
280
374
  code: 404,
@@ -1,6 +1,6 @@
1
1
  export function resolveAdminAssetPath(pathname: string): string {
2
2
  if (pathname === '/admin' || pathname === '/admin/') {
3
- return '/';
3
+ return '/admin/index.html';
4
4
  }
5
5
 
6
6
  if (!pathname.startsWith('/admin/')) {
@@ -9,19 +9,19 @@ export function resolveAdminAssetPath(pathname: string): string {
9
9
 
10
10
  const assetPath = pathname.slice('/admin'.length) || '/';
11
11
  if (assetPath === '/' || assetPath === '') {
12
- return '/';
12
+ return '/admin/index.html';
13
13
  }
14
14
 
15
15
  if (assetPath.startsWith('/_app/')) {
16
- return assetPath;
16
+ return `/admin${assetPath}`;
17
17
  }
18
18
 
19
19
  const lastSegment = assetPath.split('/').pop() ?? '';
20
20
  if (lastSegment.includes('.')) {
21
- return assetPath;
21
+ return `/admin${assetPath}`;
22
22
  }
23
23
 
24
- return '/';
24
+ return '/admin/index.html';
25
25
  }
26
26
 
27
27
  export function createAdminAssetRequest(request: Request): Request {
@@ -0,0 +1,129 @@
1
+ import { normalizeFrontendMountPath, type FrontendConfigLike } from './frontend-config.js';
2
+
3
+ interface ResolveFrontendAssetPathOptions {
4
+ method?: string;
5
+ accept?: string | null;
6
+ mountPath?: string;
7
+ spaFallback?: boolean;
8
+ }
9
+
10
+ const HTML_ACCEPT_MARKERS = ['text/html', 'application/xhtml+xml'];
11
+ const HASHED_ASSET_PATTERN = /(?:^|[-._])[A-Za-z0-9]{8,}\.[A-Za-z0-9]+$/;
12
+
13
+ function isExplicitAssetPath(pathname: string): boolean {
14
+ const lastSegment = pathname.split('/').pop() ?? '';
15
+ return lastSegment.includes('.');
16
+ }
17
+
18
+ function isHtmlNavigationRequest(method: string | undefined, accept: string | null | undefined): boolean {
19
+ if (method && method !== 'GET' && method !== 'HEAD') {
20
+ return false;
21
+ }
22
+
23
+ if (!accept) {
24
+ return false;
25
+ }
26
+
27
+ return HTML_ACCEPT_MARKERS.some((marker) => accept.includes(marker));
28
+ }
29
+
30
+ function stripMountPath(pathname: string, mountPath: string): string | null {
31
+ if (mountPath === '/') {
32
+ return pathname || '/';
33
+ }
34
+
35
+ if (pathname === mountPath || pathname === `${mountPath}/`) {
36
+ return '/';
37
+ }
38
+
39
+ if (!pathname.startsWith(`${mountPath}/`)) {
40
+ return null;
41
+ }
42
+
43
+ return pathname.slice(mountPath.length) || '/';
44
+ }
45
+
46
+ export function resolveFrontendAssetPath(
47
+ pathname: string,
48
+ options: ResolveFrontendAssetPathOptions = {},
49
+ ): string | null {
50
+ const mountPath = normalizeFrontendMountPath(options.mountPath);
51
+ const relativePath = stripMountPath(pathname || '/', mountPath);
52
+ if (relativePath === null) {
53
+ return null;
54
+ }
55
+
56
+ const assetPrefix = mountPath === '/' ? '' : mountPath;
57
+
58
+ if (relativePath === '/' || relativePath === '') {
59
+ return `${assetPrefix}/index.html`;
60
+ }
61
+
62
+ const explicitAssetPath = `${assetPrefix}${relativePath}`;
63
+ if (isExplicitAssetPath(relativePath)) {
64
+ return explicitAssetPath;
65
+ }
66
+
67
+ if (options.spaFallback && isHtmlNavigationRequest(options.method, options.accept)) {
68
+ return `${assetPrefix}/index.html`;
69
+ }
70
+
71
+ return explicitAssetPath;
72
+ }
73
+
74
+ export function createFrontendAssetRequest(
75
+ request: Request,
76
+ config: FrontendConfigLike,
77
+ ): Request | null {
78
+ const url = new URL(request.url);
79
+ const pathname = resolveFrontendAssetPath(url.pathname, {
80
+ method: request.method,
81
+ accept: request.headers.get('accept'),
82
+ mountPath: config.mountPath,
83
+ spaFallback: config.spaFallback,
84
+ });
85
+
86
+ if (!pathname) {
87
+ return null;
88
+ }
89
+
90
+ url.pathname = pathname;
91
+ return new Request(url.toString(), request);
92
+ }
93
+
94
+ function getFrontendCacheControl(pathname: string): string | null {
95
+ const assetName = pathname.split('/').pop() ?? '';
96
+
97
+ if (assetName === 'index.html' || assetName === 'manifest.webmanifest' || assetName === 'sw.js') {
98
+ return 'no-cache';
99
+ }
100
+
101
+ if (HASHED_ASSET_PATTERN.test(assetName)) {
102
+ return 'public, max-age=31536000, immutable';
103
+ }
104
+
105
+ if (isExplicitAssetPath(pathname)) {
106
+ return 'public, max-age=300';
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ export function applyFrontendAssetHeaders(response: Response, pathname: string): Response {
113
+ if (!response.ok) {
114
+ return response;
115
+ }
116
+
117
+ const cacheControl = getFrontendCacheControl(pathname);
118
+ if (!cacheControl) {
119
+ return response;
120
+ }
121
+
122
+ const headers = new Headers(response.headers);
123
+ headers.set('Cache-Control', cacheControl);
124
+ return new Response(response.body, {
125
+ status: response.status,
126
+ statusText: response.statusText,
127
+ headers,
128
+ });
129
+ }
@@ -0,0 +1,11 @@
1
+ export interface FrontendConfigLike {
2
+ directory: string;
3
+ mountPath?: string;
4
+ spaFallback?: boolean;
5
+ }
6
+
7
+ export function normalizeFrontendMountPath(mountPath: string | undefined): string {
8
+ if (!mountPath) return '/';
9
+ if (mountPath === '/') return '/';
10
+ return mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath;
11
+ }
@@ -47,10 +47,7 @@ const USER_BEARER_PATHS = new Set([
47
47
  '/api/push/topic/unsubscribe',
48
48
  ]);
49
49
 
50
- const USER_BEARER_PREFIXES = [
51
- '/api/room/media/realtime/',
52
- '/api/room/media/cloudflare_realtimekit/',
53
- ];
50
+ const USER_BEARER_PREFIXES: string[] = [];
54
51
 
55
52
  const SERVICE_KEY_ONLY_PATHS = new Set([
56
53
  '/api/db/broadcast',
@@ -65,109 +65,6 @@ const roomSummaryCollectionSchema = z.object({
65
65
  deniedIds: z.array(z.string()),
66
66
  updatedAt: z.string(),
67
67
  });
68
- const roomRealtimeSessionDescriptionSchema = z.object({
69
- sdp: z.string().openapi({ description: 'WebRTC session description payload' }),
70
- type: z.enum(['offer', 'answer']).openapi({ description: 'Session description type' }),
71
- });
72
- const roomRealtimeTrackSchema = z.object({
73
- location: z.enum(['local', 'remote']).openapi({ description: 'Track direction relative to the caller' }),
74
- mid: z.string().optional().openapi({ description: 'WebRTC media ID' }),
75
- sessionId: z.string().optional().openapi({ description: 'Provider session ID associated with this track' }),
76
- trackName: z.string().optional().openapi({ description: 'Track name used by the provider' }),
77
- bidirectionalMediaStream: z.boolean().optional().openapi({ description: 'Whether the track should be bidirectional' }),
78
- kind: z.string().optional().openapi({ description: 'Track kind reported by the provider' }),
79
- simulcast: z.object({
80
- preferredRid: z.string().optional(),
81
- priorityOrdering: z.enum(['none', 'asciibetical']).optional(),
82
- ridNotAvailable: z.enum(['none', 'asciibetical']).optional(),
83
- }).optional().openapi({ description: 'Optional simulcast preferences' }),
84
- errorCode: z.string().optional().openapi({ description: 'Provider-level error code for this track' }),
85
- errorDescription: z.string().optional().openapi({ description: 'Provider-level error description for this track' }),
86
- });
87
- const roomRealtimeCreateSessionBodySchema = z.object({
88
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the realtime session to' }),
89
- correlationId: z.string().optional().openapi({ description: 'Optional provider correlation ID' }),
90
- thirdparty: z.boolean().optional().openapi({ description: 'Forward Cloudflare Realtime thirdparty mode' }),
91
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
92
- });
93
- const roomCloudflareRealtimeKitCreateSessionBodySchema = z.object({
94
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the Cloudflare RealtimeKit participant to' }),
95
- customParticipantId: z.string().optional().openapi({ description: 'Optional custom participant identifier for the provisioned RealtimeKit participant' }),
96
- name: z.string().optional().openapi({ description: 'Optional display name for the provisioned RealtimeKit participant' }),
97
- picture: z.string().optional().openapi({ description: 'Optional avatar URL for the provisioned RealtimeKit participant' }),
98
- });
99
- const roomRealtimeCreateSessionResponseSchema = z.object({
100
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
101
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
102
- errorCode: z.string().optional(),
103
- errorDescription: z.string().optional(),
104
- connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
105
- reused: z.boolean().optional().openapi({ description: 'Whether an existing provider session was reused' }),
106
- });
107
- const roomCloudflareRealtimeKitCreateSessionResponseSchema = z.object({
108
- sessionId: z.string().openapi({ description: 'Cloudflare RealtimeKit participant ID' }),
109
- meetingId: z.string().openapi({ description: 'Cloudflare RealtimeKit meeting ID backing the room session' }),
110
- participantId: z.string().openapi({ description: 'Cloudflare RealtimeKit participant ID' }),
111
- authToken: z.string().openapi({ description: 'RealtimeKit auth token for the provisioned participant' }),
112
- presetName: z.string().optional().openapi({ description: 'RealtimeKit preset used for the provisioned participant' }),
113
- connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
114
- reused: z.boolean().optional().openapi({ description: 'Whether an existing provider participant was reused' }),
115
- });
116
- const roomRealtimeSessionStateSchema = z.object({
117
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
118
- connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
119
- createdAt: z.number().openapi({ description: 'Unix epoch milliseconds when the session was created' }),
120
- updatedAt: z.number().openapi({ description: 'Unix epoch milliseconds when the session was last updated' }),
121
- });
122
- const roomRealtimeIceServerSchema = z.object({
123
- urls: z.union([z.array(z.string()), z.string()]).openapi({ description: 'ICE server URL or URL list' }),
124
- username: z.string().optional(),
125
- credential: z.string().optional(),
126
- });
127
- const roomRealtimeIceServersBodySchema = z.object({
128
- ttl: z.number().optional().openapi({ description: 'Requested TURN credential TTL in seconds' }),
129
- });
130
- const roomRealtimeIceServersResponseSchema = z.object({
131
- iceServers: z.array(roomRealtimeIceServerSchema).openapi({ description: 'ICE servers returned by Cloudflare TURN' }),
132
- });
133
- const roomRealtimeTracksResponseSchema = z.object({
134
- errorCode: z.string().optional(),
135
- errorDescription: z.string().optional(),
136
- requiresImmediateRenegotiation: z.boolean().optional(),
137
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
138
- tracks: z.array(roomRealtimeTrackSchema).optional(),
139
- });
140
- const roomRealtimeTracksBodySchema = z.object({
141
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
142
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the track operation to' }),
143
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
144
- tracks: z.array(roomRealtimeTrackSchema).min(1).openapi({ description: 'Tracks to create or subscribe to' }),
145
- autoDiscover: z.boolean().optional().openapi({ description: 'Ask the provider to auto-discover remote tracks' }),
146
- publish: z.object({
147
- kind: z.enum(['audio', 'video', 'screen']).optional(),
148
- trackId: z.string().optional(),
149
- deviceId: z.string().optional(),
150
- muted: z.boolean().optional(),
151
- }).optional().openapi({ description: 'Optional room media state updates to apply after track creation' }),
152
- });
153
- const roomRealtimeRenegotiateBodySchema = z.object({
154
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
155
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the renegotiation to' }),
156
- sessionDescription: roomRealtimeSessionDescriptionSchema,
157
- });
158
- const roomRealtimeCloseTracksBodySchema = z.object({
159
- sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
160
- connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the close operation to' }),
161
- sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
162
- tracks: z.array(z.object({
163
- mid: z.string().openapi({ description: 'Track MID to close' }),
164
- })).min(1).openapi({ description: 'Tracks to close' }),
165
- force: z.boolean().optional().openapi({ description: 'Force close even if the provider reports the track as active' }),
166
- unpublish: z.object({
167
- kind: z.enum(['audio', 'video', 'screen']).optional(),
168
- }).optional().openapi({ description: 'Optional room media state cleanup after closing tracks' }),
169
- });
170
-
171
68
  function isRoomOperationPublic(
172
69
  namespaceConfig: RoomNamespaceConfig | null | undefined,
173
70
  operation: 'metadata' | 'join' | 'action',
@@ -705,185 +602,3 @@ roomRoute.openapi(getRoomSummaries, async (c) => {
705
602
  updatedAt: new Date().toISOString(),
706
603
  });
707
604
  });
708
-
709
- const getRoomRealtimeSession = createRoute({
710
- operationId: 'getRoomRealtimeSession',
711
- method: 'get',
712
- path: '/media/realtime/session',
713
- tags: ['client'],
714
- summary: 'Get the active room realtime media session',
715
- description: 'Returns the provider session currently bound to the authenticated room member.',
716
- request: {
717
- query: roomQuerySchema.extend({
718
- connectionId: z.string().optional().openapi({ description: 'Optional room connection ID override' }),
719
- }),
720
- },
721
- responses: {
722
- 200: { description: 'Active room realtime session', content: { 'application/json': { schema: roomRealtimeSessionStateSchema } } },
723
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
724
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
725
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
726
- 404: { description: 'No active session or runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
727
- },
728
- });
729
-
730
- const createRoomRealtimeSession = createRoute({
731
- operationId: 'createRoomRealtimeSession',
732
- method: 'post',
733
- path: '/media/realtime/session',
734
- tags: ['client'],
735
- summary: 'Create a room realtime media session',
736
- description: 'Creates a Cloudflare Realtime session for the authenticated room member.',
737
- request: {
738
- query: roomQuerySchema,
739
- body: { content: { 'application/json': { schema: roomRealtimeCreateSessionBodySchema } }, required: false },
740
- },
741
- responses: {
742
- 200: { description: 'Realtime session created', content: { 'application/json': { schema: roomRealtimeCreateSessionResponseSchema } } },
743
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
744
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
745
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
746
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
747
- 409: { description: 'Conflicting existing published media', content: { 'application/json': { schema: errorResponseSchema } } },
748
- },
749
- });
750
-
751
- const createRoomCloudflareRealtimeKitSession = createRoute({
752
- operationId: 'createRoomCloudflareRealtimeKitSession',
753
- method: 'post',
754
- path: '/media/cloudflare_realtimekit/session',
755
- tags: ['client'],
756
- summary: 'Create a room Cloudflare RealtimeKit session',
757
- description: 'Creates a Cloudflare RealtimeKit session for the authenticated room member.',
758
- request: {
759
- query: roomQuerySchema,
760
- body: { content: { 'application/json': { schema: roomCloudflareRealtimeKitCreateSessionBodySchema } }, required: false },
761
- },
762
- responses: {
763
- 200: { description: 'Cloudflare RealtimeKit session created', content: { 'application/json': { schema: roomCloudflareRealtimeKitCreateSessionResponseSchema } } },
764
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
765
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
766
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
767
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
768
- 409: { description: 'Conflicting existing published media', content: { 'application/json': { schema: errorResponseSchema } } },
769
- },
770
- });
771
-
772
- const createRoomRealtimeIceServers = createRoute({
773
- operationId: 'createRoomRealtimeIceServers',
774
- method: 'post',
775
- path: '/media/realtime/turn',
776
- tags: ['client'],
777
- summary: 'Generate TURN / ICE credentials for room realtime media',
778
- description: 'Generates ICE server credentials for the authenticated room member.',
779
- request: {
780
- query: roomQuerySchema,
781
- body: { content: { 'application/json': { schema: roomRealtimeIceServersBodySchema } }, required: false },
782
- },
783
- responses: {
784
- 200: { description: 'ICE servers generated', content: { 'application/json': { schema: roomRealtimeIceServersResponseSchema } } },
785
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
786
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
787
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
788
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
789
- },
790
- });
791
-
792
- const addRoomRealtimeTracks = createRoute({
793
- operationId: 'addRoomRealtimeTracks',
794
- method: 'post',
795
- path: '/media/realtime/tracks/new',
796
- tags: ['client'],
797
- summary: 'Add realtime media tracks to a room session',
798
- description: 'Creates or subscribes realtime tracks for the authenticated room member.',
799
- request: {
800
- query: roomQuerySchema,
801
- body: { content: { 'application/json': { schema: roomRealtimeTracksBodySchema } }, required: true },
802
- },
803
- responses: {
804
- 200: { description: 'Realtime tracks updated', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
805
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
806
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
807
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
808
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
809
- },
810
- });
811
-
812
- const renegotiateRoomRealtimeSession = createRoute({
813
- operationId: 'renegotiateRoomRealtimeSession',
814
- method: 'put',
815
- path: '/media/realtime/renegotiate',
816
- tags: ['client'],
817
- summary: 'Renegotiate a room realtime media session',
818
- description: 'Submits a new session description for an existing room realtime media session.',
819
- request: {
820
- query: roomQuerySchema,
821
- body: { content: { 'application/json': { schema: roomRealtimeRenegotiateBodySchema } }, required: true },
822
- },
823
- responses: {
824
- 200: { description: 'Realtime session renegotiated', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
825
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
826
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
827
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
828
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
829
- },
830
- });
831
-
832
- const closeRoomRealtimeTracks = createRoute({
833
- operationId: 'closeRoomRealtimeTracks',
834
- method: 'put',
835
- path: '/media/realtime/tracks/close',
836
- tags: ['client'],
837
- summary: 'Close room realtime media tracks',
838
- description: 'Closes provider tracks for the authenticated room member and optionally unpublishes room media state.',
839
- request: {
840
- query: roomQuerySchema,
841
- body: { content: { 'application/json': { schema: roomRealtimeCloseTracksBodySchema } }, required: true },
842
- },
843
- responses: {
844
- 200: { description: 'Realtime tracks closed', content: { 'application/json': { schema: roomRealtimeTracksResponseSchema } } },
845
- 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
846
- 401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
847
- 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
848
- 404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
849
- },
850
- });
851
-
852
- roomRoute.openapi(getRoomRealtimeSession, async (c) =>
853
- proxyRoomDoRequest(c, '/media/realtime/session', 'GET', { requireAuth: true }));
854
-
855
- roomRoute.openapi(createRoomRealtimeSession, async (c) =>
856
- proxyRoomDoRequest(c, '/media/realtime/session', 'POST', {
857
- requireAuth: true,
858
- validatedJson: c.req.valid('json'),
859
- }));
860
-
861
- roomRoute.openapi(createRoomRealtimeIceServers, async (c) =>
862
- proxyRoomDoRequest(c, '/media/realtime/turn', 'POST', {
863
- requireAuth: true,
864
- validatedJson: c.req.valid('json'),
865
- }));
866
-
867
- roomRoute.openapi(addRoomRealtimeTracks, async (c) =>
868
- proxyRoomDoRequest(c, '/media/realtime/tracks/new', 'POST', {
869
- requireAuth: true,
870
- validatedJson: c.req.valid('json'),
871
- }));
872
-
873
- roomRoute.openapi(renegotiateRoomRealtimeSession, async (c) =>
874
- proxyRoomDoRequest(c, '/media/realtime/renegotiate', 'PUT', {
875
- requireAuth: true,
876
- validatedJson: c.req.valid('json'),
877
- }));
878
-
879
- roomRoute.openapi(closeRoomRealtimeTracks, async (c) =>
880
- proxyRoomDoRequest(c, '/media/realtime/tracks/close', 'PUT', {
881
- requireAuth: true,
882
- validatedJson: c.req.valid('json'),
883
- }));
884
-
885
- roomRoute.openapi(createRoomCloudflareRealtimeKitSession, async (c) =>
886
- proxyRoomDoRequest(c, '/media/cloudflare_realtimekit/session', 'POST', {
887
- requireAuth: true,
888
- validatedJson: c.req.valid('json'),
889
- }));
package/src/types.ts CHANGED
@@ -7,7 +7,7 @@ export interface Env {
7
7
  DATABASE: DurableObjectNamespace;
8
8
  AUTH: DurableObjectNamespace;
9
9
  DATABASE_LIVE: DurableObjectNamespace;
10
- /** Room DO — per-room state synchronization, members, signals, media */
10
+ /** Room DO — per-room state synchronization, members, and signals */
11
11
  ROOMS: DurableObjectNamespace;
12
12
 
13
13
  // ─── R2 Storage ───
@@ -66,19 +66,6 @@ export interface Env {
66
66
  TURNSTILE_SECRET?: string;
67
67
  /** Turnstile site key — public, returned to clients via GET /api/config (§34) */
68
68
  CAPTCHA_SITE_KEY?: string;
69
- /** Cloudflare Realtime app ID for SFU session control. */
70
- CF_REALTIME_APP_ID?: string;
71
- /** Cloudflare RealtimeKit preset name used when creating participant tokens. */
72
- CF_REALTIME_PRESET_NAME?: string;
73
- /** Cloudflare Realtime app secret for SFU session control. */
74
- CF_REALTIME_APP_SECRET?: string;
75
- /** Optional override for the Cloudflare Realtime API base URL. */
76
- CF_REALTIME_BASE_URL?: string;
77
- /** Cloudflare TURN key ID used to mint short-lived ICE credentials. */
78
- CF_REALTIME_TURN_KEY_ID?: string;
79
- /** Cloudflare TURN API token returned when the TURN key is created. */
80
- CF_REALTIME_TURN_API_TOKEN?: string;
81
-
82
69
  // ─── Environment Identification ───
83
70
  /** Server environment name for Service Key constraints.env evaluation */
84
71
  ENVIRONMENT?: string;