@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-base/server",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "EdgeBase runtime assets consumed by the EdgeBase CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  "jose": "^6.0.0",
35
35
  "pg": "^8.16.3",
36
36
  "zod": "^4.3.6",
37
- "@edge-base/core": "0.2.6",
38
- "@edge-base/shared": "0.2.6"
37
+ "@edge-base/core": "0.2.8",
38
+ "@edge-base/shared": "0.2.8"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@cloudflare/vitest-pool-workers": "^0.8.71",
@@ -8,18 +8,18 @@ import {
8
8
 
9
9
  describe('admin asset path resolution', () => {
10
10
  it('serves index for the admin root', () => {
11
- expect(resolveAdminAssetPath('/admin')).toBe('/');
12
- expect(resolveAdminAssetPath('/admin/')).toBe('/');
11
+ expect(resolveAdminAssetPath('/admin')).toBe('/admin/index.html');
12
+ expect(resolveAdminAssetPath('/admin/')).toBe('/admin/index.html');
13
13
  });
14
14
 
15
15
  it('strips the /admin prefix for built assets', () => {
16
- expect(resolveAdminAssetPath('/admin/_app/version.json')).toBe('/_app/version.json');
17
- expect(resolveAdminAssetPath('/admin/favicon.png')).toBe('/favicon.png');
16
+ expect(resolveAdminAssetPath('/admin/_app/version.json')).toBe('/admin/_app/version.json');
17
+ expect(resolveAdminAssetPath('/admin/favicon.png')).toBe('/admin/favicon.png');
18
18
  });
19
19
 
20
20
  it('falls back to index for client-side admin routes', () => {
21
- expect(resolveAdminAssetPath('/admin/login')).toBe('/');
22
- expect(resolveAdminAssetPath('/admin/database/tables')).toBe('/');
21
+ expect(resolveAdminAssetPath('/admin/login')).toBe('/admin/index.html');
22
+ expect(resolveAdminAssetPath('/admin/database/tables')).toBe('/admin/index.html');
23
23
  });
24
24
 
25
25
  it('rewrites requests without dropping the query string', () => {
@@ -27,7 +27,7 @@ describe('admin asset path resolution', () => {
27
27
  const rewritten = createAdminAssetRequest(request);
28
28
  const url = new URL(rewritten.url);
29
29
 
30
- expect(url.pathname).toBe('/');
30
+ expect(url.pathname).toBe('/admin/index.html');
31
31
  expect(url.search).toBe('?next=%2Fadmin%2Fdatabase');
32
32
  });
33
33
  });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ applyFrontendAssetHeaders,
4
+ createFrontendAssetRequest,
5
+ resolveFrontendAssetPath,
6
+ } from '../lib/frontend-assets.js';
7
+
8
+ describe('frontend asset path resolution', () => {
9
+ it('serves index.html for the root mount path', () => {
10
+ expect(resolveFrontendAssetPath('/', { mountPath: '/' })).toBe('/index.html');
11
+ });
12
+
13
+ it('serves index.html for custom mount roots', () => {
14
+ expect(resolveFrontendAssetPath('/app', { mountPath: '/app' })).toBe('/app/index.html');
15
+ expect(resolveFrontendAssetPath('/app/', { mountPath: '/app' })).toBe('/app/index.html');
16
+ });
17
+
18
+ it('keeps explicit asset paths intact', () => {
19
+ expect(resolveFrontendAssetPath('/assets/main.js', { mountPath: '/' })).toBe('/assets/main.js');
20
+ expect(resolveFrontendAssetPath('/app/assets/main.js', { mountPath: '/app' })).toBe('/app/assets/main.js');
21
+ });
22
+
23
+ it('falls back to index.html only for HTML navigation when enabled', () => {
24
+ expect(resolveFrontendAssetPath('/dashboard/settings', {
25
+ mountPath: '/',
26
+ spaFallback: true,
27
+ method: 'GET',
28
+ accept: 'text/html,application/xhtml+xml',
29
+ })).toBe('/index.html');
30
+
31
+ expect(resolveFrontendAssetPath('/dashboard/settings', {
32
+ mountPath: '/',
33
+ spaFallback: true,
34
+ method: 'GET',
35
+ accept: 'application/json',
36
+ })).toBe('/dashboard/settings');
37
+ });
38
+
39
+ it('returns null outside the configured mount path', () => {
40
+ expect(resolveFrontendAssetPath('/dashboard', { mountPath: '/app' })).toBeNull();
41
+ });
42
+
43
+ it('rewrites requests without dropping the query string', () => {
44
+ const request = new Request('http://localhost:8787/app/dashboard?tab=settings', {
45
+ headers: { accept: 'text/html' },
46
+ });
47
+ const rewritten = createFrontendAssetRequest(request, {
48
+ directory: './web/dist',
49
+ mountPath: '/app',
50
+ spaFallback: true,
51
+ });
52
+ const url = new URL(rewritten!.url);
53
+
54
+ expect(url.pathname).toBe('/app/index.html');
55
+ expect(url.search).toBe('?tab=settings');
56
+ });
57
+ });
58
+
59
+ describe('frontend cache headers', () => {
60
+ it('marks html and PWA entry files as no-cache', () => {
61
+ const response = applyFrontendAssetHeaders(new Response('ok'), '/index.html');
62
+ expect(response.headers.get('Cache-Control')).toBe('no-cache');
63
+
64
+ const manifest = applyFrontendAssetHeaders(new Response('ok'), '/manifest.webmanifest');
65
+ expect(manifest.headers.get('Cache-Control')).toBe('no-cache');
66
+ });
67
+
68
+ it('marks hashed assets as immutable and unhashed assets as short-lived', () => {
69
+ const hashed = applyFrontendAssetHeaders(new Response('ok'), '/assets/app-abc123def456.js');
70
+ expect(hashed.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable');
71
+
72
+ const plain = applyFrontendAssetHeaders(new Response('ok'), '/favicon.ico');
73
+ expect(plain.headers.get('Cache-Control')).toBe('public, max-age=300');
74
+ });
75
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeFrontendMountPath } from '../lib/frontend-config.js';
3
+
4
+ describe('frontend mount path normalization', () => {
5
+ it('defaults to the root mount path when unset', () => {
6
+ expect(normalizeFrontendMountPath(undefined)).toBe('/');
7
+ });
8
+
9
+ it('preserves the root mount path', () => {
10
+ expect(normalizeFrontendMountPath('/')).toBe('/');
11
+ });
12
+
13
+ it('trims a trailing slash from custom mount paths', () => {
14
+ expect(normalizeFrontendMountPath('/app/')).toBe('/app');
15
+ });
16
+ });
@@ -0,0 +1,200 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('cloudflare:workers', () => ({
4
+ DurableObject: class DurableObject {},
5
+ }));
6
+
7
+ afterEach(() => {
8
+ vi.resetModules();
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ function createExecutionContext(): ExecutionContext {
13
+ return {
14
+ waitUntil(promise: Promise<unknown>) {
15
+ void promise.catch(() => {});
16
+ },
17
+ passThroughOnException() {},
18
+ } as ExecutionContext;
19
+ }
20
+
21
+ function createEnv(overrides: Record<string, unknown> = {}): Record<string, unknown> {
22
+ const logsBinding = {
23
+ idFromName: vi.fn((name: string) => ({ toString: () => name })),
24
+ get: vi.fn(() => ({
25
+ fetch: vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 })),
26
+ })),
27
+ };
28
+
29
+ return {
30
+ DATABASE: {} as never,
31
+ AUTH: {} as never,
32
+ DATABASE_LIVE: {} as never,
33
+ ROOMS: {} as never,
34
+ LOGS: logsBinding as never,
35
+ STORAGE: {} as never,
36
+ KV: {} as never,
37
+ AUTH_DB: {} as never,
38
+ CONTROL_DB: {} as never,
39
+ EDGEBASE_CONFIG: {},
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ async function loadWorker() {
45
+ vi.doMock('../lib/runtime-startup.js', () => ({
46
+ ensureServerStartup: vi.fn().mockResolvedValue(undefined),
47
+ }));
48
+
49
+ const mod = await import('../index.js');
50
+ return mod.default;
51
+ }
52
+
53
+ describe('frontend routing', () => {
54
+ it('serves the frontend root when a root-mounted bundle is configured', async () => {
55
+ const assetsFetch = vi.fn(async (request: Request) => {
56
+ const pathname = new URL(request.url).pathname;
57
+ return new Response(`asset:${pathname}`, { status: 200 });
58
+ });
59
+ const worker = await loadWorker();
60
+
61
+ const response = await worker.fetch(
62
+ new Request('http://localhost:8787/'),
63
+ createEnv({
64
+ EDGEBASE_CONFIG: {
65
+ frontend: {
66
+ directory: './web/dist',
67
+ spaFallback: true,
68
+ },
69
+ },
70
+ ASSETS: { fetch: assetsFetch },
71
+ }) as never,
72
+ createExecutionContext(),
73
+ );
74
+
75
+ expect(response.status).toBe(200);
76
+ expect(await response.text()).toBe('asset:/index.html');
77
+ expect(response.headers.get('Cache-Control')).toBe('no-cache');
78
+ expect(assetsFetch).toHaveBeenCalledTimes(1);
79
+ });
80
+
81
+ it('follows same-origin asset redirects for canonical frontend index routes', async () => {
82
+ const assetPaths: string[] = [];
83
+ const assetsFetch = vi.fn(async (request: Request) => {
84
+ const pathname = new URL(request.url).pathname;
85
+ assetPaths.push(pathname);
86
+
87
+ if (pathname === '/index.html') {
88
+ return new Response(null, {
89
+ status: 307,
90
+ headers: { location: '/' },
91
+ });
92
+ }
93
+
94
+ return new Response('asset:/', { status: 200 });
95
+ });
96
+ const worker = await loadWorker();
97
+
98
+ const response = await worker.fetch(
99
+ new Request('http://localhost:8787/'),
100
+ createEnv({
101
+ EDGEBASE_CONFIG: {
102
+ frontend: {
103
+ directory: './web/dist',
104
+ spaFallback: true,
105
+ },
106
+ },
107
+ ASSETS: { fetch: assetsFetch },
108
+ }) as never,
109
+ createExecutionContext(),
110
+ );
111
+
112
+ expect(response.status).toBe(200);
113
+ expect(await response.text()).toBe('asset:/');
114
+ expect(response.headers.get('Cache-Control')).toBe('no-cache');
115
+ expect(assetPaths).toEqual(['/index.html', '/']);
116
+ });
117
+
118
+ it('applies SPA fallback only to HTML navigation routes', async () => {
119
+ const assetPaths: string[] = [];
120
+ const assetsFetch = vi.fn(async (request: Request) => {
121
+ const pathname = new URL(request.url).pathname;
122
+ assetPaths.push(pathname);
123
+ return pathname === '/index.html'
124
+ ? new Response('frontend-index', { status: 200 })
125
+ : new Response('missing', { status: 404 });
126
+ });
127
+ const worker = await loadWorker();
128
+
129
+ const htmlNavigation = await worker.fetch(
130
+ new Request('http://localhost:8787/dashboard/settings', {
131
+ headers: { accept: 'text/html,application/xhtml+xml' },
132
+ }),
133
+ createEnv({
134
+ EDGEBASE_CONFIG: {
135
+ frontend: {
136
+ directory: './web/dist',
137
+ spaFallback: true,
138
+ },
139
+ },
140
+ ASSETS: { fetch: assetsFetch },
141
+ }) as never,
142
+ createExecutionContext(),
143
+ );
144
+ const missingAsset = await worker.fetch(
145
+ new Request('http://localhost:8787/assets/missing.js', {
146
+ headers: { accept: 'text/html,application/xhtml+xml' },
147
+ }),
148
+ createEnv({
149
+ EDGEBASE_CONFIG: {
150
+ frontend: {
151
+ directory: './web/dist',
152
+ spaFallback: true,
153
+ },
154
+ },
155
+ ASSETS: { fetch: assetsFetch },
156
+ }) as never,
157
+ createExecutionContext(),
158
+ );
159
+
160
+ expect(await htmlNavigation.text()).toBe('frontend-index');
161
+ expect(htmlNavigation.status).toBe(200);
162
+ expect(missingAsset.status).toBe(404);
163
+ expect(assetPaths).toEqual(['/index.html', '/assets/missing.js']);
164
+ });
165
+
166
+ it('respects custom mount paths and leaves API routes to the worker', async () => {
167
+ const assetsFetch = vi.fn(async (request: Request) => {
168
+ const pathname = new URL(request.url).pathname;
169
+ return new Response(`asset:${pathname}`, { status: 200 });
170
+ });
171
+ const worker = await loadWorker();
172
+ const env = createEnv({
173
+ EDGEBASE_CONFIG: {
174
+ frontend: {
175
+ directory: './web/dist',
176
+ mountPath: '/app',
177
+ spaFallback: true,
178
+ },
179
+ },
180
+ ASSETS: { fetch: assetsFetch },
181
+ }) as never;
182
+
183
+ const mounted = await worker.fetch(
184
+ new Request('http://localhost:8787/app/dashboard', {
185
+ headers: { accept: 'text/html' },
186
+ }),
187
+ env,
188
+ createExecutionContext(),
189
+ );
190
+ const api = await worker.fetch(
191
+ new Request('http://localhost:8787/api/health'),
192
+ env,
193
+ createExecutionContext(),
194
+ );
195
+
196
+ expect(await mounted.text()).toBe('asset:/app/index.html');
197
+ expect(api.status).toBe(200);
198
+ expect(assetsFetch).toHaveBeenCalledTimes(1);
199
+ });
200
+ });
@@ -118,8 +118,6 @@ describe('OpenAPI route coverage', () => {
118
118
  '/api/sql': { post: {} },
119
119
  '/admin/api/setup': { get: {} },
120
120
  '/admin/api/data/users': { get: {} },
121
- '/api/room/media/realtime/session': { post: {} },
122
- '/api/room/media/cloudflare_realtimekit/session': { post: {} },
123
121
  },
124
122
  };
125
123
 
@@ -134,10 +132,6 @@ describe('OpenAPI route coverage', () => {
134
132
  expect(schemes).toHaveProperty('serviceKeyAuth');
135
133
  expect((normalized.paths?.['/api/auth/me'] as Record<string, { security?: unknown }>).get.security)
136
134
  .toEqual([{ userBearerAuth: [] }]);
137
- expect((normalized.paths?.['/api/room/media/realtime/session'] as Record<string, { security?: unknown }>).post.security)
138
- .toEqual([{ userBearerAuth: [] }]);
139
- expect((normalized.paths?.['/api/room/media/cloudflare_realtimekit/session'] as Record<string, { security?: unknown }>).post.security)
140
- .toEqual([{ userBearerAuth: [] }]);
141
135
  expect((normalized.paths?.['/api/sql'] as Record<string, { security?: unknown }>).post.security)
142
136
  .toEqual([{ serviceKeyAuth: [] }]);
143
137
  expect((normalized.paths?.['/admin/api/setup'] as Record<string, { security?: unknown }>).get.security)
@@ -53,6 +53,7 @@ describe('room auth-state loss recovery', () => {
53
53
  room._stateSaveAt = 33_333;
54
54
  room._emptyRoomCleanupAt = 44_444;
55
55
  room._stateTTLAlarmAt = 55_555;
56
+ room._socketHeartbeatCheckAt = 66_666;
56
57
  room.ctx = {
57
58
  storage: {
58
59
  put: putSpy,
@@ -72,6 +73,7 @@ describe('room auth-state loss recovery', () => {
72
73
  stateSaveAt: 33_333,
73
74
  emptyRoomCleanupAt: 44_444,
74
75
  stateTTLAlarmAt: 55_555,
76
+ socketHeartbeatCheckAt: 66_666,
75
77
  });
76
78
  });
77
79
 
@@ -112,6 +114,7 @@ describe('room auth-state loss recovery', () => {
112
114
  room._stateTTLAlarmAt = null;
113
115
  room._metadata = {};
114
116
  room.config = {};
117
+ room.env = {};
115
118
  room.ctx = {
116
119
  getWebSockets: vi.fn(() => []),
117
120
  };
@@ -132,6 +135,7 @@ describe('room auth-state loss recovery', () => {
132
135
 
133
136
  const room: any = Object.create(RoomRuntimeBaseDO.prototype);
134
137
  room._metaCache = new Map();
138
+ room._attachmentExtraCache = new Map();
135
139
  room.ctx = {
136
140
  getTags: vi.fn(() => [
137
141
  'conn:conn-1',
@@ -171,6 +175,7 @@ describe('room auth-state loss recovery', () => {
171
175
  authStateLost: false,
172
176
  connectionId: 'conn-1',
173
177
  }]]);
178
+ room._attachmentExtraCache = new Map();
174
179
  room.safeSend = vi.fn();
175
180
 
176
181
  await room.webSocketMessage(ws, JSON.stringify({ type: 'ping' }));
@@ -193,6 +198,7 @@ describe('room auth-state loss recovery', () => {
193
198
  authStateLost: true,
194
199
  connectionId: 'conn-1',
195
200
  }]]);
201
+ room._attachmentExtraCache = new Map();
196
202
  room.safeSend = vi.fn();
197
203
 
198
204
  await room.webSocketMessage(ws, JSON.stringify({ type: 'ping' }));
@@ -127,35 +127,4 @@ describe('RoomsDO handler context', () => {
127
127
  }),
128
128
  );
129
129
  }, 15_000);
130
-
131
- it('returns 409 when creating a Cloudflare RealtimeKit session while media is already published', async () => {
132
- const { RoomsDO } = await import('../durable-objects/rooms-do.js');
133
-
134
- const room: any = Object.create(RoomsDO.prototype);
135
- room.readJsonBody = vi.fn().mockResolvedValue({ connectionId: 'conn-1' });
136
- room.authenticateRealtimeRequest = vi.fn().mockResolvedValue({
137
- memberId: 'member-1',
138
- connectionId: 'conn-1',
139
- meta: {
140
- authenticated: true,
141
- connectionId: 'conn-1',
142
- },
143
- });
144
- room.hasPublishedTracks = vi.fn().mockReturnValue(true);
145
-
146
- const response = await room.handleCloudflareRealtimeKitSessionCreate(
147
- new Request('http://do/media/cloudflare_realtimekit/session?room=game::room-1', {
148
- method: 'POST',
149
- body: JSON.stringify({ connectionId: 'conn-1' }),
150
- headers: { 'Content-Type': 'application/json' },
151
- }),
152
- new URL('http://do/media/cloudflare_realtimekit/session?room=game::room-1'),
153
- );
154
-
155
- expect(response.status).toBe(409);
156
- await expect(response.json()).resolves.toEqual({
157
- code: 409,
158
- message: 'Unpublish existing room media before creating a new Cloudflare RealtimeKit session',
159
- });
160
- });
161
130
  });
@@ -5,7 +5,7 @@ vi.mock('cloudflare:workers', () => ({
5
5
  }));
6
6
 
7
7
  describe('room rate-limit scopes', () => {
8
- it('keeps signal/media/admin buckets independent per connection', async () => {
8
+ it('keeps signal/admin buckets independent per connection', async () => {
9
9
  const { RoomRuntimeBaseDO } = await import('../durable-objects/room-runtime-base.js');
10
10
 
11
11
  const room: any = Object.create(RoomRuntimeBaseDO.prototype);
@@ -13,7 +13,6 @@ describe('room rate-limit scopes', () => {
13
13
  rateLimit: {
14
14
  actions: 2,
15
15
  signals: 4,
16
- media: 1,
17
16
  admin: 1,
18
17
  },
19
18
  };
@@ -25,9 +24,6 @@ describe('room rate-limit scopes', () => {
25
24
  expect(room.checkRateLimit('conn-1', 'signals')).toBe(true);
26
25
  expect(room.checkRateLimit('conn-1', 'signals')).toBe(false);
27
26
 
28
- expect(room.checkRateLimit('conn-1', 'media')).toBe(true);
29
- expect(room.checkRateLimit('conn-1', 'media')).toBe(false);
30
-
31
27
  expect(room.checkRateLimit('conn-1', 'admin')).toBe(true);
32
28
  expect(room.checkRateLimit('conn-1', 'admin')).toBe(false);
33
29
 
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
2
  import { defineConfig } from '@edge-base/shared';
3
3
  import { OpenAPIHono, type HonoEnv } from '../lib/hono.js';
4
4
  import { setConfig } from '../lib/do-router.js';
@@ -33,21 +33,6 @@ function createRoomApp() {
33
33
  return app;
34
34
  }
35
35
 
36
- function createAuthedRoomApp() {
37
- const app = new OpenAPIHono<HonoEnv>();
38
- app.use('/api/*', async (c, next) => {
39
- c.set('auth', {
40
- id: 'user-1',
41
- role: 'user',
42
- isAnonymous: false,
43
- meta: {},
44
- });
45
- await next();
46
- });
47
- app.route('/api/room', roomRoute);
48
- return app;
49
- }
50
-
51
36
  describe('room runtime selection', () => {
52
37
  afterEach(() => {
53
38
  setConfig({});
@@ -195,99 +180,4 @@ describe('room route runtime routing', () => {
195
180
  });
196
181
  });
197
182
 
198
- it('routes room media requests to the rooms runtime', async () => {
199
- setConfig(defineConfig({
200
- rooms: {
201
- game: {
202
- runtime: {
203
- target: 'rooms',
204
- },
205
- },
206
- },
207
- }));
208
-
209
- const doFetch = vi.fn(async (request: Request) => new Response(JSON.stringify({
210
- runtime: 'rooms',
211
- path: new URL(request.url).pathname,
212
- auth: request.headers.get('Authorization'),
213
- body: await request.clone().json(),
214
- }), {
215
- headers: { 'Content-Type': 'application/json' },
216
- status: 201,
217
- }));
218
-
219
- const env = createRoomRuntimeEnv();
220
- env.ROOMS = {
221
- idFromName: (name: string) => name as unknown as DurableObjectId,
222
- get: () => ({ fetch: doFetch }),
223
- } as unknown as DurableObjectNamespace;
224
-
225
- const app = createAuthedRoomApp();
226
- const response = await app.request('/api/room/media/realtime/session?namespace=game&id=room-1', {
227
- method: 'POST',
228
- headers: {
229
- Authorization: 'Bearer room-token',
230
- 'Content-Type': 'application/json',
231
- },
232
- body: JSON.stringify({ connectionId: 'conn-1' }),
233
- }, env);
234
-
235
- expect(response.status).toBe(201);
236
- await expect(response.json()).resolves.toMatchObject({
237
- runtime: 'rooms',
238
- path: '/media/realtime/session',
239
- auth: 'Bearer room-token',
240
- body: { connectionId: 'conn-1' },
241
- });
242
- expect(doFetch).toHaveBeenCalledTimes(1);
243
- expect(new URL((doFetch.mock.calls[0][0] as Request).url).searchParams.get('room')).toBe('game::room-1');
244
- });
245
-
246
- it('routes room cloudflare realtimekit session requests to the rooms runtime', async () => {
247
- setConfig(defineConfig({
248
- rooms: {
249
- game: {
250
- runtime: {
251
- target: 'rooms',
252
- },
253
- },
254
- },
255
- }));
256
-
257
- const doFetch = vi.fn(async (request: Request) => new Response(JSON.stringify({
258
- runtime: 'rooms',
259
- path: new URL(request.url).pathname,
260
- auth: request.headers.get('Authorization'),
261
- body: await request.clone().json(),
262
- }), {
263
- headers: { 'Content-Type': 'application/json' },
264
- status: 201,
265
- }));
266
-
267
- const env = createRoomRuntimeEnv();
268
- env.ROOMS = {
269
- idFromName: (name: string) => name as unknown as DurableObjectId,
270
- get: () => ({ fetch: doFetch }),
271
- } as unknown as DurableObjectNamespace;
272
-
273
- const app = createAuthedRoomApp();
274
- const response = await app.request('/api/room/media/cloudflare_realtimekit/session?namespace=game&id=room-1', {
275
- method: 'POST',
276
- headers: {
277
- Authorization: 'Bearer room-token',
278
- 'Content-Type': 'application/json',
279
- },
280
- body: JSON.stringify({ connectionId: 'conn-1' }),
281
- }, env);
282
-
283
- expect(response.status).toBe(201);
284
- await expect(response.json()).resolves.toMatchObject({
285
- runtime: 'rooms',
286
- path: '/media/cloudflare_realtimekit/session',
287
- auth: 'Bearer room-token',
288
- body: { connectionId: 'conn-1' },
289
- });
290
- expect(doFetch).toHaveBeenCalledTimes(1);
291
- expect(new URL((doFetch.mock.calls[0][0] as Request).url).searchParams.get('room')).toBe('game::room-1');
292
- });
293
183
  });
@@ -38,7 +38,7 @@ describe('smoke skip report', () => {
38
38
  {
39
39
  "skippedRouteCount": 0,
40
40
  "summaryByReason": {},
41
- "totalRoutes": 197,
41
+ "totalRoutes": 190,
42
42
  }
43
43
  `);
44
44
  });