@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.
Files changed (65) hide show
  1. package/admin-build/_app/immutable/chunks/{CIOC1v_q.js → 0M5txo3l.js} +3 -3
  2. package/admin-build/_app/immutable/chunks/{ejoEf2I5.js → 39YMyjjq.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/B0zJ5_Wk.js +1 -0
  4. package/admin-build/_app/immutable/chunks/{BhCO1Fpt.js → BN-pHKcH.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{BEM1BeVF.js → BfhqI0W8.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{DnpbvAPi.js → C5nMidnK.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{iEyeblJR.js → CI1lKBjB.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{Dz9cUCuv.js → CPLcjNR9.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/{BfpUQYr3.js → DBkor_jM.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{Tea2dBJ8.js → DBniE7GN.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{BYyykAbh.js → DUhE3Ynq.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/DZo4s3wn.js +1 -0
  13. package/admin-build/_app/immutable/chunks/{BDYewzou.js → DllVADtt.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{CvczjTXx.js → DncT8xH5.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{DaXO-sFP.js → YGUb3X-Q.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{D1u3u7xu.js → _xa30BOD.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.DoUaxnew.js → app.BW-Ac3jY.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.Bcf0SjPV.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.Dsxi8s7i.js → 0.CRSQPtos.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.Cp2l-hol.js → 1.HmStNbZP.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.4oY6m8Nz.js → 10.BVlz13nr.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.DfcozD4J.js → 11.VwQdbWwH.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.uJgZdCIA.js → 12.BFAAgi1f.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.CaN1kRev.js → 13.DDdauC84.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.DQ5xIi3s.js → 14.D_rXaafJ.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.B_EkebTJ.js → 15.rfFjjiZc.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.Tko1ZX8-.js → 16.DDEpGJ4Z.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.BCmWMJX9.js → 17.ChQWNtRN.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.hmGhl1O2.js → 18.Rir_JwlF.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.D-1infOo.js → 19.CF76nxk1.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.CY4KKcBL.js → 20.JQer3KPM.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.CzGYxjpu.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.14Vd7bnt.js → 22.DUw7X6z_.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.Be6jK77o.js → 23.DS3svA-a.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CSTFkr6R.js → 24.BYOsQM6B.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.DRTg8fHc.js → 25.n0pyDBRv.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.DKt-9lwQ.js → 26.C-Oo8sYK.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.D5caPu0F.js → 27.BfxbFNkk.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.hJhlnlyY.js → 28.DyHSN1q7.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.CDYBzFyT.js → 29.CUAmZbCc.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.DMyKwkGn.js → 3.KQm4VJJg.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.BaHNeEmc.js → 30.CQdPre5F.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.C6PV5L-2.js → 31.BP6raetq.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.9E118Ftm.js → 4.Glf2jJHB.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.D8guAl3v.js → 5.BdFtj_-X.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.D1u__DtT.js → 6.JfnwWq8s.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.DWXHnRFf.js → 7.C-rw9qDh.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.Dojd8krc.js → 8.CA7r1Sjs.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.CLtrr0K_.js → 9.B2jgJDkH.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -3
  53. package/src/__tests__/admin-assets.test.ts +7 -7
  54. package/src/__tests__/frontend-assets.test.ts +75 -0
  55. package/src/__tests__/frontend-config.test.ts +16 -0
  56. package/src/__tests__/frontend-routing.test.ts +200 -0
  57. package/src/durable-objects/room-runtime-base.ts +80 -6
  58. package/src/index.ts +97 -3
  59. package/src/lib/admin-assets.ts +5 -5
  60. package/src/lib/frontend-assets.ts +129 -0
  61. package/src/lib/frontend-config.ts +11 -0
  62. package/admin-build/_app/immutable/chunks/BaUG2TJ-.js +0 -1
  63. package/admin-build/_app/immutable/chunks/qKdzaeX3.js +0 -1
  64. package/admin-build/_app/immutable/entry/start.MmZh8oBH.js +0 -1
  65. 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/CvczjTXx.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/DaXO-sFP.js";import{D as rt,T as nt}from"../chunks/BfpUQYr3.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
+ 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":"1774791730603"}
1
+ {"version":"1775689125712"}
@@ -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.MmZh8oBH.js" rel="modulepreload">
9
- <link href="/admin/_app/immutable/chunks/BaUG2TJ-.js" rel="modulepreload">
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/qKdzaeX3.js" rel="modulepreload">
13
- <link href="/admin/_app/immutable/entry/app.DoUaxnew.js" rel="modulepreload">
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
- __sveltekit_jgts8u = {
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.MmZh8oBH.js"),
38
- import("/admin/_app/immutable/entry/app.DoUaxnew.js")
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.7",
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.7",
38
- "@edge-base/shared": "0.2.7"
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(ws, meta, msg.token as string);
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(ws: WebSocket, meta: RoomWSMeta, token: string): Promise<void> {
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
- if (!token) {
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 ${token}`);
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
- token,
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 {