@miadi/stateloom-web 0.1.0 → 0.1.1

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 (79) hide show
  1. package/package.json +1 -1
  2. package/server/.next/BUILD_ID +1 -1
  3. package/server/.next/build-manifest.json +4 -4
  4. package/server/.next/server/app/_global-error/page/build-manifest.json +2 -2
  5. package/server/.next/server/app/_global-error/page.js +1 -1
  6. package/server/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/server/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/server/.next/server/app/_global-error.html +2 -2
  9. package/server/.next/server/app/_global-error.rsc +1 -1
  10. package/server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/server/.next/server/app/_not-found/page/build-manifest.json +2 -2
  16. package/server/.next/server/app/_not-found/page.js +1 -1
  17. package/server/.next/server/app/_not-found/page.js.nft.json +1 -1
  18. package/server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/server/.next/server/app/_not-found.html +1 -1
  20. package/server/.next/server/app/_not-found.rsc +2 -2
  21. package/server/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  22. package/server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  23. package/server/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  24. package/server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  25. package/server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  26. package/server/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  27. package/server/.next/server/app/api/file/route.js.nft.json +1 -1
  28. package/server/.next/server/app/api/rispec/route.js.nft.json +1 -1
  29. package/server/.next/server/app/api/watch/route.js +3 -2
  30. package/server/.next/server/app/api/watch/route.js.nft.json +1 -1
  31. package/server/.next/server/app/index.html +1 -1
  32. package/server/.next/server/app/index.rsc +3 -3
  33. package/server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  34. package/server/.next/server/app/index.segments/_full.segment.rsc +3 -3
  35. package/server/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/server/.next/server/app/index.segments/_index.segment.rsc +2 -2
  37. package/server/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/server/.next/server/app/page/build-manifest.json +2 -2
  39. package/server/.next/server/app/page.js +1 -1
  40. package/server/.next/server/app/page.js.nft.json +1 -1
  41. package/server/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/server/.next/server/chunks/[root-of-the-server]__2046b812._.js +2 -2
  43. package/server/.next/server/chunks/[root-of-the-server]__87bf4b91._.js +1 -1
  44. package/server/.next/server/chunks/[root-of-the-server]__95b05635._.js +8 -0
  45. package/server/.next/server/chunks/ssr/[root-of-the-server]__2955c0d8._.js +3 -0
  46. package/server/.next/server/chunks/ssr/{[root-of-the-server]__b4e7f753._.js → [root-of-the-server]__385c85a7._.js} +2 -2
  47. package/server/.next/server/chunks/ssr/[root-of-the-server]__a977f174._.js +3 -0
  48. package/server/.next/server/chunks/ssr/[root-of-the-server]__ac84e72e._.js +3 -0
  49. package/server/.next/server/chunks/ssr/[root-of-the-server]__edbb995c._.js +3 -3
  50. package/server/.next/server/chunks/ssr/{[root-of-the-server]__9cafe12a._.js → [root-of-the-server]__f25b51be._.js} +2 -2
  51. package/server/.next/server/middleware-build-manifest.js +2 -2
  52. package/server/.next/server/pages/404.html +1 -1
  53. package/server/.next/server/pages/500.html +2 -2
  54. package/server/.next/static/chunks/2c679fab817159f2.css +1 -0
  55. package/server/.next/static/chunks/{8a03c0d2436d4c0f.js → 662d1b0ce457c16b.js} +1 -1
  56. package/server/.next/static/chunks/{367d28d76d2be05f.js → 7921d7703dd690a6.js} +4 -4
  57. package/server/.next/static/chunks/aea5b1716c68391c.js +1 -0
  58. package/server/.next/static/chunks/{turbopack-8f57da603e40457d.js → turbopack-34fc7de1568e2a7f.js} +1 -1
  59. package/server/package-lock.json +2 -2
  60. package/server/package.json +1 -1
  61. package/server/src/app/api/file/route.ts +20 -6
  62. package/server/src/app/api/watch/route.ts +11 -3
  63. package/server/src/components/BridgeProvider.tsx +10 -3
  64. package/server/src/components/DocSwitcher.tsx +84 -0
  65. package/server/src/components/SocketBridgeProvider.tsx +11 -4
  66. package/server/src/components/Toolbar.tsx +11 -3
  67. package/server/src/lib/docParam.ts +51 -0
  68. package/server/src/lib/layoutMemory.ts +20 -2
  69. package/server/src/lib/projectFile.ts +68 -1
  70. package/server/stateloom-web.json +2 -2
  71. package/server/tsconfig.tsbuildinfo +1 -1
  72. package/server/.next/server/chunks/[root-of-the-server]__0a18d740._.js +0 -8
  73. package/server/.next/server/chunks/ssr/[root-of-the-server]__517c9b38._.js +0 -3
  74. package/server/.next/server/chunks/ssr/[root-of-the-server]__632c82d7._.js +0 -3
  75. package/server/.next/server/chunks/ssr/[root-of-the-server]__ecf19999._.js +0 -3
  76. package/server/.next/static/chunks/b6bd7719e3445512.css +0 -1
  77. /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → vPEqV8SJpTDJWGy_fRuh4}/_buildManifest.js +0 -0
  78. /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → vPEqV8SJpTDJWGy_fRuh4}/_clientMiddlewareManifest.json +0 -0
  79. /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → vPEqV8SJpTDJWGy_fRuh4}/_ssgManifest.js +0 -0
