@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.
- package/admin-build/_app/immutable/chunks/{CbfX3ELZ.js → B9efkx2V.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CLHN9MVr.js → BMXWUTG-.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DemDWbs-.js → Bt4AyT3o.js} +3 -3
- package/admin-build/_app/immutable/chunks/CKVjMXZi.js +1 -0
- package/admin-build/_app/immutable/chunks/{BvoGcDFV.js → CMYgGhZR.js} +1 -1
- package/admin-build/_app/immutable/chunks/{LL3ulaxa.js → CTRjWhGs.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BN_-k-Ck.js → CwyE59Yt.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DQVP4KC-.js → D8aeTKry.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Ff90owjx.js → DGAHkap7.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CR37B8DX.js → DPgR4-0v.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DdvsFblq.js → DYtrHeVQ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CrwlCAM0.js → DcVb45Ds.js} +1 -1
- package/admin-build/_app/immutable/chunks/Djnkhy-S.js +1 -0
- package/admin-build/_app/immutable/chunks/{DmDTovpg.js → fPy6xmgG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CCUxCptE.js → j4jxnAKj.js} +1 -1
- package/admin-build/_app/immutable/chunks/{qBm6xof8.js → zl2AUKMP.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.CP83Ni80.js → app.Cmz0WjMl.js} +2 -2
- package/admin-build/_app/immutable/entry/start.JE7dcbK1.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DiRq7puO.js → 0.y6D_QyUb.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.BFeyKLGT.js → 1.CndRxhbH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.zcee7hJx.js → 10.CdA5FmXy.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.BW7wLs2Y.js → 11.DG8SzMp_.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.CxJRlYSd.js → 12.CvmQqpFa.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.pp0F_5hn.js → 13.BbGNdswT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.t3AfGiGo.js → 14.CZKsN7-O.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B3agc7NX.js → 15.A7-CYgkG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.C4uG2-i8.js → 16.hgJT9H-x.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.CwGxi1Bn.js → 17.DkWZbcN2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.CrQyN_gU.js → 18.sX3Fb5gh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.NEPUOXl7.js → 19.VAZUW-1K.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.DGHO8ipr.js → 20.DkIKxacG.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.DOjJlQKc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.Dri5It7a.js → 22.BDaHvtaw.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.BPQP_Zte.js → 23.BVRzw_pD.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.D580FdSS.js → 24.CVhSJyG0.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.BMNPOZwF.js → 25.Bme-9bZn.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.XcpEcbiz.js → 26.Dsx7RIIs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.C1zHHcYv.js → 27.DMGQnzFM.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.CuKzzrY8.js → 28.GGwFmEhZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.nLpBMXnM.js → 29.Dnghr0nk.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.5G_aseoL.js → 3.Cg7zZJP1.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.CQC4nLoU.js → 30.C0J24z3I.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.Bet8kxOK.js → 31.MdxFI8v6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.nmJDYJpC.js → 4.DCAOVzGE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.CnbYLG4E.js → 5.DzUQ-cTc.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.KA01b-3y.js → 6.CptBYTVj.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.CP9fkn1L.js → 7.DfeeQ0Rg.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.BTzDb---.js → 8.CIcvctW7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.DkNJg_J6.js → 9.QKrvq4RA.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/openapi.json +6 -1941
- package/package.json +3 -3
- package/src/__tests__/admin-assets.test.ts +7 -7
- package/src/__tests__/frontend-assets.test.ts +75 -0
- package/src/__tests__/frontend-config.test.ts +16 -0
- package/src/__tests__/frontend-routing.test.ts +200 -0
- package/src/__tests__/openapi-coverage.test.ts +0 -6
- package/src/__tests__/room-auth-state-loss.test.ts +6 -0
- package/src/__tests__/room-handler-context.test.ts +0 -31
- package/src/__tests__/room-rate-limit-scopes.test.ts +1 -5
- package/src/__tests__/room-runtime-routing.test.ts +1 -111
- package/src/__tests__/smoke-skip-report.test.ts +1 -1
- package/src/durable-objects/room-runtime-base.ts +243 -17
- package/src/durable-objects/rooms-do.ts +190 -1345
- package/src/index.ts +97 -3
- package/src/lib/admin-assets.ts +5 -5
- package/src/lib/frontend-assets.ts +129 -0
- package/src/lib/frontend-config.ts +11 -0
- package/src/lib/openapi.ts +1 -4
- package/src/routes/room.ts +0 -285
- package/src/types.ts +1 -14
- package/admin-build/_app/immutable/chunks/Q3vAxeY-.js +0 -1
- package/admin-build/_app/immutable/chunks/SQVAC3Cv.js +0 -1
- package/admin-build/_app/immutable/entry/start.DY6YakU0.js +0 -1
- package/admin-build/_app/immutable/nodes/21.UVKBDvp4.js +0 -1
- package/src/__tests__/cloudflare-realtime.test.ts +0 -113
- 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.
|
|
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.
|
|
38
|
-
"@edge-base/shared": "0.2.
|
|
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/
|
|
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
|
|
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
|
});
|