@immediately-run/sdk 0.49.0 → 0.50.0
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/dist/components/MainContent.cjs +16 -3
- package/dist/components/MainContent.cjs.map +1 -1
- package/dist/components/MainContent.js +16 -3
- package/dist/components/MainContent.js.map +1 -1
- package/dist/fs.cjs.map +1 -1
- package/dist/fs.js.map +1 -1
- package/dist/index.cjs +6 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/injectedBundler.cjs.map +1 -1
- package/dist/injectedBundler.d.cts +18 -2
- package/dist/injectedBundler.d.ts +18 -2
- package/dist/injectedBundler.js.map +1 -1
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +4 -2
|
@@ -28,6 +28,7 @@ var import_react = require("react");
|
|
|
28
28
|
var import_react_error_boundary = require("react-error-boundary");
|
|
29
29
|
var import_routing = require("../routing");
|
|
30
30
|
var import_urlUtils = require("../urlUtils");
|
|
31
|
+
var import_fs = require("../fs");
|
|
31
32
|
var import_defaults = require("./defaults");
|
|
32
33
|
const candidates = [
|
|
33
34
|
"/src/App.tsx",
|
|
@@ -41,9 +42,21 @@ const candidates = [
|
|
|
41
42
|
"/README.html"
|
|
42
43
|
];
|
|
43
44
|
const fileExists = async (path) => {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const fs = (0, import_fs.sandboxFs)();
|
|
46
|
+
if (!fs) return [path, false];
|
|
47
|
+
const absolute = (0, import_urlUtils.underAppRoot)(path);
|
|
48
|
+
try {
|
|
49
|
+
const stat = fs.promises?.stat ?? fs.stat;
|
|
50
|
+
if (typeof stat === "function") {
|
|
51
|
+
await stat.call(fs.promises ?? fs, absolute);
|
|
52
|
+
return [path, true];
|
|
53
|
+
}
|
|
54
|
+
const holder = fs.promises ?? fs;
|
|
55
|
+
await holder.readFile(absolute);
|
|
56
|
+
return [path, true];
|
|
57
|
+
} catch {
|
|
58
|
+
return [path, false];
|
|
59
|
+
}
|
|
47
60
|
};
|
|
48
61
|
const MainContentRedirect = ({ filename }) => {
|
|
49
62
|
const url = (0, import_routing.useTinkerableLink)(filename);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/MainContent.tsx"],"sourcesContent":["import { Suspense, use, useMemo } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { navigate, useTinkerableLink } from '../routing';\nimport { FILES_PREFIX, underAppRoot } from '../urlUtils';\n\nimport { defaultErrorComponent, defaultLoadingComponent } from './defaults';\n\n// Repo-relative candidate paths. These are kept repo-relative because the\n// returned path is reused below to build the redirect URL (which is anchored to\n// `/app` by the file router); only the filesystem existence check is resolved\n// under `APP_ROOT`, since `bundler.fs` is rooted at `/`.\nconst candidates = [\n '/src/App.tsx',\n '/src/App.ts',\n '/src/App.js',\n '/App.tsx',\n '/App.ts',\n '/App.js',\n '/README.md',\n '/README.mdx',\n '/README.html',\n];\n\nconst fileExists = async (path: string): Promise<[string, boolean]> => {\n
|
|
1
|
+
{"version":3,"sources":["../../src/components/MainContent.tsx"],"sourcesContent":["import { Suspense, use, useMemo } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { navigate, useTinkerableLink } from '../routing';\nimport { FILES_PREFIX, underAppRoot } from '../urlUtils';\nimport { sandboxFs, type SandboxFsPort } from '../fs';\n\nimport { defaultErrorComponent, defaultLoadingComponent } from './defaults';\n\n// Repo-relative candidate paths. These are kept repo-relative because the\n// returned path is reused below to build the redirect URL (which is anchored to\n// `/app` by the file router); only the filesystem existence check is resolved\n// under `APP_ROOT`, since `bundler.fs` is rooted at `/`.\nconst candidates = [\n '/src/App.tsx',\n '/src/App.ts',\n '/src/App.js',\n '/App.tsx',\n '/App.ts',\n '/App.js',\n '/README.md',\n '/README.mdx',\n '/README.html',\n];\n\n// R3-278: the existence probe goes through the SDK's OWN fs surface (`sandboxFs`),\n// not the injected bundler — this was the last direct `bundler.*` read on a\n// PUBLIC component, and the one with no fallback: with no injected bundler\n// (npm-fetched SDK, `vite dev`, pre-boot) the old read THREW. Unavailable fs\n// simply answers `false` for every candidate (the \"no main content file\" path),\n// so the component degrades instead of crashing — the regression case this\n// item exists to close.\nconst fileExists = async (path: string): Promise<[string, boolean]> => {\n const fs = sandboxFs();\n if (!fs) return [path, false];\n const absolute = underAppRoot(path);\n try {\n // stat when the port has it, else readFile (dirs reject with EISDIR → false).\n const stat = (fs.promises as { stat?: Function } | undefined)?.stat ?? (fs as { stat?: Function }).stat;\n if (typeof stat === 'function') {\n await stat.call(fs.promises ?? fs, absolute);\n return [path, true];\n }\n const holder = fs.promises ?? (fs as { readFile: NonNullable<SandboxFsPort['readFile']> });\n await holder.readFile(absolute);\n return [path, true];\n } catch {\n return [path, false];\n }\n};\n\nexport const MainContentRedirect = ({ filename }: { filename: string }) => {\n const url = useTinkerableLink(filename);\n navigate(url);\n return <>Redirecting to {filename}</>;\n};\n\nexport const MainContentInner = ({\n candidatesExistPromise,\n}: {\n candidatesExistPromise: Promise<[string, boolean][]>;\n}) => {\n const candidatesExist = use(candidatesExistPromise);\n const filename = candidatesExist.find(([_, exists]) => exists)?.[0];\n if (!filename) {\n // todo: show file list\n throw new Error(`No main content file present`);\n }\n return <MainContentRedirect filename={FILES_PREFIX + filename} />;\n};\n\nexport const MainContent = ({\n LoadingComponent = defaultLoadingComponent,\n ErrorComponent = defaultErrorComponent,\n}: {\n LoadingComponent?: typeof defaultLoadingComponent;\n ErrorComponent?: typeof defaultErrorComponent;\n} = {}) => {\n // TODO: when to invalidate?\n const candidatesExistPromise = useMemo(() => Promise.all(candidates.map(fileExists)), []);\n return (\n <ErrorBoundary fallbackRender={ErrorComponent}>\n <Suspense fallback={<LoadingComponent />}>\n <MainContentInner candidatesExistPromise={candidatesExistPromise} />\n </Suspense>\n </ErrorBoundary>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqDS;AArDT,mBAAuC;AACvC,kCAA8B;AAC9B,qBAA4C;AAC5C,sBAA2C;AAC3C,gBAA8C;AAE9C,sBAA+D;AAM/D,MAAM,aAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AASA,MAAM,aAAa,OAAO,SAA6C;AACrE,QAAM,SAAK,qBAAU;AACrB,MAAI,CAAC,GAAI,QAAO,CAAC,MAAM,KAAK;AAC5B,QAAM,eAAW,8BAAa,IAAI;AAClC,MAAI;AAEF,UAAM,OAAQ,GAAG,UAA8C,QAAS,GAA2B;AACnG,QAAI,OAAO,SAAS,YAAY;AAC9B,YAAM,KAAK,KAAK,GAAG,YAAY,IAAI,QAAQ;AAC3C,aAAO,CAAC,MAAM,IAAI;AAAA,IACpB;AACA,UAAM,SAAS,GAAG,YAAa;AAC/B,UAAM,OAAO,SAAS,QAAQ;AAC9B,WAAO,CAAC,MAAM,IAAI;AAAA,EACpB,QAAQ;AACN,WAAO,CAAC,MAAM,KAAK;AAAA,EACrB;AACF;AAEO,MAAM,sBAAsB,CAAC,EAAE,SAAS,MAA4B;AACzE,QAAM,UAAM,kCAAkB,QAAQ;AACtC,+BAAS,GAAG;AACZ,SAAO,4EAAE;AAAA;AAAA,IAAgB;AAAA,KAAS;AACpC;AAEO,MAAM,mBAAmB,CAAC;AAAA,EAC/B;AACF,MAEM;AACJ,QAAM,sBAAkB,kBAAI,sBAAsB;AAClD,QAAM,WAAW,gBAAgB,KAAK,CAAC,CAAC,GAAG,MAAM,MAAM,MAAM,IAAI,CAAC;AAClE,MAAI,CAAC,UAAU;AAEb,UAAM,IAAI,MAAM,8BAA8B;AAAA,EAChD;AACA,SAAO,4CAAC,uBAAoB,UAAU,+BAAe,UAAU;AACjE;AAEO,MAAM,cAAc,CAAC;AAAA,EAC1B,mBAAmB;AAAA,EACnB,iBAAiB;AACnB,IAGI,CAAC,MAAM;AAET,QAAM,6BAAyB,sBAAQ,MAAM,QAAQ,IAAI,WAAW,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;AACxF,SACE,4CAAC,6CAAc,gBAAgB,gBAC7B,sDAAC,yBAAS,UAAU,4CAAC,oBAAiB,GACpC,sDAAC,oBAAiB,wBAAgD,GACpE,GACF;AAEJ;","names":[]}
|
|
@@ -4,6 +4,7 @@ import { Suspense, use, useMemo } from "react";
|
|
|
4
4
|
import { ErrorBoundary } from "react-error-boundary";
|
|
5
5
|
import { navigate, useTinkerableLink } from "../routing";
|
|
6
6
|
import { FILES_PREFIX, underAppRoot } from "../urlUtils";
|
|
7
|
+
import { sandboxFs } from "../fs";
|
|
7
8
|
import { defaultErrorComponent, defaultLoadingComponent } from "./defaults";
|
|
8
9
|
const candidates = [
|
|
9
10
|
"/src/App.tsx",
|
|
@@ -17,9 +18,21 @@ const candidates = [
|
|
|
17
18
|
"/README.html"
|
|
18
19
|
];
|
|
19
20
|
const fileExists = async (path) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const fs = sandboxFs();
|
|
22
|
+
if (!fs) return [path, false];
|
|
23
|
+
const absolute = underAppRoot(path);
|
|
24
|
+
try {
|
|
25
|
+
const stat = fs.promises?.stat ?? fs.stat;
|
|
26
|
+
if (typeof stat === "function") {
|
|
27
|
+
await stat.call(fs.promises ?? fs, absolute);
|
|
28
|
+
return [path, true];
|
|
29
|
+
}
|
|
30
|
+
const holder = fs.promises ?? fs;
|
|
31
|
+
await holder.readFile(absolute);
|
|
32
|
+
return [path, true];
|
|
33
|
+
} catch {
|
|
34
|
+
return [path, false];
|
|
35
|
+
}
|
|
23
36
|
};
|
|
24
37
|
const MainContentRedirect = ({ filename }) => {
|
|
25
38
|
const url = useTinkerableLink(filename);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/MainContent.tsx"],"sourcesContent":["import { Suspense, use, useMemo } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { navigate, useTinkerableLink } from '../routing';\nimport { FILES_PREFIX, underAppRoot } from '../urlUtils';\n\nimport { defaultErrorComponent, defaultLoadingComponent } from './defaults';\n\n// Repo-relative candidate paths. These are kept repo-relative because the\n// returned path is reused below to build the redirect URL (which is anchored to\n// `/app` by the file router); only the filesystem existence check is resolved\n// under `APP_ROOT`, since `bundler.fs` is rooted at `/`.\nconst candidates = [\n '/src/App.tsx',\n '/src/App.ts',\n '/src/App.js',\n '/App.tsx',\n '/App.ts',\n '/App.js',\n '/README.md',\n '/README.mdx',\n '/README.html',\n];\n\nconst fileExists = async (path: string): Promise<[string, boolean]> => {\n
|
|
1
|
+
{"version":3,"sources":["../../src/components/MainContent.tsx"],"sourcesContent":["import { Suspense, use, useMemo } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { navigate, useTinkerableLink } from '../routing';\nimport { FILES_PREFIX, underAppRoot } from '../urlUtils';\nimport { sandboxFs, type SandboxFsPort } from '../fs';\n\nimport { defaultErrorComponent, defaultLoadingComponent } from './defaults';\n\n// Repo-relative candidate paths. These are kept repo-relative because the\n// returned path is reused below to build the redirect URL (which is anchored to\n// `/app` by the file router); only the filesystem existence check is resolved\n// under `APP_ROOT`, since `bundler.fs` is rooted at `/`.\nconst candidates = [\n '/src/App.tsx',\n '/src/App.ts',\n '/src/App.js',\n '/App.tsx',\n '/App.ts',\n '/App.js',\n '/README.md',\n '/README.mdx',\n '/README.html',\n];\n\n// R3-278: the existence probe goes through the SDK's OWN fs surface (`sandboxFs`),\n// not the injected bundler — this was the last direct `bundler.*` read on a\n// PUBLIC component, and the one with no fallback: with no injected bundler\n// (npm-fetched SDK, `vite dev`, pre-boot) the old read THREW. Unavailable fs\n// simply answers `false` for every candidate (the \"no main content file\" path),\n// so the component degrades instead of crashing — the regression case this\n// item exists to close.\nconst fileExists = async (path: string): Promise<[string, boolean]> => {\n const fs = sandboxFs();\n if (!fs) return [path, false];\n const absolute = underAppRoot(path);\n try {\n // stat when the port has it, else readFile (dirs reject with EISDIR → false).\n const stat = (fs.promises as { stat?: Function } | undefined)?.stat ?? (fs as { stat?: Function }).stat;\n if (typeof stat === 'function') {\n await stat.call(fs.promises ?? fs, absolute);\n return [path, true];\n }\n const holder = fs.promises ?? (fs as { readFile: NonNullable<SandboxFsPort['readFile']> });\n await holder.readFile(absolute);\n return [path, true];\n } catch {\n return [path, false];\n }\n};\n\nexport const MainContentRedirect = ({ filename }: { filename: string }) => {\n const url = useTinkerableLink(filename);\n navigate(url);\n return <>Redirecting to {filename}</>;\n};\n\nexport const MainContentInner = ({\n candidatesExistPromise,\n}: {\n candidatesExistPromise: Promise<[string, boolean][]>;\n}) => {\n const candidatesExist = use(candidatesExistPromise);\n const filename = candidatesExist.find(([_, exists]) => exists)?.[0];\n if (!filename) {\n // todo: show file list\n throw new Error(`No main content file present`);\n }\n return <MainContentRedirect filename={FILES_PREFIX + filename} />;\n};\n\nexport const MainContent = ({\n LoadingComponent = defaultLoadingComponent,\n ErrorComponent = defaultErrorComponent,\n}: {\n LoadingComponent?: typeof defaultLoadingComponent;\n ErrorComponent?: typeof defaultErrorComponent;\n} = {}) => {\n // TODO: when to invalidate?\n const candidatesExistPromise = useMemo(() => Promise.all(candidates.map(fileExists)), []);\n return (\n <ErrorBoundary fallbackRender={ErrorComponent}>\n <Suspense fallback={<LoadingComponent />}>\n <MainContentInner candidatesExistPromise={candidatesExistPromise} />\n </Suspense>\n </ErrorBoundary>\n );\n};\n"],"mappings":";AAqDS,mBAcA,KAdA;AArDT,SAAS,UAAU,KAAK,eAAe;AACvC,SAAS,qBAAqB;AAC9B,SAAS,UAAU,yBAAyB;AAC5C,SAAS,cAAc,oBAAoB;AAC3C,SAAS,iBAAqC;AAE9C,SAAS,uBAAuB,+BAA+B;AAM/D,MAAM,aAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AASA,MAAM,aAAa,OAAO,SAA6C;AACrE,QAAM,KAAK,UAAU;AACrB,MAAI,CAAC,GAAI,QAAO,CAAC,MAAM,KAAK;AAC5B,QAAM,WAAW,aAAa,IAAI;AAClC,MAAI;AAEF,UAAM,OAAQ,GAAG,UAA8C,QAAS,GAA2B;AACnG,QAAI,OAAO,SAAS,YAAY;AAC9B,YAAM,KAAK,KAAK,GAAG,YAAY,IAAI,QAAQ;AAC3C,aAAO,CAAC,MAAM,IAAI;AAAA,IACpB;AACA,UAAM,SAAS,GAAG,YAAa;AAC/B,UAAM,OAAO,SAAS,QAAQ;AAC9B,WAAO,CAAC,MAAM,IAAI;AAAA,EACpB,QAAQ;AACN,WAAO,CAAC,MAAM,KAAK;AAAA,EACrB;AACF;AAEO,MAAM,sBAAsB,CAAC,EAAE,SAAS,MAA4B;AACzE,QAAM,MAAM,kBAAkB,QAAQ;AACtC,WAAS,GAAG;AACZ,SAAO,iCAAE;AAAA;AAAA,IAAgB;AAAA,KAAS;AACpC;AAEO,MAAM,mBAAmB,CAAC;AAAA,EAC/B;AACF,MAEM;AACJ,QAAM,kBAAkB,IAAI,sBAAsB;AAClD,QAAM,WAAW,gBAAgB,KAAK,CAAC,CAAC,GAAG,MAAM,MAAM,MAAM,IAAI,CAAC;AAClE,MAAI,CAAC,UAAU;AAEb,UAAM,IAAI,MAAM,8BAA8B;AAAA,EAChD;AACA,SAAO,oBAAC,uBAAoB,UAAU,eAAe,UAAU;AACjE;AAEO,MAAM,cAAc,CAAC;AAAA,EAC1B,mBAAmB;AAAA,EACnB,iBAAiB;AACnB,IAGI,CAAC,MAAM;AAET,QAAM,yBAAyB,QAAQ,MAAM,QAAQ,IAAI,WAAW,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;AACxF,SACE,oBAAC,iBAAc,gBAAgB,gBAC7B,8BAAC,YAAS,UAAU,oBAAC,oBAAiB,GACpC,8BAAC,oBAAiB,wBAAgD,GACpE,GACF;AAEJ;","names":[]}
|
package/dist/fs.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\nimport { onFsChange } from './onFsChange';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean => typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\n// Lazily constructed so merely *importing* this module doesn't touch the\n// TextEncoder/TextDecoder globals — some non-DOM test/build environments only\n// provide them on demand, and no image/URL path needs them at all.\nlet _decoder: TextDecoder | undefined;\nlet _encoder: TextEncoder | undefined;\nconst decoder = (): TextDecoder => (_decoder ??= new TextDecoder());\nconst encoder = (): TextEncoder => (_encoder ??= new TextEncoder());\n\n// Extension → MIME type for the kinds an app displays inline. Images first (the\n// common case — `<img src>` off a mount), plus a couple of adjacent binary kinds.\n// Deliberately small: `mimeTypeFor` returns undefined for anything not here and\n// callers fall back to `application/octet-stream`.\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n};\n\n/**\n * Best-effort MIME type from a filename's extension — mainly image kinds\n * (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the\n * extension isn't recognized (the caller falls back to `application/octet-stream`).\n * Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an\n * app can label a Blob it builds itself.\n */\nexport function mimeTypeFor(path: string): string | undefined {\n const dot = path.lastIndexOf('.');\n if (dot < 0) return undefined;\n return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];\n}\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path =\n '/' +\n relPath\n .split('/')\n .filter((s) => s && s !== '.')\n .join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the\n * extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to\n * `application/octet-stream`). The building block for downloads and object URLs. */\n readBlob(relPath: string, opts?: { type?: string }): Promise<Blob>;\n /** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the\n * fix for \"an opaque-origin iframe can't fetch a mount path\". Returns the `url`\n * and a `revoke()` you MUST call when done (typically on unmount) or the URL\n * leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke\n * for you; reach for this directly only outside React. */\n readObjectUrl(relPath: string, opts?: { type?: string }): Promise<{ url: string; revoke: () => void }>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n /** Subscribe to changes to files in this mount — the mount-scoped projection of\n * the host working-tree change stream (`onFsChange`), so a viewer re-reads an\n * affected file instead of polling (SDK_FS_SURFACE_SPEC §5). The callback gets\n * the changed paths RELATIVE to this mount (feed them straight back into\n * `readFile`/`stat`/…). Returns an unsubscribe fn.\n *\n * **Working-tree-only in v1 (an honest gap, O2):** the host push channel carries\n * only working-tree changes, so `onChange` on a NON-working-tree mount (a space)\n * is an inert subscription that never fires until that channel lands. Like\n * `onFsChange`, origin-exclusion (ignoring the echo of your own write) is the\n * caller's responsibility. */\n onChange(cb: (changedRelPaths: string[]) => void): () => void;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises => port.promises ?? (port as unknown as NodeFsPromises);\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes = typeof data === 'string' ? encoder().encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder().decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async readBlob(relPath, opts) {\n const bytes = await api.readFile(relPath);\n const type = opts?.type ?? mimeTypeFor(relPath) ?? 'application/octet-stream';\n return new Blob([bytes as BlobPart], { type });\n },\n async readObjectUrl(relPath, opts) {\n const blob = await api.readBlob(relPath, opts);\n const url = URL.createObjectURL(blob);\n return { url, revoke: () => URL.revokeObjectURL(url) };\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder().encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n onChange(cb) {\n // §5 — mount-scoped projection of the working-tree change channel\n // (`onFsChange`). v1 is WORKING-TREE-ONLY: the host pushes only working-tree\n // changes, so a non-working-tree mount (a space) has no channel yet (O2) and\n // gets an inert subscription rather than another mount's paths leaking in.\n if (root !== getAppMountPath()) {\n return () => {}; // no channel for this mount — inert (honest v1 gap)\n }\n return onFsChange((change) => {\n // Skip the empty pre-first-event initial batch; forward only real changes,\n // as mount-relative paths (drop the repo-relative leading slash) so they\n // feed straight back into readFile/stat/etc.\n if (change.paths.length === 0) return;\n cb(change.paths.map((p) => p.replace(/^\\/+/, '')));\n });\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\nimport { onFsChange } from './onFsChange';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean => typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n // DEPRECATION WINDOW (opened 2026-08-25, R3-278): this `bundler.fs.layers` fallback\n // is injected-bundler API reading — the supported surface is the\n // `__sandpackSharedFs` discovery global above (and `openFs`/`sandboxFs` themselves).\n // Kept through the SDK_PACKAGING_SPEC §9 window; new code must not read bundler.*\n // (scripts/check-bundler-reads.mjs).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\n// Lazily constructed so merely *importing* this module doesn't touch the\n// TextEncoder/TextDecoder globals — some non-DOM test/build environments only\n// provide them on demand, and no image/URL path needs them at all.\nlet _decoder: TextDecoder | undefined;\nlet _encoder: TextEncoder | undefined;\nconst decoder = (): TextDecoder => (_decoder ??= new TextDecoder());\nconst encoder = (): TextEncoder => (_encoder ??= new TextEncoder());\n\n// Extension → MIME type for the kinds an app displays inline. Images first (the\n// common case — `<img src>` off a mount), plus a couple of adjacent binary kinds.\n// Deliberately small: `mimeTypeFor` returns undefined for anything not here and\n// callers fall back to `application/octet-stream`.\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n};\n\n/**\n * Best-effort MIME type from a filename's extension — mainly image kinds\n * (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the\n * extension isn't recognized (the caller falls back to `application/octet-stream`).\n * Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an\n * app can label a Blob it builds itself.\n */\nexport function mimeTypeFor(path: string): string | undefined {\n const dot = path.lastIndexOf('.');\n if (dot < 0) return undefined;\n return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];\n}\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path =\n '/' +\n relPath\n .split('/')\n .filter((s) => s && s !== '.')\n .join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the\n * extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to\n * `application/octet-stream`). The building block for downloads and object URLs. */\n readBlob(relPath: string, opts?: { type?: string }): Promise<Blob>;\n /** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the\n * fix for \"an opaque-origin iframe can't fetch a mount path\". Returns the `url`\n * and a `revoke()` you MUST call when done (typically on unmount) or the URL\n * leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke\n * for you; reach for this directly only outside React. */\n readObjectUrl(relPath: string, opts?: { type?: string }): Promise<{ url: string; revoke: () => void }>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n /** Subscribe to changes to files in this mount — the mount-scoped projection of\n * the host working-tree change stream (`onFsChange`), so a viewer re-reads an\n * affected file instead of polling (SDK_FS_SURFACE_SPEC §5). The callback gets\n * the changed paths RELATIVE to this mount (feed them straight back into\n * `readFile`/`stat`/…). Returns an unsubscribe fn.\n *\n * **Working-tree-only in v1 (an honest gap, O2):** the host push channel carries\n * only working-tree changes, so `onChange` on a NON-working-tree mount (a space)\n * is an inert subscription that never fires until that channel lands. Like\n * `onFsChange`, origin-exclusion (ignoring the echo of your own write) is the\n * caller's responsibility. */\n onChange(cb: (changedRelPaths: string[]) => void): () => void;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises => port.promises ?? (port as unknown as NodeFsPromises);\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes = typeof data === 'string' ? encoder().encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder().decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async readBlob(relPath, opts) {\n const bytes = await api.readFile(relPath);\n const type = opts?.type ?? mimeTypeFor(relPath) ?? 'application/octet-stream';\n return new Blob([bytes as BlobPart], { type });\n },\n async readObjectUrl(relPath, opts) {\n const blob = await api.readBlob(relPath, opts);\n const url = URL.createObjectURL(blob);\n return { url, revoke: () => URL.revokeObjectURL(url) };\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder().encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n onChange(cb) {\n // §5 — mount-scoped projection of the working-tree change channel\n // (`onFsChange`). v1 is WORKING-TREE-ONLY: the host pushes only working-tree\n // changes, so a non-working-tree mount (a space) has no channel yet (O2) and\n // gets an inert subscription rather than another mount's paths leaking in.\n if (root !== getAppMountPath()) {\n return () => {}; // no channel for this mount — inert (honest v1 gap)\n }\n return onFsChange((change) => {\n // Skip the empty pre-first-event initial batch; forward only real changes,\n // as mount-relative paths (drop the repo-relative leading slash) so they\n // feed straight back into readFile/stat/etc.\n if (change.paths.length === 0) return;\n cb(change.paths.map((p) => p.replace(/^\\/+/, '')));\n });\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
|
package/dist/fs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\nimport { onFsChange } from './onFsChange';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean => typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\n// Lazily constructed so merely *importing* this module doesn't touch the\n// TextEncoder/TextDecoder globals — some non-DOM test/build environments only\n// provide them on demand, and no image/URL path needs them at all.\nlet _decoder: TextDecoder | undefined;\nlet _encoder: TextEncoder | undefined;\nconst decoder = (): TextDecoder => (_decoder ??= new TextDecoder());\nconst encoder = (): TextEncoder => (_encoder ??= new TextEncoder());\n\n// Extension → MIME type for the kinds an app displays inline. Images first (the\n// common case — `<img src>` off a mount), plus a couple of adjacent binary kinds.\n// Deliberately small: `mimeTypeFor` returns undefined for anything not here and\n// callers fall back to `application/octet-stream`.\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n};\n\n/**\n * Best-effort MIME type from a filename's extension — mainly image kinds\n * (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the\n * extension isn't recognized (the caller falls back to `application/octet-stream`).\n * Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an\n * app can label a Blob it builds itself.\n */\nexport function mimeTypeFor(path: string): string | undefined {\n const dot = path.lastIndexOf('.');\n if (dot < 0) return undefined;\n return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];\n}\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path =\n '/' +\n relPath\n .split('/')\n .filter((s) => s && s !== '.')\n .join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the\n * extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to\n * `application/octet-stream`). The building block for downloads and object URLs. */\n readBlob(relPath: string, opts?: { type?: string }): Promise<Blob>;\n /** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the\n * fix for \"an opaque-origin iframe can't fetch a mount path\". Returns the `url`\n * and a `revoke()` you MUST call when done (typically on unmount) or the URL\n * leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke\n * for you; reach for this directly only outside React. */\n readObjectUrl(relPath: string, opts?: { type?: string }): Promise<{ url: string; revoke: () => void }>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n /** Subscribe to changes to files in this mount — the mount-scoped projection of\n * the host working-tree change stream (`onFsChange`), so a viewer re-reads an\n * affected file instead of polling (SDK_FS_SURFACE_SPEC §5). The callback gets\n * the changed paths RELATIVE to this mount (feed them straight back into\n * `readFile`/`stat`/…). Returns an unsubscribe fn.\n *\n * **Working-tree-only in v1 (an honest gap, O2):** the host push channel carries\n * only working-tree changes, so `onChange` on a NON-working-tree mount (a space)\n * is an inert subscription that never fires until that channel lands. Like\n * `onFsChange`, origin-exclusion (ignoring the echo of your own write) is the\n * caller's responsibility. */\n onChange(cb: (changedRelPaths: string[]) => void): () => void;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises => port.promises ?? (port as unknown as NodeFsPromises);\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes = typeof data === 'string' ? encoder().encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder().decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async readBlob(relPath, opts) {\n const bytes = await api.readFile(relPath);\n const type = opts?.type ?? mimeTypeFor(relPath) ?? 'application/octet-stream';\n return new Blob([bytes as BlobPart], { type });\n },\n async readObjectUrl(relPath, opts) {\n const blob = await api.readBlob(relPath, opts);\n const url = URL.createObjectURL(blob);\n return { url, revoke: () => URL.revokeObjectURL(url) };\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder().encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n onChange(cb) {\n // §5 — mount-scoped projection of the working-tree change channel\n // (`onFsChange`). v1 is WORKING-TREE-ONLY: the host pushes only working-tree\n // changes, so a non-working-tree mount (a space) has no channel yet (O2) and\n // gets an inert subscription rather than another mount's paths leaking in.\n if (root !== getAppMountPath()) {\n return () => {}; // no channel for this mount — inert (honest v1 gap)\n }\n return onFsChange((change) => {\n // Skip the empty pre-first-event initial batch; forward only real changes,\n // as mount-relative paths (drop the repo-relative leading slash) so they\n // feed straight back into readFile/stat/etc.\n if (change.paths.length === 0) return;\n cb(change.paths.map((p) => p.replace(/^\\/+/, '')));\n });\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\nimport { onFsChange } from './onFsChange';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean => typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n // DEPRECATION WINDOW (opened 2026-08-25, R3-278): this `bundler.fs.layers` fallback\n // is injected-bundler API reading — the supported surface is the\n // `__sandpackSharedFs` discovery global above (and `openFs`/`sandboxFs` themselves).\n // Kept through the SDK_PACKAGING_SPEC §9 window; new code must not read bundler.*\n // (scripts/check-bundler-reads.mjs).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\n// Lazily constructed so merely *importing* this module doesn't touch the\n// TextEncoder/TextDecoder globals — some non-DOM test/build environments only\n// provide them on demand, and no image/URL path needs them at all.\nlet _decoder: TextDecoder | undefined;\nlet _encoder: TextEncoder | undefined;\nconst decoder = (): TextDecoder => (_decoder ??= new TextDecoder());\nconst encoder = (): TextEncoder => (_encoder ??= new TextEncoder());\n\n// Extension → MIME type for the kinds an app displays inline. Images first (the\n// common case — `<img src>` off a mount), plus a couple of adjacent binary kinds.\n// Deliberately small: `mimeTypeFor` returns undefined for anything not here and\n// callers fall back to `application/octet-stream`.\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n};\n\n/**\n * Best-effort MIME type from a filename's extension — mainly image kinds\n * (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the\n * extension isn't recognized (the caller falls back to `application/octet-stream`).\n * Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an\n * app can label a Blob it builds itself.\n */\nexport function mimeTypeFor(path: string): string | undefined {\n const dot = path.lastIndexOf('.');\n if (dot < 0) return undefined;\n return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];\n}\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path =\n '/' +\n relPath\n .split('/')\n .filter((s) => s && s !== '.')\n .join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the\n * extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to\n * `application/octet-stream`). The building block for downloads and object URLs. */\n readBlob(relPath: string, opts?: { type?: string }): Promise<Blob>;\n /** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the\n * fix for \"an opaque-origin iframe can't fetch a mount path\". Returns the `url`\n * and a `revoke()` you MUST call when done (typically on unmount) or the URL\n * leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke\n * for you; reach for this directly only outside React. */\n readObjectUrl(relPath: string, opts?: { type?: string }): Promise<{ url: string; revoke: () => void }>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n /** Subscribe to changes to files in this mount — the mount-scoped projection of\n * the host working-tree change stream (`onFsChange`), so a viewer re-reads an\n * affected file instead of polling (SDK_FS_SURFACE_SPEC §5). The callback gets\n * the changed paths RELATIVE to this mount (feed them straight back into\n * `readFile`/`stat`/…). Returns an unsubscribe fn.\n *\n * **Working-tree-only in v1 (an honest gap, O2):** the host push channel carries\n * only working-tree changes, so `onChange` on a NON-working-tree mount (a space)\n * is an inert subscription that never fires until that channel lands. Like\n * `onFsChange`, origin-exclusion (ignoring the echo of your own write) is the\n * caller's responsibility. */\n onChange(cb: (changedRelPaths: string[]) => void): () => void;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises => port.promises ?? (port as unknown as NodeFsPromises);\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes = typeof data === 'string' ? encoder().encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder().decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async readBlob(relPath, opts) {\n const bytes = await api.readFile(relPath);\n const type = opts?.type ?? mimeTypeFor(relPath) ?? 'application/octet-stream';\n return new Blob([bytes as BlobPart], { type });\n },\n async readObjectUrl(relPath, opts) {\n const blob = await api.readBlob(relPath, opts);\n const url = URL.createObjectURL(blob);\n return { url, revoke: () => URL.revokeObjectURL(url) };\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder().encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n onChange(cb) {\n // §5 — mount-scoped projection of the working-tree change channel\n // (`onFsChange`). v1 is WORKING-TREE-ONLY: the host pushes only working-tree\n // changes, so a non-working-tree mount (a space) has no channel yet (O2) and\n // gets an inert subscription rather than another mount's paths leaking in.\n if (root !== getAppMountPath()) {\n return () => {}; // no channel for this mount — inert (honest v1 gap)\n }\n return onFsChange((change) => {\n // Skip the empty pre-first-event initial batch; forward only real changes,\n // as mount-relative paths (drop the repo-relative leading slash) so they\n // feed straight back into readFile/stat/etc.\n if (change.paths.length === 0) return;\n cb(change.paths.map((p) => p.replace(/^\\/+/, '')));\n });\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -19,7 +19,9 @@ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "defau
|
|
|
19
19
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
20
|
var index_exports = {};
|
|
21
21
|
__export(index_exports, {
|
|
22
|
-
SafeInclude: () => import_SafeInclude.SafeInclude
|
|
22
|
+
SafeInclude: () => import_SafeInclude.SafeInclude,
|
|
23
|
+
getInjectedMetadataEmitter: () => import_injectedBundler.getInjectedMetadataEmitter,
|
|
24
|
+
getInjectedMetadataSnapshot: () => import_injectedBundler.getInjectedMetadataSnapshot
|
|
23
25
|
});
|
|
24
26
|
module.exports = __toCommonJS(index_exports);
|
|
25
27
|
__reExport(index_exports, require("./MDXProvider"), module.exports);
|
|
@@ -34,6 +36,7 @@ __reExport(index_exports, require("./components/MountImage"), module.exports);
|
|
|
34
36
|
__reExport(index_exports, require("./components/Routes"), module.exports);
|
|
35
37
|
__reExport(index_exports, require("./hooks"), module.exports);
|
|
36
38
|
__reExport(index_exports, require("./metadataSource"), module.exports);
|
|
39
|
+
var import_injectedBundler = require("./injectedBundler");
|
|
37
40
|
__reExport(index_exports, require("./auth"), module.exports);
|
|
38
41
|
__reExport(index_exports, require("./theme"), module.exports);
|
|
39
42
|
__reExport(index_exports, require("./editorContext"), module.exports);
|
|
@@ -67,6 +70,8 @@ __reExport(index_exports, require("./safeContent"), module.exports);
|
|
|
67
70
|
// Annotate the CommonJS export names for ESM import in node:
|
|
68
71
|
0 && (module.exports = {
|
|
69
72
|
SafeInclude,
|
|
73
|
+
getInjectedMetadataEmitter,
|
|
74
|
+
getInjectedMetadataSnapshot,
|
|
70
75
|
...require("./MDXProvider"),
|
|
71
76
|
...require("./routing"),
|
|
72
77
|
...require("./boot"),
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAQA,yBAA4B;AAC5B,0BAAc,0BATd;AAUA,0BAAc,uCAVd;AAWA,0BAAc,wBAXd;AAYA,0BAAc,oCAZd;AAaA,0BAAc,gCAbd;AAcA,0BAAc,oBAdd;AAiBA,0BAAc,6BAjBd;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAQA,yBAA4B;AAC5B,0BAAc,0BATd;AAUA,0BAAc,uCAVd;AAWA,0BAAc,wBAXd;AAYA,0BAAc,oCAZd;AAaA,0BAAc,gCAbd;AAcA,0BAAc,oBAdd;AAiBA,0BAAc,6BAjBd;AAoBA,6BAAwE;AACxE,0BAAc,mBArBd;AAsBA,0BAAc,oBAtBd;AAuBA,0BAAc,4BAvBd;AAwBA,0BAAc,qBAxBd;AAyBA,0BAAc,yBAzBd;AA0BA,0BAAc,4BA1Bd;AA2BA,0BAAc,qBA3Bd;AA4BA,0BAAc,qBA5Bd;AA6BA,0BAAc,yBA7Bd;AA8BA,0BAAc,sBA9Bd;AA+BA,0BAAc,kBA/Bd;AAgCA,0BAAc,kBAhCd;AAiCA,0BAAc,uBAjCd;AAkCA,0BAAc,sBAlCd;AAmCA,0BAAc,kBAnCd;AAoCA,0BAAc,0BApCd;AAqCA,0BAAc,kBArCd;AAsCA,0BAAc,yBAtCd;AAuCA,0BAAc,iBAvCd;AAwCA,0BAAc,oBAxCd;AAyCA,0BAAc,oBAzCd;AA0CA,0BAAc,qBA1Cd;AA2CA,0BAAc,sBA3Cd;AA4CA,0BAAc,wBA5Cd;AA6CA,0BAAc,oBA7Cd;AA8CA,0BAAc,sBA9Cd;AA+CA,0BAAc,6BA/Cd;AAgDA,0BAAc,+BAhDd;AAiDA,0BAAc,2BAjDd;AAkDA,0BAAc,0BAlDd;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -10,6 +10,7 @@ export { MountImage, MountImageProps } from './components/MountImage.cjs';
|
|
|
10
10
|
export { Route, RouteProps, Routes } from './components/Routes.cjs';
|
|
11
11
|
export { ObjectUrlState, useAllMetadata, useFileMetadata, useMetadataQuery, useObjectUrl } from './hooks.cjs';
|
|
12
12
|
export { MetadataSource, MetadataSourceMode, MetadataSourceProps, useMetadataStore } from './metadataSource.cjs';
|
|
13
|
+
export { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler.cjs';
|
|
13
14
|
export { AuthState, AuthStatus, SandboxUser, getAuthState, onAuthChange, useAuth } from './auth.cjs';
|
|
14
15
|
export { HostTheme, getHostTheme, onHostThemeChange, setHostTheme, useHostTheme } from './theme.cjs';
|
|
15
16
|
export { EditorContext, getEditorContext, onEditorContextChange, useEditorContext } from './editorContext.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { MountImage, MountImageProps } from './components/MountImage.js';
|
|
|
10
10
|
export { Route, RouteProps, Routes } from './components/Routes.js';
|
|
11
11
|
export { ObjectUrlState, useAllMetadata, useFileMetadata, useMetadataQuery, useObjectUrl } from './hooks.js';
|
|
12
12
|
export { MetadataSource, MetadataSourceMode, MetadataSourceProps, useMetadataStore } from './metadataSource.js';
|
|
13
|
+
export { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler.js';
|
|
13
14
|
export { AuthState, AuthStatus, SandboxUser, getAuthState, onAuthChange, useAuth } from './auth.js';
|
|
14
15
|
export { HostTheme, getHostTheme, onHostThemeChange, setHostTheme, useHostTheme } from './theme.js';
|
|
15
16
|
export { EditorContext, getEditorContext, onEditorContextChange, useEditorContext } from './editorContext.js';
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./components/MountImage";
|
|
|
11
11
|
export * from "./components/Routes";
|
|
12
12
|
export * from "./hooks";
|
|
13
13
|
export * from "./metadataSource";
|
|
14
|
+
import { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from "./injectedBundler";
|
|
14
15
|
export * from "./auth";
|
|
15
16
|
export * from "./theme";
|
|
16
17
|
export * from "./editorContext";
|
|
@@ -42,6 +43,8 @@ export * from "./protocolDeadline";
|
|
|
42
43
|
export * from "./sandboxTypes";
|
|
43
44
|
export * from "./safeContent";
|
|
44
45
|
export {
|
|
45
|
-
SafeInclude
|
|
46
|
+
SafeInclude,
|
|
47
|
+
getInjectedMetadataEmitter,
|
|
48
|
+
getInjectedMetadataSnapshot
|
|
46
49
|
};
|
|
47
50
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n"],"mappings":";AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,mBAAmB;AAC5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n"],"mappings":";AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,mBAAmB;AAC5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAGd,SAAS,4BAA4B,mCAAmC;AACxE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/injectedBundler.ts"],"sourcesContent":["// Injected sandbox-bundler access (SDK_PACKAGING_SPEC §4/§8, Phase 5). The SDK was\n// historically wired straight to the injected bundler service objects\n// (`module.evaluation.module.bundler.<x>`). Phase 5 makes the SDK transport-agnostic\n// so those services can eventually be retired: every reader PREFERS the injection\n// (so the current, live path stays byte-for-byte unchanged) and FALLS BACK to the\n// §4 transport when the SDK is fetched from npm with no injection present.\n//\n// This module centralizes the `module.evaluation.module.bundler.*` reads (the same\n// philosophy as `sandboxUtils`' transport resolver) and exposes a PURE resolver for\n// the metadata-update subscription so the dual-mode decision is unit-tested without\n// a live bundler. The ambient reads themselves are thin (untestable without the\n// sandbox realm, exactly like `sandboxUtils.transport()`).\n\n/** vscode-style Event source: subscribe with a listener, get a disposable back. */\nexport type EventSource = (listener: (msg: any) => void) => { dispose(): void };\n\n/** The injected bundler's metadata emitter — fires `{type:'metadata-update', update}`\n * as files (re)compile. Absent when the SDK is npm-fetched (no injection). */\nexport interface InjectedMetadataEmitter {\n onMetadataChange: EventSource;\n /** Start the DelayedEmitter once a subscriber is attached (injected path only). */\n enable(): void;\n}\n\n/** The injected bundler object, or null when there is no injection (npm-fetched). */\nconst injectedBundler = (): any | null => {\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n return module?.evaluation?.module?.bundler ?? null;\n } catch {\n return null;\n }\n};\n\n/** The injected bundler's metadata emitter, or null when npm-fetched. */\nexport const getInjectedMetadataEmitter = (): InjectedMetadataEmitter | null => {\n const b = injectedBundler();\n if (b && typeof b.onMetadataChange === 'function' && b.onMetadataChangeEmitter) {\n return {\n onMetadataChange: b.onMetadataChange,\n enable: () => b.onMetadataChangeEmitter.enable(),\n };\n }\n return null;\n};\n\n
|
|
1
|
+
{"version":3,"sources":["../src/injectedBundler.ts"],"sourcesContent":["// Injected sandbox-bundler access (SDK_PACKAGING_SPEC §4/§8, Phase 5). The SDK was\n// historically wired straight to the injected bundler service objects\n// (`module.evaluation.module.bundler.<x>`). Phase 5 makes the SDK transport-agnostic\n// so those services can eventually be retired: every reader PREFERS the injection\n// (so the current, live path stays byte-for-byte unchanged) and FALLS BACK to the\n// §4 transport when the SDK is fetched from npm with no injection present.\n//\n/** @deprecated The injected-bundler adapter tier — DEPRECATION WINDOW OPENED 2026-08-25\n * (R3-278, SDK_PACKAGING_SPEC §9). `module.evaluation.module.bundler.*` stops being\n * API: every read here has a protocol equivalent (the §4 transport paths in\n * `sandboxUtils`/`hostRuntime`, the metadata event-fill, `sandboxFs`, the transport\n * mount service). The injection is still PREFERRED at runtime so today's live path\n * is byte-for-byte unchanged — deprecating is an announcement, not a removal (the\n * window closes only when no host injects and no pinned app reads; see\n * DEPRECATION_CANDIDATES.md). New code MUST NOT read `bundler.*` — enforced by\n * `scripts/check-bundler-reads.mjs` in `verify`.\n */\n// This module centralizes the `module.evaluation.module.bundler.*` reads (the same\n// philosophy as `sandboxUtils`' transport resolver) and exposes a PURE resolver for\n// the metadata-update subscription so the dual-mode decision is unit-tested without\n// a live bundler. The ambient reads themselves are thin (untestable without the\n// sandbox realm, exactly like `sandboxUtils.transport()`).\n\n/** vscode-style Event source: subscribe with a listener, get a disposable back. */\nexport type EventSource = (listener: (msg: any) => void) => { dispose(): void };\n\n/** The injected bundler's metadata emitter — fires `{type:'metadata-update', update}`\n * as files (re)compile. Absent when the SDK is npm-fetched (no injection). */\nexport interface InjectedMetadataEmitter {\n onMetadataChange: EventSource;\n /** Start the DelayedEmitter once a subscriber is attached (injected path only). */\n enable(): void;\n}\n\n/** The injected bundler object, or null when there is no injection (npm-fetched). */\nconst injectedBundler = (): any | null => {\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n return module?.evaluation?.module?.bundler ?? null;\n } catch {\n return null;\n }\n};\n\n/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the\n * protocol equivalent is the metadata event-fill over the §4 transport, which\n * `resolveMetadataSource` already selects when this returns null.\n *\n * The injected bundler's metadata emitter, or null when npm-fetched. */\nexport const getInjectedMetadataEmitter = (): InjectedMetadataEmitter | null => {\n const b = injectedBundler();\n if (b && typeof b.onMetadataChange === 'function' && b.onMetadataChangeEmitter) {\n return {\n onMetadataChange: b.onMetadataChange,\n enable: () => b.onMetadataChangeEmitter.enable(),\n };\n }\n return null;\n};\n\n/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the boot\n * seed degrades to transport event-fill when this returns null.\n *\n * The injected bundler's synchronous metadata snapshot for the boot seed\n * (MDX_CONTENT_COLLECTIONS_SPEC §1.4). Returns the full `/app`-keyed collection the\n * bundler seeded (from the frontmatter sidecar) so the app's first render already\n * holds it — the SDK-side counterpart of the bundler seeding. Null when npm-fetched\n * (no in-realm bundler) → the SDK degrades to event-fill over the §4 transport, no\n * first-paint guarantee. The returned VALUE refs are the same objects the emitter\n * replays, so the `enable()` replay is a no-op (the §1.4 identity contract).\n */\nexport const getInjectedMetadataSnapshot = (): Record<string, Record<string, any>> | null => {\n const b = injectedBundler();\n if (b && typeof b.getMetadataSnapshot === 'function') {\n return b.getMetadataSnapshot();\n }\n return null;\n};\n\n/** What `boot` needs to subscribe to metadata updates: the `event` source to hand\n * `addListener` (the injected emitter, or `undefined` → listen over the transport)\n * and an `enable` to start the injected DelayedEmitter (a no-op off-injection). */\nexport interface MetadataSource {\n event?: EventSource;\n enable(): void;\n}\n\n/**\n * Resolve the metadata-update subscription source (PURE — the dual-mode decision).\n * With the injected emitter: use it and arm it, so the live path is byte-identical.\n * Without it (npm-fetched): return no `event`, so the caller's `addListener` falls\n * back to the §4 transport's `onMessage`, and `enable` is a no-op.\n */\nexport const resolveMetadataSource = (injected: InjectedMetadataEmitter | null): MetadataSource =>\n injected\n ? { event: injected.onMetadataChange, enable: () => injected.enable() }\n : { event: undefined, enable: () => {} };\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmCA,MAAM,kBAAkB,MAAkB;AACxC,MAAI;AAEF,WAAO,QAAQ,YAAY,QAAQ,WAAW;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,MAAM,6BAA6B,MAAsC;AAC9E,QAAM,IAAI,gBAAgB;AAC1B,MAAI,KAAK,OAAO,EAAE,qBAAqB,cAAc,EAAE,yBAAyB;AAC9E,WAAO;AAAA,MACL,kBAAkB,EAAE;AAAA,MACpB,QAAQ,MAAM,EAAE,wBAAwB,OAAO;AAAA,IACjD;AAAA,EACF;AACA,SAAO;AACT;AAaO,MAAM,8BAA8B,MAAkD;AAC3F,QAAM,IAAI,gBAAgB;AAC1B,MAAI,KAAK,OAAO,EAAE,wBAAwB,YAAY;AACpD,WAAO,EAAE,oBAAoB;AAAA,EAC/B;AACA,SAAO;AACT;AAgBO,MAAM,wBAAwB,CAAC,aACpC,WACI,EAAE,OAAO,SAAS,kBAAkB,QAAQ,MAAM,SAAS,OAAO,EAAE,IACpE,EAAE,OAAO,QAAW,QAAQ,MAAM;AAAC,EAAE;","names":[]}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/** @deprecated The injected-bundler adapter tier — DEPRECATION WINDOW OPENED 2026-08-25
|
|
2
|
+
* (R3-278, SDK_PACKAGING_SPEC §9). `module.evaluation.module.bundler.*` stops being
|
|
3
|
+
* API: every read here has a protocol equivalent (the §4 transport paths in
|
|
4
|
+
* `sandboxUtils`/`hostRuntime`, the metadata event-fill, `sandboxFs`, the transport
|
|
5
|
+
* mount service). The injection is still PREFERRED at runtime so today's live path
|
|
6
|
+
* is byte-for-byte unchanged — deprecating is an announcement, not a removal (the
|
|
7
|
+
* window closes only when no host injects and no pinned app reads; see
|
|
8
|
+
* DEPRECATION_CANDIDATES.md). New code MUST NOT read `bundler.*` — enforced by
|
|
9
|
+
* `scripts/check-bundler-reads.mjs` in `verify`.
|
|
10
|
+
*/
|
|
1
11
|
/** vscode-style Event source: subscribe with a listener, get a disposable back. */
|
|
2
12
|
type EventSource = (listener: (msg: any) => void) => {
|
|
3
13
|
dispose(): void;
|
|
@@ -9,9 +19,15 @@ interface InjectedMetadataEmitter {
|
|
|
9
19
|
/** Start the DelayedEmitter once a subscriber is attached (injected path only). */
|
|
10
20
|
enable(): void;
|
|
11
21
|
}
|
|
12
|
-
/**
|
|
22
|
+
/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the
|
|
23
|
+
* protocol equivalent is the metadata event-fill over the §4 transport, which
|
|
24
|
+
* `resolveMetadataSource` already selects when this returns null.
|
|
25
|
+
*
|
|
26
|
+
* The injected bundler's metadata emitter, or null when npm-fetched. */
|
|
13
27
|
declare const getInjectedMetadataEmitter: () => InjectedMetadataEmitter | null;
|
|
14
|
-
/**
|
|
28
|
+
/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the boot
|
|
29
|
+
* seed degrades to transport event-fill when this returns null.
|
|
30
|
+
*
|
|
15
31
|
* The injected bundler's synchronous metadata snapshot for the boot seed
|
|
16
32
|
* (MDX_CONTENT_COLLECTIONS_SPEC §1.4). Returns the full `/app`-keyed collection the
|
|
17
33
|
* bundler seeded (from the frontmatter sidecar) so the app's first render already
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/** @deprecated The injected-bundler adapter tier — DEPRECATION WINDOW OPENED 2026-08-25
|
|
2
|
+
* (R3-278, SDK_PACKAGING_SPEC §9). `module.evaluation.module.bundler.*` stops being
|
|
3
|
+
* API: every read here has a protocol equivalent (the §4 transport paths in
|
|
4
|
+
* `sandboxUtils`/`hostRuntime`, the metadata event-fill, `sandboxFs`, the transport
|
|
5
|
+
* mount service). The injection is still PREFERRED at runtime so today's live path
|
|
6
|
+
* is byte-for-byte unchanged — deprecating is an announcement, not a removal (the
|
|
7
|
+
* window closes only when no host injects and no pinned app reads; see
|
|
8
|
+
* DEPRECATION_CANDIDATES.md). New code MUST NOT read `bundler.*` — enforced by
|
|
9
|
+
* `scripts/check-bundler-reads.mjs` in `verify`.
|
|
10
|
+
*/
|
|
1
11
|
/** vscode-style Event source: subscribe with a listener, get a disposable back. */
|
|
2
12
|
type EventSource = (listener: (msg: any) => void) => {
|
|
3
13
|
dispose(): void;
|
|
@@ -9,9 +19,15 @@ interface InjectedMetadataEmitter {
|
|
|
9
19
|
/** Start the DelayedEmitter once a subscriber is attached (injected path only). */
|
|
10
20
|
enable(): void;
|
|
11
21
|
}
|
|
12
|
-
/**
|
|
22
|
+
/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the
|
|
23
|
+
* protocol equivalent is the metadata event-fill over the §4 transport, which
|
|
24
|
+
* `resolveMetadataSource` already selects when this returns null.
|
|
25
|
+
*
|
|
26
|
+
* The injected bundler's metadata emitter, or null when npm-fetched. */
|
|
13
27
|
declare const getInjectedMetadataEmitter: () => InjectedMetadataEmitter | null;
|
|
14
|
-
/**
|
|
28
|
+
/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the boot
|
|
29
|
+
* seed degrades to transport event-fill when this returns null.
|
|
30
|
+
*
|
|
15
31
|
* The injected bundler's synchronous metadata snapshot for the boot seed
|
|
16
32
|
* (MDX_CONTENT_COLLECTIONS_SPEC §1.4). Returns the full `/app`-keyed collection the
|
|
17
33
|
* bundler seeded (from the frontmatter sidecar) so the app's first render already
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/injectedBundler.ts"],"sourcesContent":["// Injected sandbox-bundler access (SDK_PACKAGING_SPEC §4/§8, Phase 5). The SDK was\n// historically wired straight to the injected bundler service objects\n// (`module.evaluation.module.bundler.<x>`). Phase 5 makes the SDK transport-agnostic\n// so those services can eventually be retired: every reader PREFERS the injection\n// (so the current, live path stays byte-for-byte unchanged) and FALLS BACK to the\n// §4 transport when the SDK is fetched from npm with no injection present.\n//\n// This module centralizes the `module.evaluation.module.bundler.*` reads (the same\n// philosophy as `sandboxUtils`' transport resolver) and exposes a PURE resolver for\n// the metadata-update subscription so the dual-mode decision is unit-tested without\n// a live bundler. The ambient reads themselves are thin (untestable without the\n// sandbox realm, exactly like `sandboxUtils.transport()`).\n\n/** vscode-style Event source: subscribe with a listener, get a disposable back. */\nexport type EventSource = (listener: (msg: any) => void) => { dispose(): void };\n\n/** The injected bundler's metadata emitter — fires `{type:'metadata-update', update}`\n * as files (re)compile. Absent when the SDK is npm-fetched (no injection). */\nexport interface InjectedMetadataEmitter {\n onMetadataChange: EventSource;\n /** Start the DelayedEmitter once a subscriber is attached (injected path only). */\n enable(): void;\n}\n\n/** The injected bundler object, or null when there is no injection (npm-fetched). */\nconst injectedBundler = (): any | null => {\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n return module?.evaluation?.module?.bundler ?? null;\n } catch {\n return null;\n }\n};\n\n/** The injected bundler's metadata emitter, or null when npm-fetched. */\nexport const getInjectedMetadataEmitter = (): InjectedMetadataEmitter | null => {\n const b = injectedBundler();\n if (b && typeof b.onMetadataChange === 'function' && b.onMetadataChangeEmitter) {\n return {\n onMetadataChange: b.onMetadataChange,\n enable: () => b.onMetadataChangeEmitter.enable(),\n };\n }\n return null;\n};\n\n
|
|
1
|
+
{"version":3,"sources":["../src/injectedBundler.ts"],"sourcesContent":["// Injected sandbox-bundler access (SDK_PACKAGING_SPEC §4/§8, Phase 5). The SDK was\n// historically wired straight to the injected bundler service objects\n// (`module.evaluation.module.bundler.<x>`). Phase 5 makes the SDK transport-agnostic\n// so those services can eventually be retired: every reader PREFERS the injection\n// (so the current, live path stays byte-for-byte unchanged) and FALLS BACK to the\n// §4 transport when the SDK is fetched from npm with no injection present.\n//\n/** @deprecated The injected-bundler adapter tier — DEPRECATION WINDOW OPENED 2026-08-25\n * (R3-278, SDK_PACKAGING_SPEC §9). `module.evaluation.module.bundler.*` stops being\n * API: every read here has a protocol equivalent (the §4 transport paths in\n * `sandboxUtils`/`hostRuntime`, the metadata event-fill, `sandboxFs`, the transport\n * mount service). The injection is still PREFERRED at runtime so today's live path\n * is byte-for-byte unchanged — deprecating is an announcement, not a removal (the\n * window closes only when no host injects and no pinned app reads; see\n * DEPRECATION_CANDIDATES.md). New code MUST NOT read `bundler.*` — enforced by\n * `scripts/check-bundler-reads.mjs` in `verify`.\n */\n// This module centralizes the `module.evaluation.module.bundler.*` reads (the same\n// philosophy as `sandboxUtils`' transport resolver) and exposes a PURE resolver for\n// the metadata-update subscription so the dual-mode decision is unit-tested without\n// a live bundler. The ambient reads themselves are thin (untestable without the\n// sandbox realm, exactly like `sandboxUtils.transport()`).\n\n/** vscode-style Event source: subscribe with a listener, get a disposable back. */\nexport type EventSource = (listener: (msg: any) => void) => { dispose(): void };\n\n/** The injected bundler's metadata emitter — fires `{type:'metadata-update', update}`\n * as files (re)compile. Absent when the SDK is npm-fetched (no injection). */\nexport interface InjectedMetadataEmitter {\n onMetadataChange: EventSource;\n /** Start the DelayedEmitter once a subscriber is attached (injected path only). */\n enable(): void;\n}\n\n/** The injected bundler object, or null when there is no injection (npm-fetched). */\nconst injectedBundler = (): any | null => {\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n return module?.evaluation?.module?.bundler ?? null;\n } catch {\n return null;\n }\n};\n\n/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the\n * protocol equivalent is the metadata event-fill over the §4 transport, which\n * `resolveMetadataSource` already selects when this returns null.\n *\n * The injected bundler's metadata emitter, or null when npm-fetched. */\nexport const getInjectedMetadataEmitter = (): InjectedMetadataEmitter | null => {\n const b = injectedBundler();\n if (b && typeof b.onMetadataChange === 'function' && b.onMetadataChangeEmitter) {\n return {\n onMetadataChange: b.onMetadataChange,\n enable: () => b.onMetadataChangeEmitter.enable(),\n };\n }\n return null;\n};\n\n/** @deprecated Injected-bundler read (window opened 2026-08-25, R3-278) — the boot\n * seed degrades to transport event-fill when this returns null.\n *\n * The injected bundler's synchronous metadata snapshot for the boot seed\n * (MDX_CONTENT_COLLECTIONS_SPEC §1.4). Returns the full `/app`-keyed collection the\n * bundler seeded (from the frontmatter sidecar) so the app's first render already\n * holds it — the SDK-side counterpart of the bundler seeding. Null when npm-fetched\n * (no in-realm bundler) → the SDK degrades to event-fill over the §4 transport, no\n * first-paint guarantee. The returned VALUE refs are the same objects the emitter\n * replays, so the `enable()` replay is a no-op (the §1.4 identity contract).\n */\nexport const getInjectedMetadataSnapshot = (): Record<string, Record<string, any>> | null => {\n const b = injectedBundler();\n if (b && typeof b.getMetadataSnapshot === 'function') {\n return b.getMetadataSnapshot();\n }\n return null;\n};\n\n/** What `boot` needs to subscribe to metadata updates: the `event` source to hand\n * `addListener` (the injected emitter, or `undefined` → listen over the transport)\n * and an `enable` to start the injected DelayedEmitter (a no-op off-injection). */\nexport interface MetadataSource {\n event?: EventSource;\n enable(): void;\n}\n\n/**\n * Resolve the metadata-update subscription source (PURE — the dual-mode decision).\n * With the injected emitter: use it and arm it, so the live path is byte-identical.\n * Without it (npm-fetched): return no `event`, so the caller's `addListener` falls\n * back to the §4 transport's `onMessage`, and `enable` is a no-op.\n */\nexport const resolveMetadataSource = (injected: InjectedMetadataEmitter | null): MetadataSource =>\n injected\n ? { event: injected.onMetadataChange, enable: () => injected.enable() }\n : { event: undefined, enable: () => {} };\n"],"mappings":";AAmCA,MAAM,kBAAkB,MAAkB;AACxC,MAAI;AAEF,WAAO,QAAQ,YAAY,QAAQ,WAAW;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,MAAM,6BAA6B,MAAsC;AAC9E,QAAM,IAAI,gBAAgB;AAC1B,MAAI,KAAK,OAAO,EAAE,qBAAqB,cAAc,EAAE,yBAAyB;AAC9E,WAAO;AAAA,MACL,kBAAkB,EAAE;AAAA,MACpB,QAAQ,MAAM,EAAE,wBAAwB,OAAO;AAAA,IACjD;AAAA,EACF;AACA,SAAO;AACT;AAaO,MAAM,8BAA8B,MAAkD;AAC3F,QAAM,IAAI,gBAAgB;AAC1B,MAAI,KAAK,OAAO,EAAE,wBAAwB,YAAY;AACpD,WAAO,EAAE,oBAAoB;AAAA,EAC/B;AACA,SAAO;AACT;AAgBO,MAAM,wBAAwB,CAAC,aACpC,WACI,EAAE,OAAO,SAAS,kBAAkB,QAAQ,MAAM,SAAS,OAAO,EAAE,IACpE,EAAE,OAAO,QAAW,QAAQ,MAAM;AAAC,EAAE;","names":[]}
|
package/dist/mounts.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { APP_ROOT } from '@immediately-run/platform-constants';\n\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// R3-166 — the `spaces:*` family is GENERATED from the capability descriptor set\n// (`scripts/codegen-prototype/descriptors.spaces.mjs`) rather than hand-written here.\n// Re-exported from this module so every existing import path keeps working: the\n// swap is a no-op to consumers (SDK_SIMPLIFICATION_SPEC §7 step 3), which is\n// asserted by the emitted-`.d.ts` before/after comparison, not assumed.\n//\n// `Role` is imported (not only re-exported) because `Invite` below still uses it —\n// the invite methods are the same `spaces:` scheme but are NOT yet described, so\n// they remain hand-written. That split is the next migration increment.\nimport type { Role, SpaceInfo, Member, GrantRecord } from './generated/spaces';\nexport type { Role, SpaceInfo, Member, ResolvedUser, GrantRecord } from './generated/spaces';\nexport {\n listSpaces,\n listAllSpaces,\n getSpaceMembers,\n inviteToSpace,\n unshareSpace,\n setSpaceRole,\n lookupUser,\n listGrants,\n revokeGrant,\n} from './generated/spaces';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\nimport {\n INVITATIONS,\n MOUNT_ADD,\n MOUNT_REMOVE,\n PROTOCOL_SETTINGS,\n PROTOCOL_SPACES,\n REQUEST_INVITATIONS,\n REQUEST_MOUNTS,\n REQUEST_SESSION_MOUNTS,\n SESSION_MOUNTS,\n} from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The absolute path where this app's own repository filesystem is mounted\n * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is\n * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`\n * address, and this returns the canonical one the host reports. Falls back to\n * `/app` when the host hasn't reported a canonical path (older host / before the\n * report arrives) — both paths are live, so either resolves the same files.\n */\nexport const getAppMountPath = (): string => getHostRuntime()?.appMountPath ?? APP_ROOT;\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: 'ro' | 'rw';\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason = 'revoked' | 'unshared' | 'signed-out' | 'unmounted' | 'deleted';\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value) ? (value as MountRemoveReason) : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener(MOUNT_ADD, (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener(MOUNT_REMOVE, (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage(REQUEST_MOUNTS);\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean => mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined => getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n *\n * `timeoutMs` (optional, additive) rejects with a `timeout`-coded error instead of\n * waiting forever. Omit it to keep the original unbounded behaviour — but prefer\n * setting it on any path whose caller would otherwise hang silently: a mount that\n * never arrives is indistinguishable from one that is merely slow, and an awaited\n * promise that never settles surfaces to the user as a feature that quietly does\n * nothing.\n *\n * **Hazard — `onMountsChange` calls its listener SYNCHRONOUSLY on subscribe** (the\n * documented initial replay). So when the mount is already present — the common\n * case, since callers typically `await` the host request that creates it first —\n * the callback below runs *during* the `onMountsChange(...)` call, before the\n * assignment to `unsubscribe` completes. `unsubscribe` is therefore declared with\n * `let` ABOVE the subscription and read only inside a deferred closure: writing\n * `const unsubscribe = onMountsChange(...)` and referencing it in the callback\n * throws `ReferenceError: Cannot access 'unsubscribe' before initialization` (a\n * temporal-dead-zone read) on exactly that path. That bug silently broke\n * `openSettings()` — and with it the agent's conversation memory.\n */\nexport const waitForMount = (query: MountQuery, timeoutMs?: number): Promise<SandboxMount> =>\n awaitMatchingMount(onMountsChange, query, timeoutMs);\n\n/** The framework-free core of {@link waitForMount}, with the subscription injected\n * so a test can drive the synchronous-initial-replay case that broke it. */\nexport const awaitMatchingMount = (\n subscribe: (listener: (mounts: SandboxMount[]) => void) => () => void,\n query: MountQuery,\n timeoutMs?: number,\n): Promise<SandboxMount> =>\n new Promise((resolve, reject) => {\n // `let`, declared BEFORE `subscribe(...)` — see the hazard note above. A\n // `const` bound to the subscribe call is in its temporal dead zone while the\n // synchronous initial replay runs, and any read of it from the listener\n // throws.\n let unsubscribe: (() => void) | undefined;\n let settled = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n // Deferred so we never dispose the subscription from inside its own initial\n // replay, and late enough that `unsubscribe` is always assigned.\n const stop = (): void => {\n settled = true;\n if (timer !== undefined) clearTimeout(timer);\n void Promise.resolve().then(() => unsubscribe?.());\n };\n unsubscribe = subscribe((mounts) => {\n if (settled) return;\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n stop();\n resolve(found);\n }\n });\n // The initial replay may have settled us above, before `unsubscribe` existed;\n // `stop()`'s deferred read picks it up, so nothing more is needed here.\n if (!settled && timeoutMs !== undefined) {\n timer = setTimeout(() => {\n if (settled) return;\n stop();\n const err = new Error(\n `waitForMount timed out after ${timeoutMs}ms waiting for ${JSON.stringify(query)}`,\n ) as SpaceError;\n err.code = 'timeout';\n reject(err);\n }, timeoutMs);\n }\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: SESSION_MOUNTS,\n requestType: REQUEST_SESSION_MOUNTS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (listener: (mounts: SessionMount[]) => void): (() => void) =>\n sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n // Client-side, never from the host: a bounded `waitForMount` gave up.\n | 'timeout'\n | 'unknown';\n}\n\ntype SpaceResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SPACES], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (method: string, query: Record<string, unknown>): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> => requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> => mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> => requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (ref: { mountId: string; relPath: string }, opts: { mode: 'ro' | 'rw' }): FileCap => ({\n $cap: 'file',\n mountId: ref.mountId,\n relPath: ref.relPath,\n mode: opts.mode,\n});\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SETTINGS], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n // The host has already accepted the request and announced the mount, so this\n // normally resolves on the initial replay. Bounded anyway: an unbounded wait\n // here turns any delivery failure into a promise that never settles, and every\n // caller of `openSettings()` is doing it to reach durable state — so the app\n // just quietly loses that state with nothing to report.\n return waitForMount({ id: mount.id ?? mount.path }, SETTINGS_MOUNT_TIMEOUT_MS);\n};\n\n/** How long `openSettings()` waits for the host to deliver the mount it just\n * agreed to create. Generous — this is a hang-breaker, not a latency budget. */\nconst SETTINGS_MOUNT_TIMEOUT_MS = 15_000;\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> => settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (opts: { name?: string } = {}): Promise<SandboxMount> =>\n requestMountInternal('create', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\n/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: INVITATIONS,\n requestType: REQUEST_INVITATIONS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n"],"mappings":"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","names":["mount"]}
|
|
1
|
+
{"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { APP_ROOT } from '@immediately-run/platform-constants';\n\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// R3-166 — the `spaces:*` family is GENERATED from the capability descriptor set\n// (`scripts/codegen-prototype/descriptors.spaces.mjs`) rather than hand-written here.\n// Re-exported from this module so every existing import path keeps working: the\n// swap is a no-op to consumers (SDK_SIMPLIFICATION_SPEC §7 step 3), which is\n// asserted by the emitted-`.d.ts` before/after comparison, not assumed.\n//\n// `Role` is imported (not only re-exported) because `Invite` below still uses it —\n// the invite methods are the same `spaces:` scheme but are NOT yet described, so\n// they remain hand-written. That split is the next migration increment.\nimport type { Role, SpaceInfo, Member, GrantRecord } from './generated/spaces';\nexport type { Role, SpaceInfo, Member, ResolvedUser, GrantRecord } from './generated/spaces';\nexport {\n listSpaces,\n listAllSpaces,\n getSpaceMembers,\n inviteToSpace,\n unshareSpace,\n setSpaceRole,\n lookupUser,\n listGrants,\n revokeGrant,\n} from './generated/spaces';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\nimport {\n INVITATIONS,\n MOUNT_ADD,\n MOUNT_REMOVE,\n PROTOCOL_SETTINGS,\n PROTOCOL_SPACES,\n REQUEST_INVITATIONS,\n REQUEST_MOUNTS,\n REQUEST_SESSION_MOUNTS,\n SESSION_MOUNTS,\n} from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The absolute path where this app's own repository filesystem is mounted\n * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is\n * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`\n * address, and this returns the canonical one the host reports. Falls back to\n * `/app` when the host hasn't reported a canonical path (older host / before the\n * report arrives) — both paths are live, so either resolves the same files.\n */\nexport const getAppMountPath = (): string => getHostRuntime()?.appMountPath ?? APP_ROOT;\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: 'ro' | 'rw';\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason = 'revoked' | 'unshared' | 'signed-out' | 'unmounted' | 'deleted';\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value) ? (value as MountRemoveReason) : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\n/** @deprecated-path The injected `bundler.mounts` read — window opened 2026-08-25\n * (R3-278). The protocol equivalent is `transportMountService()` below (the\n * `mount-add`/`mount-remove` mirror + `request-mounts` replay), which the dual-mode\n * chooser already falls back to. Injection stays preferred for byte-compat through\n * the window; see DEPRECATION_CANDIDATES.md.\n */\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener(MOUNT_ADD, (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener(MOUNT_REMOVE, (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage(REQUEST_MOUNTS);\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean => mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined => getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n *\n * `timeoutMs` (optional, additive) rejects with a `timeout`-coded error instead of\n * waiting forever. Omit it to keep the original unbounded behaviour — but prefer\n * setting it on any path whose caller would otherwise hang silently: a mount that\n * never arrives is indistinguishable from one that is merely slow, and an awaited\n * promise that never settles surfaces to the user as a feature that quietly does\n * nothing.\n *\n * **Hazard — `onMountsChange` calls its listener SYNCHRONOUSLY on subscribe** (the\n * documented initial replay). So when the mount is already present — the common\n * case, since callers typically `await` the host request that creates it first —\n * the callback below runs *during* the `onMountsChange(...)` call, before the\n * assignment to `unsubscribe` completes. `unsubscribe` is therefore declared with\n * `let` ABOVE the subscription and read only inside a deferred closure: writing\n * `const unsubscribe = onMountsChange(...)` and referencing it in the callback\n * throws `ReferenceError: Cannot access 'unsubscribe' before initialization` (a\n * temporal-dead-zone read) on exactly that path. That bug silently broke\n * `openSettings()` — and with it the agent's conversation memory.\n */\nexport const waitForMount = (query: MountQuery, timeoutMs?: number): Promise<SandboxMount> =>\n awaitMatchingMount(onMountsChange, query, timeoutMs);\n\n/** The framework-free core of {@link waitForMount}, with the subscription injected\n * so a test can drive the synchronous-initial-replay case that broke it. */\nexport const awaitMatchingMount = (\n subscribe: (listener: (mounts: SandboxMount[]) => void) => () => void,\n query: MountQuery,\n timeoutMs?: number,\n): Promise<SandboxMount> =>\n new Promise((resolve, reject) => {\n // `let`, declared BEFORE `subscribe(...)` — see the hazard note above. A\n // `const` bound to the subscribe call is in its temporal dead zone while the\n // synchronous initial replay runs, and any read of it from the listener\n // throws.\n let unsubscribe: (() => void) | undefined;\n let settled = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n // Deferred so we never dispose the subscription from inside its own initial\n // replay, and late enough that `unsubscribe` is always assigned.\n const stop = (): void => {\n settled = true;\n if (timer !== undefined) clearTimeout(timer);\n void Promise.resolve().then(() => unsubscribe?.());\n };\n unsubscribe = subscribe((mounts) => {\n if (settled) return;\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n stop();\n resolve(found);\n }\n });\n // The initial replay may have settled us above, before `unsubscribe` existed;\n // `stop()`'s deferred read picks it up, so nothing more is needed here.\n if (!settled && timeoutMs !== undefined) {\n timer = setTimeout(() => {\n if (settled) return;\n stop();\n const err = new Error(\n `waitForMount timed out after ${timeoutMs}ms waiting for ${JSON.stringify(query)}`,\n ) as SpaceError;\n err.code = 'timeout';\n reject(err);\n }, timeoutMs);\n }\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: SESSION_MOUNTS,\n requestType: REQUEST_SESSION_MOUNTS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (listener: (mounts: SessionMount[]) => void): (() => void) =>\n sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n // Client-side, never from the host: a bounded `waitForMount` gave up.\n | 'timeout'\n | 'unknown';\n}\n\ntype SpaceResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SPACES], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (method: string, query: Record<string, unknown>): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> => requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> => mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> => requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (ref: { mountId: string; relPath: string }, opts: { mode: 'ro' | 'rw' }): FileCap => ({\n $cap: 'file',\n mountId: ref.mountId,\n relPath: ref.relPath,\n mode: opts.mode,\n});\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SETTINGS], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n // The host has already accepted the request and announced the mount, so this\n // normally resolves on the initial replay. Bounded anyway: an unbounded wait\n // here turns any delivery failure into a promise that never settles, and every\n // caller of `openSettings()` is doing it to reach durable state — so the app\n // just quietly loses that state with nothing to report.\n return waitForMount({ id: mount.id ?? mount.path }, SETTINGS_MOUNT_TIMEOUT_MS);\n};\n\n/** How long `openSettings()` waits for the host to deliver the mount it just\n * agreed to create. Generous — this is a hang-breaker, not a latency budget. */\nconst SETTINGS_MOUNT_TIMEOUT_MS = 15_000;\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> => settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (opts: { name?: string } = {}): Promise<SandboxMount> =>\n requestMountInternal('create', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\n/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: INVITATIONS,\n requestType: REQUEST_INVITATIONS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n"],"mappings":"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","names":["mount"]}
|
package/dist/mounts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { APP_ROOT } from '@immediately-run/platform-constants';\n\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// R3-166 — the `spaces:*` family is GENERATED from the capability descriptor set\n// (`scripts/codegen-prototype/descriptors.spaces.mjs`) rather than hand-written here.\n// Re-exported from this module so every existing import path keeps working: the\n// swap is a no-op to consumers (SDK_SIMPLIFICATION_SPEC §7 step 3), which is\n// asserted by the emitted-`.d.ts` before/after comparison, not assumed.\n//\n// `Role` is imported (not only re-exported) because `Invite` below still uses it —\n// the invite methods are the same `spaces:` scheme but are NOT yet described, so\n// they remain hand-written. That split is the next migration increment.\nimport type { Role, SpaceInfo, Member, GrantRecord } from './generated/spaces';\nexport type { Role, SpaceInfo, Member, ResolvedUser, GrantRecord } from './generated/spaces';\nexport {\n listSpaces,\n listAllSpaces,\n getSpaceMembers,\n inviteToSpace,\n unshareSpace,\n setSpaceRole,\n lookupUser,\n listGrants,\n revokeGrant,\n} from './generated/spaces';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\nimport {\n INVITATIONS,\n MOUNT_ADD,\n MOUNT_REMOVE,\n PROTOCOL_SETTINGS,\n PROTOCOL_SPACES,\n REQUEST_INVITATIONS,\n REQUEST_MOUNTS,\n REQUEST_SESSION_MOUNTS,\n SESSION_MOUNTS,\n} from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The absolute path where this app's own repository filesystem is mounted\n * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is\n * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`\n * address, and this returns the canonical one the host reports. Falls back to\n * `/app` when the host hasn't reported a canonical path (older host / before the\n * report arrives) — both paths are live, so either resolves the same files.\n */\nexport const getAppMountPath = (): string => getHostRuntime()?.appMountPath ?? APP_ROOT;\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: 'ro' | 'rw';\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason = 'revoked' | 'unshared' | 'signed-out' | 'unmounted' | 'deleted';\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value) ? (value as MountRemoveReason) : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener(MOUNT_ADD, (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener(MOUNT_REMOVE, (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage(REQUEST_MOUNTS);\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean => mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined => getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n *\n * `timeoutMs` (optional, additive) rejects with a `timeout`-coded error instead of\n * waiting forever. Omit it to keep the original unbounded behaviour — but prefer\n * setting it on any path whose caller would otherwise hang silently: a mount that\n * never arrives is indistinguishable from one that is merely slow, and an awaited\n * promise that never settles surfaces to the user as a feature that quietly does\n * nothing.\n *\n * **Hazard — `onMountsChange` calls its listener SYNCHRONOUSLY on subscribe** (the\n * documented initial replay). So when the mount is already present — the common\n * case, since callers typically `await` the host request that creates it first —\n * the callback below runs *during* the `onMountsChange(...)` call, before the\n * assignment to `unsubscribe` completes. `unsubscribe` is therefore declared with\n * `let` ABOVE the subscription and read only inside a deferred closure: writing\n * `const unsubscribe = onMountsChange(...)` and referencing it in the callback\n * throws `ReferenceError: Cannot access 'unsubscribe' before initialization` (a\n * temporal-dead-zone read) on exactly that path. That bug silently broke\n * `openSettings()` — and with it the agent's conversation memory.\n */\nexport const waitForMount = (query: MountQuery, timeoutMs?: number): Promise<SandboxMount> =>\n awaitMatchingMount(onMountsChange, query, timeoutMs);\n\n/** The framework-free core of {@link waitForMount}, with the subscription injected\n * so a test can drive the synchronous-initial-replay case that broke it. */\nexport const awaitMatchingMount = (\n subscribe: (listener: (mounts: SandboxMount[]) => void) => () => void,\n query: MountQuery,\n timeoutMs?: number,\n): Promise<SandboxMount> =>\n new Promise((resolve, reject) => {\n // `let`, declared BEFORE `subscribe(...)` — see the hazard note above. A\n // `const` bound to the subscribe call is in its temporal dead zone while the\n // synchronous initial replay runs, and any read of it from the listener\n // throws.\n let unsubscribe: (() => void) | undefined;\n let settled = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n // Deferred so we never dispose the subscription from inside its own initial\n // replay, and late enough that `unsubscribe` is always assigned.\n const stop = (): void => {\n settled = true;\n if (timer !== undefined) clearTimeout(timer);\n void Promise.resolve().then(() => unsubscribe?.());\n };\n unsubscribe = subscribe((mounts) => {\n if (settled) return;\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n stop();\n resolve(found);\n }\n });\n // The initial replay may have settled us above, before `unsubscribe` existed;\n // `stop()`'s deferred read picks it up, so nothing more is needed here.\n if (!settled && timeoutMs !== undefined) {\n timer = setTimeout(() => {\n if (settled) return;\n stop();\n const err = new Error(\n `waitForMount timed out after ${timeoutMs}ms waiting for ${JSON.stringify(query)}`,\n ) as SpaceError;\n err.code = 'timeout';\n reject(err);\n }, timeoutMs);\n }\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: SESSION_MOUNTS,\n requestType: REQUEST_SESSION_MOUNTS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (listener: (mounts: SessionMount[]) => void): (() => void) =>\n sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n // Client-side, never from the host: a bounded `waitForMount` gave up.\n | 'timeout'\n | 'unknown';\n}\n\ntype SpaceResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SPACES], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (method: string, query: Record<string, unknown>): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> => requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> => mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> => requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (ref: { mountId: string; relPath: string }, opts: { mode: 'ro' | 'rw' }): FileCap => ({\n $cap: 'file',\n mountId: ref.mountId,\n relPath: ref.relPath,\n mode: opts.mode,\n});\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SETTINGS], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n // The host has already accepted the request and announced the mount, so this\n // normally resolves on the initial replay. Bounded anyway: an unbounded wait\n // here turns any delivery failure into a promise that never settles, and every\n // caller of `openSettings()` is doing it to reach durable state — so the app\n // just quietly loses that state with nothing to report.\n return waitForMount({ id: mount.id ?? mount.path }, SETTINGS_MOUNT_TIMEOUT_MS);\n};\n\n/** How long `openSettings()` waits for the host to deliver the mount it just\n * agreed to create. Generous — this is a hang-breaker, not a latency budget. */\nconst SETTINGS_MOUNT_TIMEOUT_MS = 15_000;\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> => settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (opts: { name?: string } = {}): Promise<SandboxMount> =>\n requestMountInternal('create', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\n/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: INVITATIONS,\n requestType: REQUEST_INVITATIONS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n"],"mappings":"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","names":["mount"]}
|
|
1
|
+
{"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { APP_ROOT } from '@immediately-run/platform-constants';\n\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// R3-166 — the `spaces:*` family is GENERATED from the capability descriptor set\n// (`scripts/codegen-prototype/descriptors.spaces.mjs`) rather than hand-written here.\n// Re-exported from this module so every existing import path keeps working: the\n// swap is a no-op to consumers (SDK_SIMPLIFICATION_SPEC §7 step 3), which is\n// asserted by the emitted-`.d.ts` before/after comparison, not assumed.\n//\n// `Role` is imported (not only re-exported) because `Invite` below still uses it —\n// the invite methods are the same `spaces:` scheme but are NOT yet described, so\n// they remain hand-written. That split is the next migration increment.\nimport type { Role, SpaceInfo, Member, GrantRecord } from './generated/spaces';\nexport type { Role, SpaceInfo, Member, ResolvedUser, GrantRecord } from './generated/spaces';\nexport {\n listSpaces,\n listAllSpaces,\n getSpaceMembers,\n inviteToSpace,\n unshareSpace,\n setSpaceRole,\n lookupUser,\n listGrants,\n revokeGrant,\n} from './generated/spaces';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\nimport {\n INVITATIONS,\n MOUNT_ADD,\n MOUNT_REMOVE,\n PROTOCOL_SETTINGS,\n PROTOCOL_SPACES,\n REQUEST_INVITATIONS,\n REQUEST_MOUNTS,\n REQUEST_SESSION_MOUNTS,\n SESSION_MOUNTS,\n} from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The absolute path where this app's own repository filesystem is mounted\n * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is\n * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`\n * address, and this returns the canonical one the host reports. Falls back to\n * `/app` when the host hasn't reported a canonical path (older host / before the\n * report arrives) — both paths are live, so either resolves the same files.\n */\nexport const getAppMountPath = (): string => getHostRuntime()?.appMountPath ?? APP_ROOT;\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: 'ro' | 'rw';\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason = 'revoked' | 'unshared' | 'signed-out' | 'unmounted' | 'deleted';\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value) ? (value as MountRemoveReason) : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\n/** @deprecated-path The injected `bundler.mounts` read — window opened 2026-08-25\n * (R3-278). The protocol equivalent is `transportMountService()` below (the\n * `mount-add`/`mount-remove` mirror + `request-mounts` replay), which the dual-mode\n * chooser already falls back to. Injection stays preferred for byte-compat through\n * the window; see DEPRECATION_CANDIDATES.md.\n */\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener(MOUNT_ADD, (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener(MOUNT_REMOVE, (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage(REQUEST_MOUNTS);\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean => mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined => getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n *\n * `timeoutMs` (optional, additive) rejects with a `timeout`-coded error instead of\n * waiting forever. Omit it to keep the original unbounded behaviour — but prefer\n * setting it on any path whose caller would otherwise hang silently: a mount that\n * never arrives is indistinguishable from one that is merely slow, and an awaited\n * promise that never settles surfaces to the user as a feature that quietly does\n * nothing.\n *\n * **Hazard — `onMountsChange` calls its listener SYNCHRONOUSLY on subscribe** (the\n * documented initial replay). So when the mount is already present — the common\n * case, since callers typically `await` the host request that creates it first —\n * the callback below runs *during* the `onMountsChange(...)` call, before the\n * assignment to `unsubscribe` completes. `unsubscribe` is therefore declared with\n * `let` ABOVE the subscription and read only inside a deferred closure: writing\n * `const unsubscribe = onMountsChange(...)` and referencing it in the callback\n * throws `ReferenceError: Cannot access 'unsubscribe' before initialization` (a\n * temporal-dead-zone read) on exactly that path. That bug silently broke\n * `openSettings()` — and with it the agent's conversation memory.\n */\nexport const waitForMount = (query: MountQuery, timeoutMs?: number): Promise<SandboxMount> =>\n awaitMatchingMount(onMountsChange, query, timeoutMs);\n\n/** The framework-free core of {@link waitForMount}, with the subscription injected\n * so a test can drive the synchronous-initial-replay case that broke it. */\nexport const awaitMatchingMount = (\n subscribe: (listener: (mounts: SandboxMount[]) => void) => () => void,\n query: MountQuery,\n timeoutMs?: number,\n): Promise<SandboxMount> =>\n new Promise((resolve, reject) => {\n // `let`, declared BEFORE `subscribe(...)` — see the hazard note above. A\n // `const` bound to the subscribe call is in its temporal dead zone while the\n // synchronous initial replay runs, and any read of it from the listener\n // throws.\n let unsubscribe: (() => void) | undefined;\n let settled = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n // Deferred so we never dispose the subscription from inside its own initial\n // replay, and late enough that `unsubscribe` is always assigned.\n const stop = (): void => {\n settled = true;\n if (timer !== undefined) clearTimeout(timer);\n void Promise.resolve().then(() => unsubscribe?.());\n };\n unsubscribe = subscribe((mounts) => {\n if (settled) return;\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n stop();\n resolve(found);\n }\n });\n // The initial replay may have settled us above, before `unsubscribe` existed;\n // `stop()`'s deferred read picks it up, so nothing more is needed here.\n if (!settled && timeoutMs !== undefined) {\n timer = setTimeout(() => {\n if (settled) return;\n stop();\n const err = new Error(\n `waitForMount timed out after ${timeoutMs}ms waiting for ${JSON.stringify(query)}`,\n ) as SpaceError;\n err.code = 'timeout';\n reject(err);\n }, timeoutMs);\n }\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: SESSION_MOUNTS,\n requestType: REQUEST_SESSION_MOUNTS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (listener: (mounts: SessionMount[]) => void): (() => void) =>\n sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n // Client-side, never from the host: a bounded `waitForMount` gave up.\n | 'timeout'\n | 'unknown';\n}\n\ntype SpaceResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SPACES], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (method: string, query: Record<string, unknown>): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> => requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> => mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> => requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (ref: { mountId: string; relPath: string }, opts: { mode: 'ro' | 'rw' }): FileCap => ({\n $cap: 'file',\n mountId: ref.mountId,\n relPath: ref.relPath,\n mode: opts.mode,\n});\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(method: string, query: Record<string, unknown> = {}): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SETTINGS], method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n // The host has already accepted the request and announced the mount, so this\n // normally resolves on the initial replay. Bounded anyway: an unbounded wait\n // here turns any delivery failure into a promise that never settles, and every\n // caller of `openSettings()` is doing it to reach durable state — so the app\n // just quietly loses that state with nothing to report.\n return waitForMount({ id: mount.id ?? mount.path }, SETTINGS_MOUNT_TIMEOUT_MS);\n};\n\n/** How long `openSettings()` waits for the host to deliver the mount it just\n * agreed to create. Generous — this is a hang-breaker, not a latency budget. */\nconst SETTINGS_MOUNT_TIMEOUT_MS = 15_000;\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> => settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (opts: { name?: string } = {}): Promise<SandboxMount> =>\n requestMountInternal('create', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\n/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: INVITATIONS,\n requestType: REQUEST_INVITATIONS,\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n"],"mappings":"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","names":["mount"]}
|
package/dist/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
SDK_VERSION: () => SDK_VERSION
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const SDK_VERSION = "0.
|
|
24
|
+
const SDK_VERSION = "0.50.0";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
SDK_VERSION
|
package/dist/version.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.50.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
|
package/dist/version.d.cts
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.50.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@immediately-run/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Runtime SDK for code executing inside an immediately.run sandbox.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:immediately-run/immediately-run-sdk",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"protocol:check": "node scripts/check-protocol-snapshot.mjs",
|
|
37
37
|
"verify:codegen-parity": "node scripts/codegen-prototype/verify-drift.mjs --self-test && node scripts/codegen-prototype/verify-drift.mjs && node scripts/codegen-prototype/verify.streams.mjs --self-test && node scripts/codegen-prototype/verify.streams.mjs",
|
|
38
38
|
"api:update": "node scripts/check-api-stability.mjs --update",
|
|
39
|
-
"verify": "npm run format:check && npm run check:circular && npm run build && npm test && npm run test:safe-content && npm run test:metadata-e2e && npm run api:check && npm run compat:selftest && npm run compat:previous && npm run protocol:check && npm run protocol:selftest && npm run check:ambient:selftest && npm run check:ambient && npm run check:selfhost:selftest && npm run check:selfhost && npm run verify:codegen-parity",
|
|
39
|
+
"verify": "npm run format:check && npm run check:circular && npm run check:bundler:selftest && npm run check:bundler && npm run build && npm test && npm run test:safe-content && npm run test:metadata-e2e && npm run api:check && npm run compat:selftest && npm run compat:previous && npm run protocol:check && npm run protocol:selftest && npm run check:ambient:selftest && npm run check:ambient && npm run check:selfhost:selftest && npm run check:selfhost && npm run verify:codegen-parity",
|
|
40
40
|
"docs": "typedoc --json docs/api.json && node scripts/gen-llms.mjs",
|
|
41
41
|
"prepublishOnly": "npm run check:circular && npm run build && npm run api:check",
|
|
42
42
|
"test:safe-content": "node scripts/build-safecontent-e2e.mjs && node --test test/safeContent.e2e.mjs",
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"check:selfhost": "node scripts/build-selfhost.mjs .selfhost-check && node scripts/check-selfhost-resolvable.mjs --dir .selfhost-check",
|
|
48
48
|
"check:selfhost:selftest": "node scripts/check-selfhost-resolvable.mjs --self-test",
|
|
49
49
|
"prepare": "git config core.hooksPath .githooks || true",
|
|
50
|
+
"check:bundler": "node scripts/check-bundler-reads.mjs",
|
|
51
|
+
"check:bundler:selftest": "node scripts/check-bundler-reads.mjs --self-test",
|
|
50
52
|
"check:ambient": "node scripts/check-ambient-types.mjs",
|
|
51
53
|
"check:ambient:selftest": "node scripts/check-ambient-types.mjs --self-test"
|
|
52
54
|
},
|