@edge-base/server 0.2.7 → 0.2.9
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/{CIOC1v_q.js → 0M5txo3l.js} +3 -3
- package/admin-build/_app/immutable/chunks/{ejoEf2I5.js → 39YMyjjq.js} +1 -1
- package/admin-build/_app/immutable/chunks/B0zJ5_Wk.js +1 -0
- package/admin-build/_app/immutable/chunks/{BhCO1Fpt.js → BN-pHKcH.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BEM1BeVF.js → BfhqI0W8.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DnpbvAPi.js → C5nMidnK.js} +1 -1
- package/admin-build/_app/immutable/chunks/{iEyeblJR.js → CI1lKBjB.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dz9cUCuv.js → CPLcjNR9.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BfpUQYr3.js → DBkor_jM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Tea2dBJ8.js → DBniE7GN.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BYyykAbh.js → DUhE3Ynq.js} +1 -1
- package/admin-build/_app/immutable/chunks/DZo4s3wn.js +1 -0
- package/admin-build/_app/immutable/chunks/{BDYewzou.js → DllVADtt.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CvczjTXx.js → DncT8xH5.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DaXO-sFP.js → YGUb3X-Q.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D1u3u7xu.js → _xa30BOD.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.DoUaxnew.js → app.BW-Ac3jY.js} +2 -2
- package/admin-build/_app/immutable/entry/start.Bcf0SjPV.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.Dsxi8s7i.js → 0.CRSQPtos.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.Cp2l-hol.js → 1.HmStNbZP.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.4oY6m8Nz.js → 10.BVlz13nr.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.DfcozD4J.js → 11.VwQdbWwH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.uJgZdCIA.js → 12.BFAAgi1f.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.CaN1kRev.js → 13.DDdauC84.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.DQ5xIi3s.js → 14.D_rXaafJ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B_EkebTJ.js → 15.rfFjjiZc.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Tko1ZX8-.js → 16.DDEpGJ4Z.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.BCmWMJX9.js → 17.ChQWNtRN.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.hmGhl1O2.js → 18.Rir_JwlF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.D-1infOo.js → 19.CF76nxk1.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.CY4KKcBL.js → 20.JQer3KPM.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.CzGYxjpu.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.14Vd7bnt.js → 22.DUw7X6z_.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.Be6jK77o.js → 23.DS3svA-a.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CSTFkr6R.js → 24.BYOsQM6B.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.DRTg8fHc.js → 25.n0pyDBRv.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DKt-9lwQ.js → 26.C-Oo8sYK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.D5caPu0F.js → 27.BfxbFNkk.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.hJhlnlyY.js → 28.DyHSN1q7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.CDYBzFyT.js → 29.CUAmZbCc.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.DMyKwkGn.js → 3.KQm4VJJg.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.BaHNeEmc.js → 30.CQdPre5F.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.C6PV5L-2.js → 31.BP6raetq.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.9E118Ftm.js → 4.Glf2jJHB.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.D8guAl3v.js → 5.BdFtj_-X.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.D1u__DtT.js → 6.JfnwWq8s.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.DWXHnRFf.js → 7.C-rw9qDh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.Dojd8krc.js → 8.CA7r1Sjs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.CLtrr0K_.js → 9.B2jgJDkH.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- 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/durable-objects/room-runtime-base.ts +80 -6
- 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/admin-build/_app/immutable/chunks/BaUG2TJ-.js +0 -1
- package/admin-build/_app/immutable/chunks/qKdzaeX3.js +0 -1
- package/admin-build/_app/immutable/entry/start.MmZh8oBH.js +0 -1
- package/admin-build/_app/immutable/nodes/21.B9lbNUQr.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/
|
|
1
|
+
import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/DncT8xH5.js";import{a as F,f as I}from"../chunks/DEELgv7K.js";import{d as Y,s as Z}from"../chunks/CjcrXziO.js";import{P as tt}from"../chunks/BkZCgsc3.js";import{a as et}from"../chunks/Q2nPFxS6.js";import{M as _,T as at}from"../chunks/YGUb3X-Q.js";import{D as rt,T as nt}from"../chunks/DBkor_jM.js";var st=I("<button> </button>"),ot=I('<div class="range-selector"></div>'),lt=I('<div class="analytics-grid"><!> <!> <!> <!></div> <div class="analytics-chart"><!></div> <div class="analytics-bottom"><!> <!></div>',1);function yt(w,A){j(A,!0);let s=D(!0),o=D(null),p=D("24h");const M=[{value:"1h",label:"1H"},{value:"6h",label:"6H"},{value:"24h",label:"24H"},{value:"7d",label:"7D"},{value:"30d",label:"30D"}],T=n(()=>()=>{switch(t(p)){case"1h":return"minute";case"6h":case"24h":return"hour";default:return"day"}});async function $(){try{const e=await Q.fetch(`data/analytics?range=${t(p)}&category=function&metric=overview&groupBy=${t(T)()}`);y(o,e,!0)}catch(e){V(Y(e,"Failed to load functions analytics."))}finally{y(s,!1)}}function k(e){y(p,e,!0),y(s,!0),$()}U(()=>{$();const e=setInterval($,3e4);return()=>clearInterval(e)});const H=n(()=>()=>{var c;if(!((c=t(o))!=null&&c.summary))return 0;const{totalRequests:e,totalErrors:a}=t(o).summary;return e>0?a/e*100:0}),B=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.timeSeries)||[]).map(a=>({timestamp:a.timestamp,value:a.requests??a.value??0}))}),C=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.breakdown)||[]).map(a=>({label:a.label||"other",value:a.count}))}),E=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.topItems)||[]).map(a=>({label:a.label,count:a.count,secondary:`${Math.round(a.avgLatency)}ms · ${a.errorRate.toFixed(1)}% 5xx`}))});tt(w,{title:"Functions Analytics",description:"Serverless function execution and performance metrics",get docsHref(){return et},actions:a=>{var c=ot();W(c,21,()=>M,X,(f,d)=>{var l=st();let v;var h=b(l,!0);g(l),J(()=>{v=Z(l,1,"range-btn",null,v,{"range-btn--active":t(p)===t(d).value}),N(h,t(d).label)}),O("click",l,()=>k(t(d).value)),F(f,l)}),g(c),F(a,c)},children:(a,c)=>{var f=lt(),d=G(f),l=b(d);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.totalRequests)??0});_(l,{label:"Invocations",get value(){return t(r)},get loading(){return t(s)}})}var v=m(l,2);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.uniqueUsers)??0});_(v,{label:"Unique Users",get value(){return t(r)},get loading(){return t(s)}})}var h=m(v,2);{let r=n(()=>t(H)().toFixed(1));_(h,{label:"5xx Rate",get value(){return t(r)},unit:"%",get loading(){return t(s)}})}var L=m(h,2);{let r=n(()=>{var i,u;return Math.round(((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.avgLatency)??0)});_(L,{label:"Avg Duration",get value(){return t(r)},unit:"ms",get loading(){return t(s)}})}g(d);var x=m(d,2),P=b(x);{let r=n(()=>t(B)());at(P,{get data(){return t(r)},type:"bar",height:240,label:"Function invocations over time",get loading(){return t(s)},color:"#dc2626"})}g(x);var R=m(x,2),q=b(R);{let r=n(()=>t(C)());rt(q,{get items(){return t(r)},title:"Functions by invocation count",get loading(){return t(s)}})}var S=m(q,2);{let r=n(()=>t(E)());nt(S,{get items(){return t(r)},title:"Top Functions",get loading(){return t(s)}})}g(R),F(a,f)}}),z()}K(["click"]);export{yt as component};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"
|
|
1
|
+
{"version":"1775689125712"}
|
package/admin-build/index.html
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>EdgeBase Admin</title>
|
|
7
7
|
<link rel="icon" href="/admin/favicon.svg" type="image/svg+xml" />
|
|
8
|
-
<link href="/admin/_app/immutable/entry/start.
|
|
9
|
-
<link href="/admin/_app/immutable/chunks/
|
|
8
|
+
<link href="/admin/_app/immutable/entry/start.Bcf0SjPV.js" rel="modulepreload">
|
|
9
|
+
<link href="/admin/_app/immutable/chunks/B0zJ5_Wk.js" rel="modulepreload">
|
|
10
10
|
<link href="/admin/_app/immutable/chunks/BdTBlfLy.js" rel="modulepreload">
|
|
11
11
|
<link href="/admin/_app/immutable/chunks/Bn2NtlTj.js" rel="modulepreload">
|
|
12
|
-
<link href="/admin/_app/immutable/chunks/
|
|
13
|
-
<link href="/admin/_app/immutable/entry/app.
|
|
12
|
+
<link href="/admin/_app/immutable/chunks/DZo4s3wn.js" rel="modulepreload">
|
|
13
|
+
<link href="/admin/_app/immutable/entry/app.BW-Ac3jY.js" rel="modulepreload">
|
|
14
14
|
<link href="/admin/_app/immutable/chunks/B2bEC_Hm.js" rel="modulepreload">
|
|
15
15
|
<link href="/admin/_app/immutable/chunks/Bb0e0sAP.js" rel="modulepreload">
|
|
16
16
|
<link href="/admin/_app/immutable/chunks/DtZk82gG.js" rel="modulepreload">
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<div style="display: contents">
|
|
27
27
|
<script>
|
|
28
28
|
{
|
|
29
|
-
|
|
29
|
+
__sveltekit_1iz3xys = {
|
|
30
30
|
base: "/admin",
|
|
31
31
|
assets: "/admin"
|
|
32
32
|
};
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
const element = document.currentScript.parentElement;
|
|
35
35
|
|
|
36
36
|
Promise.all([
|
|
37
|
-
import("/admin/_app/immutable/entry/start.
|
|
38
|
-
import("/admin/_app/immutable/entry/app.
|
|
37
|
+
import("/admin/_app/immutable/entry/start.Bcf0SjPV.js"),
|
|
38
|
+
import("/admin/_app/immutable/entry/app.BW-Ac3jY.js")
|
|
39
39
|
]).then(([kit, app]) => {
|
|
40
40
|
kit.start(app, element);
|
|
41
41
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edge-base/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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.9",
|
|
38
|
+
"@edge-base/shared": "0.2.9"
|
|
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
|
+
});
|
|
@@ -52,6 +52,33 @@ export interface RoomWSMeta {
|
|
|
52
52
|
lastSeenAt?: number;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function normalizeAnonymousRoomAuthPayload(value: unknown): SharedAuthContext | null {
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const typed = value as Record<string, unknown>;
|
|
61
|
+
if (typed.type !== 'anonymous' || typeof typed.subject !== 'string' || typed.subject.trim().length === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const subject = typed.subject.trim();
|
|
66
|
+
return {
|
|
67
|
+
id: subject,
|
|
68
|
+
role: typeof typed.role === 'string' && typed.role.trim().length > 0 ? typed.role.trim() : undefined,
|
|
69
|
+
email: typeof typed.email === 'string' && typed.email.trim().length > 0 ? typed.email.trim() : undefined,
|
|
70
|
+
isAnonymous: typed.isAnonymous !== false,
|
|
71
|
+
custom:
|
|
72
|
+
typed.custom && typeof typed.custom === 'object' && !Array.isArray(typed.custom)
|
|
73
|
+
? (typed.custom as Record<string, unknown>)
|
|
74
|
+
: undefined,
|
|
75
|
+
meta:
|
|
76
|
+
typed.meta && typeof typed.meta === 'object' && !Array.isArray(typed.meta)
|
|
77
|
+
? (typed.meta as Record<string, unknown>)
|
|
78
|
+
: undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
interface PersistedRoomWSAttachment {
|
|
56
83
|
version: 1;
|
|
57
84
|
meta: RoomWSMeta;
|
|
@@ -489,7 +516,12 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
489
516
|
|
|
490
517
|
// Auth must be first
|
|
491
518
|
if (type === 'auth') {
|
|
492
|
-
await this.handleAuth(
|
|
519
|
+
await this.handleAuth(
|
|
520
|
+
ws,
|
|
521
|
+
meta,
|
|
522
|
+
typeof msg.token === 'string' ? msg.token : undefined,
|
|
523
|
+
msg.authPayload,
|
|
524
|
+
);
|
|
493
525
|
return;
|
|
494
526
|
}
|
|
495
527
|
|
|
@@ -765,15 +797,53 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
765
797
|
|
|
766
798
|
// ─── Auth Handler ───
|
|
767
799
|
|
|
768
|
-
private async handleAuth(
|
|
800
|
+
private async handleAuth(
|
|
801
|
+
ws: WebSocket,
|
|
802
|
+
meta: RoomWSMeta,
|
|
803
|
+
token?: string,
|
|
804
|
+
authPayload?: unknown,
|
|
805
|
+
): Promise<void> {
|
|
769
806
|
const isReAuth = meta.authenticated;
|
|
770
807
|
|
|
771
|
-
|
|
808
|
+
const anonymousAuth = !token ? normalizeAnonymousRoomAuthPayload(authPayload) : null;
|
|
809
|
+
if (!token && !anonymousAuth) {
|
|
772
810
|
this.safeSend(ws, { type: 'error', code: 'AUTH_FAILED', message: 'Token required' });
|
|
773
811
|
ws.close(4002, 'Authentication failed');
|
|
774
812
|
return;
|
|
775
813
|
}
|
|
776
814
|
|
|
815
|
+
if (anonymousAuth) {
|
|
816
|
+
meta.authenticated = true;
|
|
817
|
+
meta.authStateLost = false;
|
|
818
|
+
meta.lastSeenAt = Date.now();
|
|
819
|
+
meta.userId = anonymousAuth.id;
|
|
820
|
+
meta.role = anonymousAuth.role;
|
|
821
|
+
meta.auth = anonymousAuth;
|
|
822
|
+
this.setWSMeta(ws, meta);
|
|
823
|
+
|
|
824
|
+
if (this.pendingAuth.delete(meta.connectionId)) {
|
|
825
|
+
this.syncEphemeralTimersToStorage();
|
|
826
|
+
this._scheduleNextAlarm();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (!isReAuth && meta.userId) {
|
|
830
|
+
if (this.disconnectTimers.delete(meta.userId)) {
|
|
831
|
+
this.syncEphemeralTimersToStorage();
|
|
832
|
+
this._scheduleNextAlarm();
|
|
833
|
+
}
|
|
834
|
+
this.addPlayer(meta.connectionId, meta.userId);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
this.safeSend(ws, {
|
|
838
|
+
type: isReAuth ? 'auth_refreshed' : 'auth_success',
|
|
839
|
+
userId: anonymousAuth.id,
|
|
840
|
+
connectionId: meta.connectionId,
|
|
841
|
+
});
|
|
842
|
+
this.syncRoomMonitoringSnapshot();
|
|
843
|
+
await this.recoverStateIfNeeded();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
777
847
|
const secret = this.env.JWT_USER_SECRET;
|
|
778
848
|
if (!secret) {
|
|
779
849
|
this.safeSend(ws, { type: 'error', code: 'SERVER_ERROR', message: 'JWT secret not configured' });
|
|
@@ -782,14 +852,15 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
782
852
|
}
|
|
783
853
|
|
|
784
854
|
try {
|
|
855
|
+
const accessToken = token as string;
|
|
785
856
|
const headers = new Headers();
|
|
786
857
|
if (meta.ip) headers.set('CF-Connecting-IP', meta.ip);
|
|
787
858
|
if (meta.userAgent) headers.set('User-Agent', meta.userAgent);
|
|
788
|
-
headers.set('Authorization', `Bearer ${
|
|
859
|
+
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
789
860
|
const { resolveAuthContextFromToken } = await import('../middleware/auth.js');
|
|
790
861
|
const auth = await resolveAuthContextFromToken(
|
|
791
862
|
this.env,
|
|
792
|
-
|
|
863
|
+
accessToken,
|
|
793
864
|
new Request('http://internal/api/room/auth', { headers }),
|
|
794
865
|
);
|
|
795
866
|
meta.authenticated = true;
|
|
@@ -1157,6 +1228,8 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
1157
1228
|
|
|
1158
1229
|
protected buildRoomServerAPI(): RoomServerAPI {
|
|
1159
1230
|
return {
|
|
1231
|
+
namespace: this.namespace ?? '',
|
|
1232
|
+
roomId: this.roomId ?? '',
|
|
1160
1233
|
getSharedState: (): Record<string, unknown> => {
|
|
1161
1234
|
return cloneState(this.sharedState);
|
|
1162
1235
|
},
|
|
@@ -1343,7 +1416,8 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
1343
1416
|
userId: meta.userId!,
|
|
1344
1417
|
connectionId: meta.connectionId,
|
|
1345
1418
|
role: meta.role,
|
|
1346
|
-
|
|
1419
|
+
auth: this.buildAuthFromMeta(meta),
|
|
1420
|
+
} as RoomSender;
|
|
1347
1421
|
}
|
|
1348
1422
|
|
|
1349
1423
|
protected buildAuthFromMeta(meta: RoomWSMeta): SharedAuthContext {
|