@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.
- package/package.json +1 -1
- package/server/.next/BUILD_ID +1 -1
- package/server/.next/build-manifest.json +4 -4
- package/server/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/server/.next/server/app/_global-error/page.js +1 -1
- package/server/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/server/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/server/.next/server/app/_global-error.html +2 -2
- package/server/.next/server/app/_global-error.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/server/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/server/.next/server/app/_not-found/page.js +1 -1
- package/server/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/server/.next/server/app/_not-found.html +1 -1
- package/server/.next/server/app/_not-found.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/server/.next/server/app/api/file/route.js.nft.json +1 -1
- package/server/.next/server/app/api/rispec/route.js.nft.json +1 -1
- package/server/.next/server/app/api/watch/route.js +3 -2
- package/server/.next/server/app/api/watch/route.js.nft.json +1 -1
- package/server/.next/server/app/index.html +1 -1
- package/server/.next/server/app/index.rsc +3 -3
- package/server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/server/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/server/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/server/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/server/.next/server/app/page/build-manifest.json +2 -2
- package/server/.next/server/app/page.js +1 -1
- package/server/.next/server/app/page.js.nft.json +1 -1
- package/server/.next/server/app/page_client-reference-manifest.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__2046b812._.js +2 -2
- package/server/.next/server/chunks/[root-of-the-server]__87bf4b91._.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__95b05635._.js +8 -0
- package/server/.next/server/chunks/ssr/[root-of-the-server]__2955c0d8._.js +3 -0
- package/server/.next/server/chunks/ssr/{[root-of-the-server]__b4e7f753._.js → [root-of-the-server]__385c85a7._.js} +2 -2
- package/server/.next/server/chunks/ssr/[root-of-the-server]__a977f174._.js +3 -0
- package/server/.next/server/chunks/ssr/[root-of-the-server]__ac84e72e._.js +3 -0
- package/server/.next/server/chunks/ssr/[root-of-the-server]__edbb995c._.js +3 -3
- package/server/.next/server/chunks/ssr/{[root-of-the-server]__9cafe12a._.js → [root-of-the-server]__f25b51be._.js} +2 -2
- package/server/.next/server/middleware-build-manifest.js +2 -2
- package/server/.next/server/pages/404.html +1 -1
- package/server/.next/server/pages/500.html +2 -2
- package/server/.next/static/chunks/2c679fab817159f2.css +1 -0
- package/server/.next/static/chunks/{8a03c0d2436d4c0f.js → 662d1b0ce457c16b.js} +1 -1
- package/server/.next/static/chunks/{367d28d76d2be05f.js → 7921d7703dd690a6.js} +4 -4
- package/server/.next/static/chunks/aea5b1716c68391c.js +1 -0
- package/server/.next/static/chunks/{turbopack-8f57da603e40457d.js → turbopack-34fc7de1568e2a7f.js} +1 -1
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
- package/server/src/app/api/file/route.ts +20 -6
- package/server/src/app/api/watch/route.ts +11 -3
- package/server/src/components/BridgeProvider.tsx +10 -3
- package/server/src/components/DocSwitcher.tsx +84 -0
- package/server/src/components/SocketBridgeProvider.tsx +11 -4
- package/server/src/components/Toolbar.tsx +11 -3
- package/server/src/lib/docParam.ts +51 -0
- package/server/src/lib/layoutMemory.ts +20 -2
- package/server/src/lib/projectFile.ts +68 -1
- package/server/stateloom-web.json +2 -2
- package/server/tsconfig.tsbuildinfo +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__0a18d740._.js +0 -8
- package/server/.next/server/chunks/ssr/[root-of-the-server]__517c9b38._.js +0 -3
- package/server/.next/server/chunks/ssr/[root-of-the-server]__632c82d7._.js +0 -3
- package/server/.next/server/chunks/ssr/[root-of-the-server]__ecf19999._.js +0 -3
- package/server/.next/static/chunks/b6bd7719e3445512.css +0 -1
- /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → vPEqV8SJpTDJWGy_fRuh4}/_buildManifest.js +0 -0
- /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → vPEqV8SJpTDJWGy_fRuh4}/_clientMiddlewareManifest.json +0 -0
- /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)))}]);
|
package/server/.next/static/chunks/{turbopack-8f57da603e40457d.js → turbopack-34fc7de1568e2a7f.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,{otherChunks:["static/chunks/
|
|
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)})();
|
package/server/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stateloom-web",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
9
|
+
"version": "0.1.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@miadi/stateloom-client": "file:../bridge-client",
|
|
12
12
|
"@miadi/stateloom-protocol": "file:../bridge-protocol",
|
package/server/package.json
CHANGED
|
@@ -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 {
|
|
3
|
+
import { resolveDocPath } from "@/lib/projectFile";
|
|
4
4
|
|
|
5
5
|
export const dynamic = "force-dynamic";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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:
|
|
18
|
-
const
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
119
|
-
// /api/config
|
|
120
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
+
}
|