@@ -0,0 +1 @@
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,76800,e=>{"use strict";var t=e.i(30878);let n="stateloom:doc-change";function o(){return new URLSearchParams(window.location.search).get("doc")}function r(e){return e?`?doc=${encodeURIComponent(e)}`:""}function s(e){let t=new URL(window.location.href);e?t.searchParams.set("doc",e):t.searchParams.delete("doc"),window.history.pushState({},"",t),window.dispatchEvent(new Event(n))}function c(){let[e,r]=(0,t.useState)(null);return(0,t.useEffect)(()=>{let e=()=>r(o());return e(),window.addEventListener("popstate",e),window.addEventListener(n,e),()=>{window.removeEventListener("popstate",e),window.removeEventListener(n,e)}},[]),e}e.s(["docQuery",()=>r,"navigateToDoc",()=>s,"requestedDoc",()=>o,"useRequestedDoc",()=>c])},32058,e=>{e.v(e=>Promise.resolve().then(()=>e(76800)))}]);
@@ -1,4 +1,4 @@
1
- (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,{otherChunks:["static/chunks/8a03c0d2436d4c0f.js","static/chunks/5e1795a806bd6c51.js","static/chunks/96818f5620f039a1.js"],runtimeModuleIds:[95506]}]),(()=>{let e;if(!Array.isArray(globalThis.TURBOPACK))return;let t="/_next/",r=(self.TURBOPACK_CHUNK_SUFFIX??document?.currentScript?.getAttribute?.("src")?.replace(/^(.*(?=\?)|^.*$)/,""))||"",n=new WeakMap;function o(e,t){this.m=e,this.e=t}let l=o.prototype,i=Object.prototype.hasOwnProperty,s="u">typeof Symbol&&Symbol.toStringTag;function u(e,t,r){i.call(e,t)||Object.defineProperty(e,t,r)}function a(e,t){let r=e[t];return r||(r=c(t),e[t]=r),r}function c(e){return{exports:{},error:void 0,id:e,namespaceObject:void 0}}function f(e,t){u(e,"__esModule",{value:!0}),s&&u(e,s,{value:"Module"});let r=0;for(;r<t.length;){let n=t[r++],o=t[r++];if("number"==typeof o)if(0===o)u(e,n,{value:t[r++],enumerable:!0,writable:!1});else throw Error(`unexpected tag: ${o}`);else"function"==typeof t[r]?u(e,n,{get:o,set:t[r++],enumerable:!0}):u(e,n,{get:o,enumerable:!0})}Object.seal(e)}l.s=function(e,t){let r,n;null!=t?n=(r=a(this.c,t)).exports:(r=this.m,n=this.e),r.namespaceObject=n,f(n,e)},l.j=function(e,t){var r,o;let l,s,u;null!=t?s=(l=a(this.c,t)).exports:(l=this.m,s=this.e);let c=(r=l,o=s,(u=n.get(r))||(n.set(r,u=[]),r.exports=r.namespaceObject=new Proxy(o,{get(e,t){if(i.call(e,t)||"default"===t||"__esModule"===t)return Reflect.get(e,t);for(let e of u){let r=Reflect.get(e,t);if(void 0!==r)return r}},ownKeys(e){let t=Reflect.ownKeys(e);for(let e of u)for(let r of Reflect.ownKeys(e))"default"===r||t.includes(r)||t.push(r);return t}})),u);"object"==typeof e&&null!==e&&c.push(e)},l.v=function(e,t){(null!=t?a(this.c,t):this.m).exports=e},l.n=function(e,t){let r;(r=null!=t?a(this.c,t):this.m).exports=r.namespaceObject=e};let p=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,h=[null,p({}),p([]),p(p)];function d(e,t,r){let n=[],o=-1;for(let t=e;("object"==typeof t||"function"==typeof t)&&!h.includes(t);t=p(t))for(let r of Object.getOwnPropertyNames(t))n.push(r,function(e,t){return()=>e[t]}(e,r)),-1===o&&"default"===r&&(o=n.length-1);return r&&o>=0||(o>=0?n.splice(o,1,0,e):n.push("default",0,e)),f(t,n),t}function m(e){let t=B(e,this.m);if(t.namespaceObject)return t.namespaceObject;let r=t.exports;return t.namespaceObject=d(r,"function"==typeof r?function(...e){return r.apply(this,e)}:Object.create(null),r&&r.__esModule)}function b(e){let t=e.indexOf("#");-1!==t&&(e=e.substring(0,t));let r=e.indexOf("?");return -1!==r&&(e=e.substring(0,r)),e}function y(){let e,t;return{promise:new Promise((r,n)=>{t=n,e=r}),resolve:e,reject:t}}l.i=m,l.A=function(e){return this.r(e)(m.bind(this))},l.t="function"==typeof require?require:function(){throw Error("Unexpected use of runtime require")},l.r=function(e){return B(e,this.m).exports},l.f=function(e){function t(t){if(t=b(t),i.call(e,t))return e[t].module();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r}return t.keys=()=>Object.keys(e),t.resolve=t=>{if(t=b(t),i.call(e,t))return e[t].id();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r},t.import=async e=>await t(e),t};let O=Symbol("turbopack queues"),g=Symbol("turbopack exports"),w=Symbol("turbopack error");function C(e){e&&1!==e.status&&(e.status=1,e.forEach(e=>e.queueCount--),e.forEach(e=>e.queueCount--?e.queueCount++:e()))}l.a=function(e,t){let r=this.m,n=t?Object.assign([],{status:-1}):void 0,o=new Set,{resolve:l,reject:i,promise:s}=y(),u=Object.assign(s,{[g]:r.exports,[O]:e=>{n&&e(n),o.forEach(e),u.catch(()=>{})}}),a={get:()=>u,set(e){e!==u&&(u[g]=e)}};Object.defineProperty(r,"exports",a),Object.defineProperty(r,"namespaceObject",a),e(function(e){let t=e.map(e=>{if(null!==e&&"object"==typeof e){if(O in e)return e;if(null!=e&&"object"==typeof e&&"then"in e&&"function"==typeof e.then){let t=Object.assign([],{status:0}),r={[g]:{},[O]:e=>e(t)};return e.then(e=>{r[g]=e,C(t)},e=>{r[w]=e,C(t)}),r}}return{[g]:e,[O]:()=>{}}}),r=()=>t.map(e=>{if(e[w])throw e[w];return e[g]}),{promise:l,resolve:i}=y(),s=Object.assign(()=>i(r),{queueCount:0});function u(e){e!==n&&!o.has(e)&&(o.add(e),e&&0===e.status&&(s.queueCount++,e.push(s)))}return t.map(e=>e[O](u)),s.queueCount?l:r()},function(e){e?i(u[w]=e):l(u[g]),C(n)}),n&&-1===n.status&&(n.status=0)};let U=function(e){let t=new URL(e,"x:/"),r={};for(let e in t)r[e]=t[e];for(let t in r.href=e,r.pathname=e.replace(/[?#].*/,""),r.origin=r.protocol="",r.toString=r.toJSON=(...t)=>e,r)Object.defineProperty(this,t,{enumerable:!0,configurable:!0,value:r[t]})};function R(e,t){throw Error(`Invariant: ${t(e)}`)}U.prototype=URL.prototype,l.U=U,l.z=function(e){throw Error("dynamic usage of require is not supported")},l.g=globalThis;let j=o.prototype;var _,k=((_=k||{})[_.Runtime=0]="Runtime",_[_.Parent=1]="Parent",_[_.Update=2]="Update",_);let v=new Map;l.M=v;let $=new Map,P=new Map;async function S(e,t,r){let n;if("string"==typeof r)return E(e,t,K(r));let o=r.included||[],l=o.map(e=>!!v.has(e)||$.get(e));if(l.length>0&&l.every(e=>e))return void await Promise.all(l);let i=r.moduleChunks||[],s=i.map(e=>P.get(e)).filter(e=>e);if(s.length>0){if(s.length===i.length)return void await Promise.all(s);let r=new Set;for(let e of i)P.has(e)||r.add(e);for(let n of r){let r=E(e,t,K(n));P.set(n,r),s.push(r)}n=Promise.all(s)}else{for(let o of(n=E(e,t,K(r.path)),i))P.has(o)||P.set(o,n)}for(let e of o)$.has(e)||$.set(e,n);await n}j.l=function(e){return S(1,this.m.id,e)};let T=Promise.resolve(void 0),A=new WeakMap;function E(t,r,n){let o=e.loadChunkCached(t,n),l=A.get(o);if(void 0===l){let e=A.set.bind(A,o,T);l=o.then(e).catch(e=>{let o;switch(t){case 0:o=`as a runtime dependency of chunk ${r}`;break;case 1:o=`from module ${r}`;break;case 2:o="from an HMR update";break;default:R(t,e=>`Unknown source type: ${e}`)}let l=Error(`Failed to load chunk ${n} ${o}${e?`: ${e}`:""}`,e?{cause:e}:void 0);throw l.name="ChunkLoadError",l}),A.set(o,l)}return l}function K(e){return`${t}${e.split("/").map(e=>encodeURIComponent(e)).join("/")}${r}`}j.L=function(e){return E(1,this.m.id,e)},j.R=function(e){let t=this.r(e);return t?.default??t},j.P=function(e){return`/ROOT/${e??""}`},j.b=function(e){let t=new Blob([`self.TURBOPACK_WORKER_LOCATION = ${JSON.stringify(location.origin)};
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,{otherChunks:["static/chunks/662d1b0ce457c16b.js","static/chunks/5e1795a806bd6c51.js","static/chunks/96818f5620f039a1.js"],runtimeModuleIds:[95506]}]),(()=>{let e;if(!Array.isArray(globalThis.TURBOPACK))return;let t="/_next/",r=(self.TURBOPACK_CHUNK_SUFFIX??document?.currentScript?.getAttribute?.("src")?.replace(/^(.*(?=\?)|^.*$)/,""))||"",n=new WeakMap;function o(e,t){this.m=e,this.e=t}let l=o.prototype,i=Object.prototype.hasOwnProperty,s="u">typeof Symbol&&Symbol.toStringTag;function u(e,t,r){i.call(e,t)||Object.defineProperty(e,t,r)}function a(e,t){let r=e[t];return r||(r=c(t),e[t]=r),r}function c(e){return{exports:{},error:void 0,id:e,namespaceObject:void 0}}function f(e,t){u(e,"__esModule",{value:!0}),s&&u(e,s,{value:"Module"});let r=0;for(;r<t.length;){let n=t[r++],o=t[r++];if("number"==typeof o)if(0===o)u(e,n,{value:t[r++],enumerable:!0,writable:!1});else throw Error(`unexpected tag: ${o}`);else"function"==typeof t[r]?u(e,n,{get:o,set:t[r++],enumerable:!0}):u(e,n,{get:o,enumerable:!0})}Object.seal(e)}l.s=function(e,t){let r,n;null!=t?n=(r=a(this.c,t)).exports:(r=this.m,n=this.e),r.namespaceObject=n,f(n,e)},l.j=function(e,t){var r,o;let l,s,u;null!=t?s=(l=a(this.c,t)).exports:(l=this.m,s=this.e);let c=(r=l,o=s,(u=n.get(r))||(n.set(r,u=[]),r.exports=r.namespaceObject=new Proxy(o,{get(e,t){if(i.call(e,t)||"default"===t||"__esModule"===t)return Reflect.get(e,t);for(let e of u){let r=Reflect.get(e,t);if(void 0!==r)return r}},ownKeys(e){let t=Reflect.ownKeys(e);for(let e of u)for(let r of Reflect.ownKeys(e))"default"===r||t.includes(r)||t.push(r);return t}})),u);"object"==typeof e&&null!==e&&c.push(e)},l.v=function(e,t){(null!=t?a(this.c,t):this.m).exports=e},l.n=function(e,t){let r;(r=null!=t?a(this.c,t):this.m).exports=r.namespaceObject=e};let p=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,h=[null,p({}),p([]),p(p)];function d(e,t,r){let n=[],o=-1;for(let t=e;("object"==typeof t||"function"==typeof t)&&!h.includes(t);t=p(t))for(let r of Object.getOwnPropertyNames(t))n.push(r,function(e,t){return()=>e[t]}(e,r)),-1===o&&"default"===r&&(o=n.length-1);return r&&o>=0||(o>=0?n.splice(o,1,0,e):n.push("default",0,e)),f(t,n),t}function m(e){let t=B(e,this.m);if(t.namespaceObject)return t.namespaceObject;let r=t.exports;return t.namespaceObject=d(r,"function"==typeof r?function(...e){return r.apply(this,e)}:Object.create(null),r&&r.__esModule)}function b(e){let t=e.indexOf("#");-1!==t&&(e=e.substring(0,t));let r=e.indexOf("?");return -1!==r&&(e=e.substring(0,r)),e}function y(){let e,t;return{promise:new Promise((r,n)=>{t=n,e=r}),resolve:e,reject:t}}l.i=m,l.A=function(e){return this.r(e)(m.bind(this))},l.t="function"==typeof require?require:function(){throw Error("Unexpected use of runtime require")},l.r=function(e){return B(e,this.m).exports},l.f=function(e){function t(t){if(t=b(t),i.call(e,t))return e[t].module();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r}return t.keys=()=>Object.keys(e),t.resolve=t=>{if(t=b(t),i.call(e,t))return e[t].id();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r},t.import=async e=>await t(e),t};let O=Symbol("turbopack queues"),g=Symbol("turbopack exports"),w=Symbol("turbopack error");function C(e){e&&1!==e.status&&(e.status=1,e.forEach(e=>e.queueCount--),e.forEach(e=>e.queueCount--?e.queueCount++:e()))}l.a=function(e,t){let r=this.m,n=t?Object.assign([],{status:-1}):void 0,o=new Set,{resolve:l,reject:i,promise:s}=y(),u=Object.assign(s,{[g]:r.exports,[O]:e=>{n&&e(n),o.forEach(e),u.catch(()=>{})}}),a={get:()=>u,set(e){e!==u&&(u[g]=e)}};Object.defineProperty(r,"exports",a),Object.defineProperty(r,"namespaceObject",a),e(function(e){let t=e.map(e=>{if(null!==e&&"object"==typeof e){if(O in e)return e;if(null!=e&&"object"==typeof e&&"then"in e&&"function"==typeof e.then){let t=Object.assign([],{status:0}),r={[g]:{},[O]:e=>e(t)};return e.then(e=>{r[g]=e,C(t)},e=>{r[w]=e,C(t)}),r}}return{[g]:e,[O]:()=>{}}}),r=()=>t.map(e=>{if(e[w])throw e[w];return e[g]}),{promise:l,resolve:i}=y(),s=Object.assign(()=>i(r),{queueCount:0});function u(e){e!==n&&!o.has(e)&&(o.add(e),e&&0===e.status&&(s.queueCount++,e.push(s)))}return t.map(e=>e[O](u)),s.queueCount?l:r()},function(e){e?i(u[w]=e):l(u[g]),C(n)}),n&&-1===n.status&&(n.status=0)};let U=function(e){let t=new URL(e,"x:/"),r={};for(let e in t)r[e]=t[e];for(let t in r.href=e,r.pathname=e.replace(/[?#].*/,""),r.origin=r.protocol="",r.toString=r.toJSON=(...t)=>e,r)Object.defineProperty(this,t,{enumerable:!0,configurable:!0,value:r[t]})};function R(e,t){throw Error(`Invariant: ${t(e)}`)}U.prototype=URL.prototype,l.U=U,l.z=function(e){throw Error("dynamic usage of require is not supported")},l.g=globalThis;let j=o.prototype;var _,k=((_=k||{})[_.Runtime=0]="Runtime",_[_.Parent=1]="Parent",_[_.Update=2]="Update",_);let v=new Map;l.M=v;let $=new Map,P=new Map;async function S(e,t,r){let n;if("string"==typeof r)return E(e,t,K(r));let o=r.included||[],l=o.map(e=>!!v.has(e)||$.get(e));if(l.length>0&&l.every(e=>e))return void await Promise.all(l);let i=r.moduleChunks||[],s=i.map(e=>P.get(e)).filter(e=>e);if(s.length>0){if(s.length===i.length)return void await Promise.all(s);let r=new Set;for(let e of i)P.has(e)||r.add(e);for(let n of r){let r=E(e,t,K(n));P.set(n,r),s.push(r)}n=Promise.all(s)}else{for(let o of(n=E(e,t,K(r.path)),i))P.has(o)||P.set(o,n)}for(let e of o)$.has(e)||$.set(e,n);await n}j.l=function(e){return S(1,this.m.id,e)};let T=Promise.resolve(void 0),A=new WeakMap;function E(t,r,n){let o=e.loadChunkCached(t,n),l=A.get(o);if(void 0===l){let e=A.set.bind(A,o,T);l=o.then(e).catch(e=>{let o;switch(t){case 0:o=`as a runtime dependency of chunk ${r}`;break;case 1:o=`from module ${r}`;break;case 2:o="from an HMR update";break;default:R(t,e=>`Unknown source type: ${e}`)}let l=Error(`Failed to load chunk ${n} ${o}${e?`: ${e}`:""}`,e?{cause:e}:void 0);throw l.name="ChunkLoadError",l}),A.set(o,l)}return l}function K(e){return`${t}${e.split("/").map(e=>encodeURIComponent(e)).join("/")}${r}`}j.L=function(e){return E(1,this.m.id,e)},j.R=function(e){let t=this.r(e);return t?.default??t},j.P=function(e){return`/ROOT/${e??""}`},j.b=function(e){let t=new Blob([`self.TURBOPACK_WORKER_LOCATION = ${JSON.stringify(location.origin)};
2
2
  self.TURBOPACK_CHUNK_SUFFIX = ${JSON.stringify(r)};
3
3
  self.TURBOPACK_NEXT_CHUNK_URLS = ${JSON.stringify(e.reverse().map(K),null,2)};
4
4
  importScripts(...self.TURBOPACK_NEXT_CHUNK_URLS.map(c => self.TURBOPACK_WORKER_LOCATION + c).reverse());`],{type:"text/javascript"});return URL.createObjectURL(t)};let x=/\.js(?:\?[^#]*)?(?:#.*)?$/,N=/\.css(?:\?[^#]*)?(?:#.*)?$/;function M(e){return N.test(e)}l.w=function(t,r,n){return e.loadWebAssembly(1,this.m.id,t,r,n)},l.u=function(t,r){return e.loadWebAssemblyModule(1,this.m.id,t,r)};let L={};l.c=L;let B=(e,t)=>{let r=L[e];if(r){if(r.error)throw r.error;return r}return q(e,k.Parent,t.id)};function q(e,t,r){let n=v.get(e);if("function"!=typeof n)throw Error(function(e,t,r){let n;switch(t){case 0:n=`as a runtime entry of chunk ${r}`;break;case 1:n=`because it was required from module ${r}`;break;case 2:n="because of an HMR update";break;default:R(t,e=>`Unknown source type: ${e}`)}return`Module ${e} was instantiated ${n}, but the module factory is not available.`}(e,t,r));let l=c(e),i=l.exports;L[e]=l;let s=new o(l,i);try{n(s,l,i)}catch(e){throw l.error=e,e}return l.namespaceObject&&l.exports!==l.namespaceObject&&d(l.exports,l.namespaceObject),l}function I(r){let n,o=function(e){if("string"==typeof e)return e;let r=decodeURIComponent(("u">typeof TURBOPACK_NEXT_CHUNK_URLS?TURBOPACK_NEXT_CHUNK_URLS.pop():e.getAttribute("src")).replace(/[?#].*$/,""));return r.startsWith(t)?r.slice(t.length):r}(r[0]);return 2===r.length?n=r[1]:(n=void 0,!function(e,t,r,n){let o=1;for(;o<e.length;){let t=e[o],n=o+1;for(;n<e.length&&"function"!=typeof e[n];)n++;if(n===e.length)throw Error("malformed chunk format, expected a factory function");if(!r.has(t)){let l=e[n];for(Object.defineProperty(l,"name",{value:"module evaluation"});o<n;o++)t=e[o],r.set(t,l)}o=n+1}}(r,0,v)),e.registerChunk(o,n)}let W=new Map;function H(e){let t=W.get(e);if(!t){let r,n;t={resolved:!1,loadingStarted:!1,promise:new Promise((e,t)=>{r=e,n=t}),resolve:()=>{t.resolved=!0,r()},reject:n},W.set(e,t)}return t}e={async registerChunk(e,t){if(H(K(e)).resolve(),null!=t){for(let e of t.otherChunks)H(K("string"==typeof e?e:e.path));if(await Promise.all(t.otherChunks.map(t=>S(0,e,t))),t.runtimeModuleIds.length>0)for(let r of t.runtimeModuleIds)!function(e,t){let r=L[t];if(r){if(r.error)throw r.error;return}q(t,k.Runtime,e)}(e,r)}},loadChunkCached:(e,t)=>(function(e,t){let r=H(t);if(r.loadingStarted)return r.promise;if(e===k.Runtime)return r.loadingStarted=!0,M(t)&&r.resolve(),r.promise;if("function"==typeof importScripts)if(M(t));else if(x.test(t))self.TURBOPACK_NEXT_CHUNK_URLS.push(t),importScripts(TURBOPACK_WORKER_LOCATION+t);else throw Error(`can't infer type of chunk from URL ${t} in worker`);else{let e=decodeURI(t);if(M(t))if(document.querySelectorAll(`link[rel=stylesheet][href="${t}"],link[rel=stylesheet][href^="${t}?"],link[rel=stylesheet][href="${e}"],link[rel=stylesheet][href^="${e}?"]`).length>0)r.resolve();else{let e=document.createElement("link");e.rel="stylesheet",e.href=t,e.onerror=()=>{r.reject()},e.onload=()=>{r.resolve()},document.head.appendChild(e)}else if(x.test(t)){let n=document.querySelectorAll(`script[src="${t}"],script[src^="${t}?"],script[src="${e}"],script[src^="${e}?"]`);if(n.length>0)for(let e of Array.from(n))e.addEventListener("error",()=>{r.reject()});else{let e=document.createElement("script");e.src=t,e.onerror=()=>{r.reject()},document.head.appendChild(e)}}else throw Error(`can't infer type of chunk from URL ${t}`)}return r.loadingStarted=!0,r.promise})(e,t),async loadWebAssembly(e,t,r,n,o){let l=fetch(K(r)),{instance:i}=await WebAssembly.instantiateStreaming(l,o);return i.exports},async loadWebAssemblyModule(e,t,r,n){let o=fetch(K(r));return await WebAssembly.compileStreaming(o)}};let F=globalThis.TURBOPACK;globalThis.TURBOPACK={push:I},F.forEach(I)})();
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "stateloom-web",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "stateloom-web",
9
- "version": "0.1.0",
9
+ "version": "0.1.1",
10
10
  "dependencies": {
11
11
  "@miadi/stateloom-client": "file:../bridge-client",
12
12
  "@miadi/stateloom-protocol": "file:../bridge-protocol",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stateloom-web",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p ${PORT:-4598}",
@@ -1,11 +1,21 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextRequest, NextResponse } from "next/server";
2
2
  import { existsSync, readFileSync, writeFileSync, statSync } from "fs";
3
- import { getProjectFilePath } from "@/lib/projectFile";
3
+ import { resolveDocPath } from "@/lib/projectFile";
4
4
 
5
5
  export const dynamic = "force-dynamic";
6
6
 
7
- export async function GET() {
8
- const path = getProjectFilePath();
7
+ // Optional `?doc=<absolute .json path>` selects the document; omitted, the
8
+ // default project file answers exactly as before. Every doc passes the root
9
+ // allowlist in lib/projectFile.ts — parameter and guard are one surface
10
+ // (chart chart_1785683022927). A refusal names its reason and is a 403,
11
+ // never a write.
12
+
13
+ export async function GET(req: NextRequest) {
14
+ const resolution = resolveDocPath(req.nextUrl.searchParams.get("doc"));
15
+ if (!resolution.ok) {
16
+ return NextResponse.json({ error: resolution.error }, { status: 403 });
17
+ }
18
+ const path = resolution.path;
9
19
  if (!existsSync(path)) {
10
20
  return NextResponse.json({ path, content: null, mtime: 0, exists: false });
11
21
  }
@@ -14,8 +24,12 @@ export async function GET() {
14
24
  return NextResponse.json({ path, content, mtime, exists: true });
15
25
  }
16
26
 
17
- export async function PUT(req: Request) {
18
- const path = getProjectFilePath();
27
+ export async function PUT(req: NextRequest) {
28
+ const resolution = resolveDocPath(req.nextUrl.searchParams.get("doc"));
29
+ if (!resolution.ok) {
30
+ return NextResponse.json({ error: resolution.error }, { status: 403 });
31
+ }
32
+ const path = resolution.path;
19
33
  const body = await req.json();
20
34
  if (typeof body?.content !== "string") {
21
35
  return NextResponse.json(
@@ -1,11 +1,19 @@
1
+ import { NextRequest, NextResponse } from "next/server";
1
2
  import { existsSync, statSync, watch } from "fs";
2
- import { getProjectFilePath } from "@/lib/projectFile";
3
+ import { resolveDocPath } from "@/lib/projectFile";
3
4
 
4
5
  export const dynamic = "force-dynamic";
5
6
  export const runtime = "nodejs";
6
7
 
7
- export async function GET() {
8
- const path = getProjectFilePath();
8
+ export async function GET(req: NextRequest) {
9
+ // Same optional `?doc=` and the same allowlist as /api/file — the watch
10
+ // stream follows whichever document the page asked for (step 2 of
11
+ // chart_1785683010290). A refusal is a named 403, not a stream.
12
+ const resolution = resolveDocPath(req.nextUrl.searchParams.get("doc"));
13
+ if (!resolution.ok) {
14
+ return NextResponse.json({ error: resolution.error }, { status: 403 });
15
+ }
16
+ const path = resolution.path;
9
17
  const encoder = new TextEncoder();
10
18
 
11
19
  const stream = new ReadableStream({
@@ -2,18 +2,23 @@
2
2
 
3
3
  import { useEffect } from "react";
4
4
  import { useDesignerStore } from "@/store/useDesignerStore";
5
+ import { docQuery, useRequestedDoc } from "@/lib/docParam";
5
6
 
6
7
  export default function BridgeProvider() {
7
8
  const applyRemote = useDesignerStore((s) => s.applyRemote);
8
9
  const setRemoteStatus = useDesignerStore((s) => s.setRemoteStatus);
9
10
  const setRemoteMtime = useDesignerStore((s) => s.setRemoteMtime);
11
+ // Step 2: the requested `?doc=` rides both the file fetch and the watch
12
+ // stream; the server resolves and guards, the browser only passes through.
13
+ const requested = useRequestedDoc();
10
14
 
11
15
  useEffect(() => {
12
16
  let cancelled = false;
17
+ const qs = docQuery(requested);
13
18
 
14
19
  async function fetchFile(): Promise<{ content: string | null; mtime: number; path: string; exists: boolean } | null> {
15
20
  try {
16
- const r = await fetch("/api/file", { cache: "no-store" });
21
+ const r = await fetch(`/api/file${qs}`, { cache: "no-store" });
17
22
  if (!r.ok) return null;
18
23
  return await r.json();
19
24
  } catch {
@@ -33,7 +38,7 @@ export default function BridgeProvider() {
33
38
  }
34
39
  })();
35
40
 
36
- const es = new EventSource("/api/watch");
41
+ const es = new EventSource(`/api/watch${qs}`);
37
42
 
38
43
  es.addEventListener("hello", (ev) => {
39
44
  try {
@@ -68,7 +73,9 @@ export default function BridgeProvider() {
68
73
  cancelled = true;
69
74
  es.close();
70
75
  };
71
- }, [applyRemote, setRemoteStatus, setRemoteMtime]);
76
+ // `requested` re-keys the whole effect (step 3): a document switch closes
77
+ // the old watch stream and loads + watches the new document, no reload.
78
+ }, [applyRemote, setRemoteStatus, setRemoteMtime, requested]);
72
79
 
73
80
  return null;
74
81
  }
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { navigateToDoc, useRequestedDoc } from "@/lib/docParam";
5
+ import { rememberedDocs } from "@/lib/layoutMemory";
6
+
7
+ /**
8
+ * A human switches diagrams without an agent and without a terminal
9
+ * (chart_1785683062725). Presentation over state that already exists: recents
10
+ * are the documents this browser holds layout memory for, plus a free path
11
+ * field. Choosing navigates via `?doc=` — the providers re-key (step 3), the
12
+ * server allowlist guards (step 1), no reload, no new transport.
13
+ */
14
+ export default function DocSwitcher() {
15
+ const requested = useRequestedDoc();
16
+ const [open, setOpen] = useState(false);
17
+ const [typed, setTyped] = useState("");
18
+
19
+ const go = (doc: string | null) => {
20
+ setOpen(false);
21
+ setTyped("");
22
+ navigateToDoc(doc);
23
+ };
24
+
25
+ const recents = open
26
+ ? rememberedDocs().filter((d) => d.startsWith("/") && d !== requested)
27
+ : [];
28
+
29
+ return (
30
+ <span className="relative inline-flex">
31
+ <button
32
+ onClick={() => setOpen((v) => !v)}
33
+ className="toolbar-btn"
34
+ title={requested ? `Document: ${requested}` : "Switch document"}
35
+ >
36
+ ⇄
37
+ </button>
38
+ {open && (
39
+ <div className="absolute left-0 top-full z-50 mt-1 w-72 rounded border border-gray-700 bg-gray-900 p-2 shadow-lg">
40
+ <div className="mb-1 text-[10px] uppercase tracking-wide text-gray-500">
41
+ switch document
42
+ </div>
43
+ {requested && (
44
+ <button
45
+ onClick={() => go(null)}
46
+ className="block w-full truncate rounded px-1.5 py-1 text-left text-xs text-gray-300 hover:bg-gray-800"
47
+ title="Return to the default project file"
48
+ >
49
+ ⌂ default document
50
+ </button>
51
+ )}
52
+ {recents.map((d) => (
53
+ <button
54
+ key={d}
55
+ onClick={() => go(d)}
56
+ className="block w-full truncate rounded px-1.5 py-1 text-left text-xs text-gray-300 hover:bg-gray-800"
57
+ title={d}
58
+ >
59
+ {d.split("/").pop()}
60
+ <span className="ml-1 text-[10px] text-gray-600">{d}</span>
61
+ </button>
62
+ ))}
63
+ <form
64
+ onSubmit={(e) => {
65
+ e.preventDefault();
66
+ if (typed.trim()) go(typed.trim());
67
+ }}
68
+ className="mt-1 flex gap-1"
69
+ >
70
+ <input
71
+ value={typed}
72
+ onChange={(e) => setTyped(e.target.value)}
73
+ placeholder="/absolute/path/to/doc.smdf.json"
74
+ className="min-w-0 flex-1 rounded border border-gray-700 bg-gray-950 px-1.5 py-1 text-xs text-gray-200 placeholder:text-gray-600"
75
+ />
76
+ <button type="submit" className="toolbar-btn" title="Open this document">
77
+ →
78
+ </button>
79
+ </form>
80
+ </div>
81
+ )}
82
+ </span>
83
+ );
84
+ }
@@ -11,6 +11,7 @@ import {
11
11
  type StateMachineDefinition,
12
12
  } from "@miadi/stateloom-protocol";
13
13
  import { useDesignerStore } from "@/store/useDesignerStore";
14
+ import { docQuery, useRequestedDoc } from "@/lib/docParam";
14
15
 
15
16
  const OUTBOUND_DEBOUNCE_MS = 60;
16
17
 
@@ -26,6 +27,9 @@ const OUTBOUND_DEBOUNCE_MS = 60;
26
27
  */
27
28
  export default function SocketBridgeProvider({ url }: { url: string }) {
28
29
  const presence = useDesignerStore((s) => s.presence);
30
+ // The requested `?doc=` (step 2): passed through to the file API, which
31
+ // resolves and guards it; the join below keys the room by the RESOLVED path.
32
+ const requested = useRequestedDoc();
29
33
 
30
34
  useEffect(() => {
31
35
  if (!url) return;
@@ -42,7 +46,7 @@ export default function SocketBridgeProvider({ url }: { url: string }) {
42
46
  // Learn the docId (the resolved project-file path) from the file API.
43
47
  let docId = "";
44
48
  try {
45
- const r = await fetch("/api/file", { cache: "no-store" });
49
+ const r = await fetch(`/api/file${docQuery(requested)}`, { cache: "no-store" });
46
50
  if (r.ok) docId = (await r.json())?.path ?? "";
47
51
  } catch {
48
52
  // Connect anyway; the hub keys the doc by whatever id we send.
@@ -115,9 +119,12 @@ export default function SocketBridgeProvider({ url }: { url: string }) {
115
119
  if (unsub) unsub();
116
120
  if (session) session.disconnect();
117
121
  };
118
- // Re-run when the URL arrives: DesignBridge resolves it asynchronously from
119
- // /api/config, so the first render can legitimately have a different value.
120
- }, [url]);
122
+ // Re-run when the URL arrives (DesignBridge resolves it asynchronously from
123
+ // /api/config) AND when the requested document changes (step 3): the
124
+ // cleanup disconnects the old session, the new run joins the new room —
125
+ // switching needs no reload. applyRemote already rebuilds everything from
126
+ // an arbitrary definition, so the arriving full-def repaints the canvas.
127
+ }, [url, requested]);
121
128
 
122
129
  if (presence.length === 0) return null;
123
130
 
@@ -9,6 +9,8 @@ import {
9
9
  type ExportFormat,
10
10
  } from "@/lib/exportImage";
11
11
  import type { StateDef } from "@/types/definition";
12
+ import DocSwitcher from "./DocSwitcher";
13
+ import { docQuery, useRequestedDoc } from "@/lib/docParam";
12
14
 
13
15
  export default function Toolbar() {
14
16
  const definition = useDesignerStore((s) => s.definition);
@@ -36,6 +38,11 @@ export default function Toolbar() {
36
38
  const setRemoteMtime = useDesignerStore((s) => s.setRemoteMtime);
37
39
  const applyRemote = useDesignerStore((s) => s.applyRemote);
38
40
 
41
+ // Save and reload must follow the SAME document the page is viewing —
42
+ // without this, 💾 on a switched document would silently write the default
43
+ // file (the exact mismatch the allowlist exists to prevent).
44
+ const requested = useRequestedDoc();
45
+
39
46
  const fileInputRef = useRef<HTMLInputElement>(null);
40
47
  const [addingState, setAddingState] = useState(false);
41
48
  const [newStateName, setNewStateName] = useState("");
@@ -63,7 +70,7 @@ export default function Toolbar() {
63
70
  const json = exportJson();
64
71
  setSaving(true);
65
72
  try {
66
- const r = await fetch("/api/file", {
73
+ const r = await fetch(`/api/file${docQuery(requested)}`, {
67
74
  method: "PUT",
68
75
  headers: { "Content-Type": "application/json" },
69
76
  body: JSON.stringify({ content: json }),
@@ -115,7 +122,7 @@ export default function Toolbar() {
115
122
 
116
123
  const handleReloadFromDisk = async () => {
117
124
  try {
118
- const r = await fetch("/api/file", { cache: "no-store" });
125
+ const r = await fetch(`/api/file${docQuery(requested)}`, { cache: "no-store" });
119
126
  if (!r.ok) {
120
127
  setRemoteStatus("error", "Reload failed");
121
128
  return;
@@ -198,11 +205,12 @@ export default function Toolbar() {
198
205
  className="hidden"
199
206
  />
200
207
 
201
- {/* File name + bridge status */}
208
+ {/* File name + document switcher + bridge status */}
202
209
  <span className="text-xs text-gray-400 mr-1 truncate max-w-[50vw] md:max-w-none">
203
210
  {fileName ?? "untitled.smdf.json"}
204
211
  {dirty && <span className="text-yellow-500 ml-0.5">●</span>}
205
212
  </span>
213
+ <DocSwitcher />
206
214
  {remoteStatus === "synced" && (
207
215
  <span className="text-[10px] text-emerald-500" title="In sync with disk">⌁ synced</span>
208
216
  )}
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ /**
6
+ * The one client-side source of the requested document (chart steps 2/3/6 of
7
+ * chart_1785683010290): the `?doc=` URL parameter. The browser only ever
8
+ * PASSES the request through — resolution and the root allowlist live on the
9
+ * server (lib/projectFile.ts), and the browser learns the resolved path from
10
+ * the API response exactly as before. No parameter → the default document,
11
+ * byte-identical to the old behavior.
12
+ */
13
+
14
+ const DOC_CHANGE_EVENT = "stateloom:doc-change";
15
+
16
+ export function requestedDoc(): string | null {
17
+ if (typeof window === "undefined") return null;
18
+ return new URLSearchParams(window.location.search).get("doc");
19
+ }
20
+
21
+ /** Query-string tail for API calls: "?doc=…" or "". */
22
+ export function docQuery(doc: string | null): string {
23
+ return doc ? `?doc=${encodeURIComponent(doc)}` : "";
24
+ }
25
+
26
+ /** Switch documents without a reload (step 6): rewrite the URL, tell the
27
+ * providers. History gets an entry so back/forward walk documents too. */
28
+ export function navigateToDoc(doc: string | null): void {
29
+ const url = new URL(window.location.href);
30
+ if (doc) url.searchParams.set("doc", doc);
31
+ else url.searchParams.delete("doc");
32
+ window.history.pushState({}, "", url);
33
+ window.dispatchEvent(new Event(DOC_CHANGE_EVENT));
34
+ }
35
+
36
+ /** The requested doc as reactive state: updates on picker navigation and on
37
+ * back/forward. Server render sees null; the first client effect corrects. */
38
+ export function useRequestedDoc(): string | null {
39
+ const [doc, setDoc] = useState<string | null>(null);
40
+ useEffect(() => {
41
+ const update = () => setDoc(requestedDoc());
42
+ update();
43
+ window.addEventListener("popstate", update);
44
+ window.addEventListener(DOC_CHANGE_EVENT, update);
45
+ return () => {
46
+ window.removeEventListener("popstate", update);
47
+ window.removeEventListener(DOC_CHANGE_EVENT, update);
48
+ };
49
+ }, []);
50
+ return doc;
51
+ }
@@ -36,6 +36,21 @@ export interface LayoutMemory {
36
36
  viewport: Viewport;
37
37
  }
38
38
 
39
+ /** Documents this browser holds layout memory for — the picker's recents
40
+ * (chart_1785683062725: presentation over state that already exists). */
41
+ export function rememberedDocs(): string[] {
42
+ try {
43
+ const docs: string[] = [];
44
+ for (let i = 0; i < window.localStorage.length; i++) {
45
+ const k = window.localStorage.key(i);
46
+ if (k?.startsWith(KEY_PREFIX)) docs.push(k.slice(KEY_PREFIX.length));
47
+ }
48
+ return docs.sort();
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
39
54
  export function memoryKey(docId: string): string {
40
55
  return `${KEY_PREFIX}${docId}`;
41
56
  }
@@ -108,10 +123,13 @@ export function writeMemory(
108
123
  }
109
124
  }
110
125
 
111
- /** The document this browser is remembering for: the resolved project path. */
126
+ /** The document this browser is remembering for: the resolved project path.
127
+ * The requested `?doc=` rides along (step 2) so memory keys follow the
128
+ * document the page actually asked for, not always the default. */
112
129
  async function resolveDocId(): Promise<string> {
113
130
  try {
114
- const r = await fetch("/api/file", { cache: "no-store" });
131
+ const { requestedDoc, docQuery } = await import("./docParam");
132
+ const r = await fetch(`/api/file${docQuery(requestedDoc())}`, { cache: "no-store" });
115
133
  if (r.ok) {
116
134
  const path = (await r.json())?.path;
117
135
  if (typeof path === "string" && path.length > 0) return path;
@@ -1,6 +1,73 @@
1
- import { resolve } from "path";
1
+ import { existsSync, realpathSync } from "fs";
2
+ import { dirname, isAbsolute, resolve, sep } from "path";
2
3
  import { envAlias } from "@miadi/stateloom-protocol";
3
4
 
4
5
  export function getProjectFilePath(): string {
5
6
  return resolve(envAlias("PROJECT_FILE") ?? "./statemachine.smdf.json");
6
7
  }
8
+
9
+ /**
10
+ * Optional-document resolution for the file API (chart chart_1785683022927).
11
+ *
12
+ * The guard is not deferrable and ships in the same change as the parameter:
13
+ * the canvas binds beyond loopback and PUT /api/file writes to disk, so an
14
+ * unconstrained path parameter would be arbitrary file write across the
15
+ * tailnet. A `doc` is admitted only when it is an absolute `.json` path whose
16
+ * real location sits under an allowed root.
17
+ *
18
+ * Roots come from STATELOOM_DOC_ROOTS (SMCRAFT_ twin honored), colon-
19
+ * separated absolute paths. Unset, the allowlist is exactly the directory of
20
+ * the default project file — the pre-parameter surface, unchanged.
21
+ */
22
+ export function allowedDocRoots(): string[] {
23
+ const raw = envAlias("DOC_ROOTS");
24
+ const roots = raw
25
+ ? raw.split(":").map((r) => r.trim()).filter(Boolean)
26
+ : [dirname(getProjectFilePath())];
27
+ const real: string[] = [];
28
+ for (const root of roots) {
29
+ try {
30
+ real.push(realpathSync(resolve(root)));
31
+ } catch {
32
+ // A configured root that does not exist admits nothing — skipped, not fatal.
33
+ }
34
+ }
35
+ return real;
36
+ }
37
+
38
+ export type DocResolution =
39
+ | { ok: true; path: string }
40
+ | { ok: false; error: string };
41
+
42
+ /** Resolve an optional `doc` request parameter to an admitted absolute path.
43
+ * No doc → the default project file, byte-identical to the old behavior. */
44
+ export function resolveDocPath(doc: string | null | undefined): DocResolution {
45
+ if (!doc) return { ok: true, path: getProjectFilePath() };
46
+
47
+ if (!isAbsolute(doc)) {
48
+ return { ok: false, error: "doc must be an absolute path" };
49
+ }
50
+ if (!doc.endsWith(".json")) {
51
+ return { ok: false, error: "doc must name a .json document" };
52
+ }
53
+
54
+ const lexical = resolve(doc);
55
+ // Realpath the file itself when it exists (GET/PUT of a known doc), else its
56
+ // parent (PUT may create the file) — symlinks cannot smuggle a path outside
57
+ // the allowlist either way. A doc whose parent does not exist is refused.
58
+ let anchored: string;
59
+ try {
60
+ anchored = existsSync(lexical)
61
+ ? realpathSync(lexical)
62
+ : resolve(realpathSync(dirname(lexical)), lexical.slice(lexical.lastIndexOf(sep) + 1));
63
+ } catch {
64
+ return { ok: false, error: "doc directory does not exist" };
65
+ }
66
+
67
+ for (const root of allowedDocRoots()) {
68
+ if (anchored === root || anchored.startsWith(root + sep)) {
69
+ return { ok: true, path: anchored };
70
+ }
71
+ }
72
+ return { ok: false, error: "doc is outside the allowed roots" };
73
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.1.0",
3
- "designer": "0.1.0",
2
+ "package": "0.1.1",
3
+ "designer": "0.1.1",
4
4
  "next": "16.1.6"
5
5
  }