@immediately-run/sdk 0.45.3 → 0.47.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/MDXProvider.cjs +1 -5
- package/dist/MDXProvider.cjs.map +1 -1
- package/dist/MDXProvider.js +1 -5
- package/dist/MDXProvider.js.map +1 -1
- package/dist/RoutingSpec.cjs.map +1 -1
- package/dist/TinkerableContext.cjs.map +1 -1
- package/dist/TinkerableContext.js.map +1 -1
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/boot.cjs +2 -9
- package/dist/boot.cjs.map +1 -1
- package/dist/boot.d.cts +2 -2
- package/dist/boot.d.ts +2 -2
- package/dist/boot.js +2 -9
- package/dist/boot.js.map +1 -1
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/components/Admonition.cjs +4 -13
- package/dist/components/Admonition.cjs.map +1 -1
- package/dist/components/Admonition.js +4 -13
- package/dist/components/Admonition.js.map +1 -1
- package/dist/components/FileRouter.cjs +11 -3
- package/dist/components/FileRouter.cjs.map +1 -1
- package/dist/components/FileRouter.js +11 -3
- package/dist/components/FileRouter.js.map +1 -1
- package/dist/components/HeadingAnchor.cjs.map +1 -1
- package/dist/components/HeadingAnchor.js.map +1 -1
- package/dist/components/Link.cjs.map +1 -1
- package/dist/components/Link.js.map +1 -1
- package/dist/components/MainContent.cjs +11 -1
- package/dist/components/MainContent.cjs.map +1 -1
- package/dist/components/MainContent.js +11 -1
- package/dist/components/MainContent.js.map +1 -1
- package/dist/components/MountImage.cjs +1 -9
- package/dist/components/MountImage.cjs.map +1 -1
- package/dist/components/MountImage.js +1 -9
- package/dist/components/MountImage.js.map +1 -1
- package/dist/components/Routes.cjs +1 -4
- package/dist/components/Routes.cjs.map +1 -1
- package/dist/components/Routes.d.cts +1 -1
- package/dist/components/Routes.d.ts +1 -1
- package/dist/components/Routes.js +1 -4
- package/dist/components/Routes.js.map +1 -1
- package/dist/components/SafeInclude.cjs +1 -4
- package/dist/components/SafeInclude.cjs.map +1 -1
- package/dist/components/SafeInclude.js +1 -4
- package/dist/components/SafeInclude.js.map +1 -1
- package/dist/components/WikiLink.cjs +1 -10
- package/dist/components/WikiLink.cjs.map +1 -1
- package/dist/components/WikiLink.js +1 -10
- package/dist/components/WikiLink.js.map +1 -1
- package/dist/components/defaults.cjs.map +1 -1
- package/dist/components/defaults.d.cts +1 -1
- package/dist/components/defaults.d.ts +1 -1
- package/dist/components/defaults.js.map +1 -1
- package/dist/components/errors.cjs +3 -1
- package/dist/components/errors.cjs.map +1 -1
- package/dist/components/errors.js +3 -1
- package/dist/components/errors.js.map +1 -1
- package/dist/contextUtils.cjs.map +1 -1
- package/dist/contextUtils.js.map +1 -1
- package/dist/contribute.cjs.map +1 -1
- package/dist/contribute.js.map +1 -1
- package/dist/debug.cjs +2 -1
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.js +3 -8
- package/dist/debug.js.map +1 -1
- package/dist/diagnostics.cjs.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/editorContext.cjs.map +1 -1
- package/dist/editorContext.js.map +1 -1
- package/dist/fs.cjs.map +1 -1
- package/dist/fs.js.map +1 -1
- package/dist/hostAttention.cjs +57 -0
- package/dist/hostAttention.cjs.map +1 -0
- package/dist/hostAttention.d.cts +39 -0
- package/dist/hostAttention.d.ts +39 -0
- package/dist/hostAttention.js +31 -0
- package/dist/hostAttention.js.map +1 -0
- package/dist/hostTransport.cjs +55 -0
- package/dist/hostTransport.cjs.map +1 -0
- package/dist/hostTransport.d.cts +12 -0
- package/dist/hostTransport.d.ts +12 -0
- package/dist/hostTransport.js +30 -0
- package/dist/hostTransport.js.map +1 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/injectedBundler.cjs.map +1 -1
- package/dist/injectedBundler.js.map +1 -1
- package/dist/ipc.cjs +1 -4
- package/dist/ipc.cjs.map +1 -1
- package/dist/ipc.js +1 -4
- package/dist/ipc.js.map +1 -1
- package/dist/irMarkers.cjs.map +1 -1
- package/dist/irMarkers.d.cts +11 -11
- package/dist/irMarkers.d.ts +11 -11
- package/dist/irMarkers.js.map +1 -1
- package/dist/launch.cjs.map +1 -1
- package/dist/launch.js.map +1 -1
- package/dist/linkSpace.cjs +11 -2
- package/dist/linkSpace.cjs.map +1 -1
- package/dist/linkSpace.d.cts +23 -0
- package/dist/linkSpace.d.ts +23 -0
- package/dist/linkSpace.js +11 -2
- package/dist/linkSpace.js.map +1 -1
- package/dist/llm.cjs +26 -8
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +75 -3
- package/dist/llm.d.ts +75 -3
- package/dist/llm.js +21 -7
- package/dist/llm.js.map +1 -1
- package/dist/loading.cjs +18 -14
- package/dist/loading.cjs.map +1 -1
- package/dist/loading.d.cts +2 -2
- package/dist/loading.d.ts +2 -2
- package/dist/loading.js +19 -19
- package/dist/loading.js.map +1 -1
- package/dist/markers.cjs.map +1 -1
- package/dist/markers.js.map +1 -1
- package/dist/metadataSource.cjs +1 -5
- package/dist/metadataSource.cjs.map +1 -1
- package/dist/metadataSource.d.cts +1 -1
- package/dist/metadataSource.d.ts +1 -1
- package/dist/metadataSource.js +1 -5
- package/dist/metadataSource.js.map +1 -1
- package/dist/moduleCache.cjs +4 -1
- package/dist/moduleCache.cjs.map +1 -1
- package/dist/moduleCache.d.cts +1 -1
- package/dist/moduleCache.d.ts +1 -1
- package/dist/moduleCache.js +4 -1
- package/dist/moduleCache.js.map +1 -1
- package/dist/mountMatch.cjs.map +1 -1
- package/dist/mountMatch.js.map +1 -1
- package/dist/mounts.cjs +6 -1
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.d.cts +2 -2
- package/dist/mounts.d.ts +2 -2
- package/dist/mounts.js +6 -1
- package/dist/mounts.js.map +1 -1
- package/dist/netFetch.cjs +3 -5
- package/dist/netFetch.cjs.map +1 -1
- package/dist/netFetch.js +3 -5
- package/dist/netFetch.js.map +1 -1
- package/dist/onFsChange.cjs.map +1 -1
- package/dist/onFsChange.js.map +1 -1
- package/dist/pathUtils.cjs +9 -12
- package/dist/pathUtils.cjs.map +1 -1
- package/dist/pathUtils.js +9 -12
- package/dist/pathUtils.js.map +1 -1
- package/dist/protocolDeadline.cjs +205 -0
- package/dist/protocolDeadline.cjs.map +1 -0
- package/dist/protocolDeadline.d.cts +146 -0
- package/dist/protocolDeadline.d.ts +146 -0
- package/dist/protocolDeadline.js +168 -0
- package/dist/protocolDeadline.js.map +1 -0
- package/dist/protocolStream.cjs +49 -6
- package/dist/protocolStream.cjs.map +1 -1
- package/dist/protocolStream.d.cts +9 -2
- package/dist/protocolStream.d.ts +9 -2
- package/dist/protocolStream.js +56 -6
- package/dist/protocolStream.js.map +1 -1
- package/dist/pushChannel.cjs +13 -9
- package/dist/pushChannel.cjs.map +1 -1
- package/dist/pushChannel.js +12 -8
- package/dist/pushChannel.js.map +1 -1
- package/dist/ready.cjs.map +1 -1
- package/dist/ready.js.map +1 -1
- package/dist/routing.cjs +3 -1
- package/dist/routing.cjs.map +1 -1
- package/dist/routing.js +3 -1
- package/dist/routing.js.map +1 -1
- package/dist/safeContent/index.cjs.map +1 -1
- package/dist/safeContent/index.js.map +1 -1
- package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
- package/dist/safeContent/parseSafeMdast.js.map +1 -1
- package/dist/safeContent/renderMdast.cjs.map +1 -1
- package/dist/safeContent/renderMdast.js.map +1 -1
- package/dist/sandboxTypes.cjs.map +1 -1
- package/dist/sandboxUtils.cjs +69 -24
- package/dist/sandboxUtils.cjs.map +1 -1
- package/dist/sandboxUtils.d.cts +37 -4
- package/dist/sandboxUtils.d.ts +37 -4
- package/dist/sandboxUtils.js +74 -22
- package/dist/sandboxUtils.js.map +1 -1
- package/dist/scrollToId.cjs.map +1 -1
- package/dist/scrollToId.js.map +1 -1
- package/dist/secrets.cjs.map +1 -1
- package/dist/secrets.js.map +1 -1
- package/dist/tasks.cjs +12 -2
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +12 -2
- package/dist/tasks.js.map +1 -1
- package/dist/testing.cjs +1 -3
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +1 -3
- package/dist/testing.js.map +1 -1
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/urlUtils.cjs +7 -18
- package/dist/urlUtils.cjs.map +1 -1
- package/dist/urlUtils.js +7 -18
- package/dist/urlUtils.js.map +1 -1
- package/dist/vcs.cjs +1 -3
- package/dist/vcs.cjs.map +1 -1
- package/dist/vcs.js +1 -3
- package/dist/vcs.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 +11 -5
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 =>\n 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 = '/' + relPath.split('/').filter((s) => s && s !== '.').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(\n relPath: string,\n opts?: { type?: string },\n ): 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 =>\n (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 =\n 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":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,oBAAgC;AAChC,wBAA2B;AAsB3B,MAAM,QAAQ,CAAC,OACb,OAAO,IAAI,UAAU,aAAa,cAAc,OAAO,IAAI,aAAa;AAcnE,SAAS,YAAkC;AAChD,MAAI;AACF,UAAM,SAAU,WAAmB;AACnC,QAAI,MAAM,MAAM,EAAG,QAAO;AAAA,EAC5B,QAAQ;AAAA,EAER;AACA,MAAI;AAEF,UAAM,SAAS,QAAQ,YAAY,QAAQ,SAAS,IAAI;AACxD,QAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,iBAAW,SAAS,QAAQ;AAC1B,cAAM,KAAK,OAAO,cAAc;AAChC,YAAI,MAAM,EAAE,EAAG,QAAO;AAAA,MACxB;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAIO,SAAS,cAAuB;AACrC,SAAO,UAAU,KAAK;AACxB;AA6BA,MAAM,QAAyC;AAAA,EAC7C,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,WAAW;AACb;AAEA,MAAM,UAAU,CAAC,MAAuB,YAA6B;AACnE,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,SAAO;AACT;AAEA,MAAM,WAAW,CAAC,MAAwB;AACxC,QAAM,QAAS,GAAgC;AAC/C,QAAM,QAAyB,QAAQ,MAAM,KAAK,IAAI,WAAc;AACpE,QAAM,MAAM,IAAI,MAAO,GAAa,WAAW,qBAAqB;AACpE,MAAI,OAAO;AACX,SAAO;AACT;AAKA,IAAI;AACJ,IAAI;AACJ,MAAM,UAAU,MAAoB,wBAAa,IAAI,YAAY;AACjE,MAAM,UAAU,MAAoB,wBAAa,IAAI,YAAY;AAMjE,MAAM,cAAsC;AAAA,EAC1C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AASO,SAAS,YAAY,MAAkC;AAC5D,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,MAAI,MAAM,EAAG,QAAO;AACpB,SAAO,YAAY,KAAK,MAAM,MAAM,CAAC,EAAE,YAAY,CAAC;AACtD;AAKA,MAAM,eAAe,CAAC,MAAc,YAA4B;AAC9D,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAM,QAAQ,gBAAgB,iDAAiD,OAAO,GAAG;AAAA,EAC3F;AACA,QAAM,QAAkB,CAAC;AACzB,aAAW,OAAO,QAAQ,MAAM,GAAG,GAAG;AACpC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,MAAM;AAChB,YAAM,QAAQ,gBAAgB,IAAI,OAAO,0BAA0B;AAAA,IACrE;AACA,UAAM,KAAK,GAAG;AAAA,EAChB;AACA,QAAM,OAAO,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AACtD,SAAO,MAAM,SAAS,GAAG,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,KAAK;AACvD;AAKA,MAAM,aAAa,CAAC,OAAqB,YAA6B;AACpE,QAAM,OAAO,MAAM,QAAQ,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,KAAK,MAAM,GAAG,EAAE,KAAK,GAAG;AAC5E,QAAM,QAAiC,MAAM;AAC7C,MAAI,SAAS,MAAM,QAAQ;AACzB,QAAI;AACJ,eAAW,KAAK,OAAO;AACrB,YAAM,MAAM,EAAE,QAAQ,SAAS,GAAG,IAAI,EAAE,UAAU,EAAE,UAAU;AAC9D,UAAI,SAAS,EAAE,WAAW,KAAK,WAAW,GAAG,KAAK,EAAE,YAAY,KAAK;AACnE,YAAI,CAAC,QAAQ,EAAE,QAAQ,SAAS,KAAK,QAAQ,OAAQ,QAAO;AAAA,MAC9D;AAAA,IACF;AACA,QAAI,KAAM,QAAO,KAAK,SAAS;AAAA,EACjC;AACA,UAAQ,MAAM,QAAQ,UAAU;AAClC;AAyDA,MAAM,aAAa,CAAC,SACjB,KAAK,YAAa;AAqBd,SAAS,OAAO,OAA8B;AACnD,QAAM,OAAO,MAAM;AAEnB,QAAM,OAAO,MAAsB;AACjC,UAAM,IAAI,UAAU;AACpB,QAAI,CAAC,EAAG,OAAM,QAAQ,eAAe,iDAAiD;AACtF,WAAO,WAAW,CAAC;AAAA,EACrB;AAEA,QAAM,MAAe;AAAA,IACnB;AAAA,IACA,MAAM,SAAS,SAAiB,UAAiC;AAC/D,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,OAAO,MAAM,EAAE,SAAS,GAAG;AACjC,cAAM,QACJ,OAAO,SAAS,WAAW,QAAQ,EAAE,OAAO,IAAI,IAAK;AACvD,eAAO,aAAa,SAAS,QAAQ,EAAE,OAAO,KAAK,IAAI;AAAA,MACzD,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,SAAS,SAAS,MAAM;AAC5B,YAAM,QAAQ,MAAM,IAAI,SAAS,OAAO;AACxC,YAAM,OAAO,MAAM,QAAQ,YAAY,OAAO,KAAK;AACnD,aAAO,IAAI,KAAK,CAAC,KAAiB,GAAG,EAAE,KAAK,CAAC;AAAA,IAC/C;AAAA,IACA,MAAM,cAAc,SAAS,MAAM;AACjC,YAAM,OAAO,MAAM,IAAI,SAAS,SAAS,IAAI;AAC7C,YAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,aAAO,EAAE,KAAK,QAAQ,MAAM,IAAI,gBAAgB,GAAG,EAAE;AAAA,IACvD;AAAA,IACA,MAAM,UAAU,SAAS,MAAM;AAC7B,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,EAAE,UAAU,KAAK,OAAO,SAAS,WAAW,QAAQ,EAAE,OAAO,IAAI,IAAI,IAAI;AAAA,MACjF,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,QAAQ,UAAU,IAAI;AAC1B,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,UAAU,MAAM,EAAE,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAC5D,eAAO,QAAQ;AAAA,UAAI,CAAC,MAClB,OAAO,MAAM,WACR,EAAE,MAAM,GAAG,MAAM,OAAO,IACxB,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,cAAc,IAAI,QAAQ,OAAO;AAAA,QAChE;AAAA,MACF,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,KAAK,SAAS;AAClB,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,IAAS,MAAM,EAAE,KAAK,GAAG;AAC/B,eAAO;AAAA,UACL,MAAM,EAAE,cAAc,IAAI,QAAQ;AAAA,UAClC,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AAAA,UAC5C,SAAS,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU;AAAA,QACvD;AAAA,MACF,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,OAAO,SAAS;AACpB,UAAI;AACF,cAAM,IAAI,KAAK,OAAO;AACtB,eAAO;AAAA,MACT,SAAS,GAAG;AACV,YAAK,EAAc,SAAS,YAAa,QAAO;AAChD,YAAK,EAAc,SAAS,iBAAkB,EAAc,SAAS,gBAAgB;AACnF,gBAAM;AAAA,QACR;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM,MAAM,SAAS,MAAM;AACzB,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,EAAE,MAAM,KAAK,EAAE,WAAW,MAAM,aAAa,MAAM,CAAC;AAAA,MAC5D,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,GAAG,SAAS,MAAM;AACtB,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,EAAE,GAAG,KAAK,EAAE,WAAW,MAAM,aAAa,MAAM,CAAC;AAAA,MACzD,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,OAAO,SAAS,OAAO;AAC3B,YAAM,IAAI,KAAK;AACf,YAAM,OAAO,aAAa,MAAM,OAAO;AACvC,YAAM,KAAK,aAAa,MAAM,KAAK;AACnC,UAAI;AACF,cAAM,EAAE,OAAO,MAAM,EAAE;AAAA,MACzB,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,SAAS,UAAU,IAAI;AACrB,aAAO,WAAW,OAAO,OAAO;AAAA,IAClC;AAAA,IACA,SAAS,IAAI;AAKX,UAAI,aAAS,+BAAgB,GAAG;AAC9B,eAAO,MAAM;AAAA,QAAC;AAAA,MAChB;AACA,iBAAO,8BAAW,CAAC,WAAW;AAI5B,YAAI,OAAO,MAAM,WAAW,EAAG;AAC/B,WAAG,OAAO,MAAM,IAAI,CAAC,MAAM,EAAE,QAAQ,QAAQ,EAAE,CAAC,CAAC;AAAA,MACnD,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAIO,SAAS,YAAqB;AACnC,SAAO,OAAO,EAAE,UAAM,+BAAgB,GAAG,MAAM,OAAO,CAAiB;AACzE;","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 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 =>\n 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 = '/' + relPath.split('/').filter((s) => s && s !== '.').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(\n relPath: string,\n opts?: { type?: string },\n ): 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 =>\n (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 =\n 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":";AAeA,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAsB3B,MAAM,QAAQ,CAAC,OACb,OAAO,IAAI,UAAU,aAAa,cAAc,OAAO,IAAI,aAAa;AAcnE,SAAS,YAAkC;AAChD,MAAI;AACF,UAAM,SAAU,WAAmB;AACnC,QAAI,MAAM,MAAM,EAAG,QAAO;AAAA,EAC5B,QAAQ;AAAA,EAER;AACA,MAAI;AAEF,UAAM,SAAS,QAAQ,YAAY,QAAQ,SAAS,IAAI;AACxD,QAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,iBAAW,SAAS,QAAQ;AAC1B,cAAM,KAAK,OAAO,cAAc;AAChC,YAAI,MAAM,EAAE,EAAG,QAAO;AAAA,MACxB;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAIO,SAAS,cAAuB;AACrC,SAAO,UAAU,KAAK;AACxB;AA6BA,MAAM,QAAyC;AAAA,EAC7C,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,WAAW;AACb;AAEA,MAAM,UAAU,CAAC,MAAuB,YAA6B;AACnE,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,SAAO;AACT;AAEA,MAAM,WAAW,CAAC,MAAwB;AACxC,QAAM,QAAS,GAAgC;AAC/C,QAAM,QAAyB,QAAQ,MAAM,KAAK,IAAI,WAAc;AACpE,QAAM,MAAM,IAAI,MAAO,GAAa,WAAW,qBAAqB;AACpE,MAAI,OAAO;AACX,SAAO;AACT;AAKA,IAAI;AACJ,IAAI;AACJ,MAAM,UAAU,MAAoB,wBAAa,IAAI,YAAY;AACjE,MAAM,UAAU,MAAoB,wBAAa,IAAI,YAAY;AAMjE,MAAM,cAAsC;AAAA,EAC1C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AASO,SAAS,YAAY,MAAkC;AAC5D,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,MAAI,MAAM,EAAG,QAAO;AACpB,SAAO,YAAY,KAAK,MAAM,MAAM,CAAC,EAAE,YAAY,CAAC;AACtD;AAKA,MAAM,eAAe,CAAC,MAAc,YAA4B;AAC9D,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAM,QAAQ,gBAAgB,iDAAiD,OAAO,GAAG;AAAA,EAC3F;AACA,QAAM,QAAkB,CAAC;AACzB,aAAW,OAAO,QAAQ,MAAM,GAAG,GAAG;AACpC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,MAAM;AAChB,YAAM,QAAQ,gBAAgB,IAAI,OAAO,0BAA0B;AAAA,IACrE;AACA,UAAM,KAAK,GAAG;AAAA,EAChB;AACA,QAAM,OAAO,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AACtD,SAAO,MAAM,SAAS,GAAG,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,KAAK;AACvD;AAKA,MAAM,aAAa,CAAC,OAAqB,YAA6B;AACpE,QAAM,OAAO,MAAM,QAAQ,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,KAAK,MAAM,GAAG,EAAE,KAAK,GAAG;AAC5E,QAAM,QAAiC,MAAM;AAC7C,MAAI,SAAS,MAAM,QAAQ;AACzB,QAAI;AACJ,eAAW,KAAK,OAAO;AACrB,YAAM,MAAM,EAAE,QAAQ,SAAS,GAAG,IAAI,EAAE,UAAU,EAAE,UAAU;AAC9D,UAAI,SAAS,EAAE,WAAW,KAAK,WAAW,GAAG,KAAK,EAAE,YAAY,KAAK;AACnE,YAAI,CAAC,QAAQ,EAAE,QAAQ,SAAS,KAAK,QAAQ,OAAQ,QAAO;AAAA,MAC9D;AAAA,IACF;AACA,QAAI,KAAM,QAAO,KAAK,SAAS;AAAA,EACjC;AACA,UAAQ,MAAM,QAAQ,UAAU;AAClC;AAyDA,MAAM,aAAa,CAAC,SACjB,KAAK,YAAa;AAqBd,SAAS,OAAO,OAA8B;AACnD,QAAM,OAAO,MAAM;AAEnB,QAAM,OAAO,MAAsB;AACjC,UAAM,IAAI,UAAU;AACpB,QAAI,CAAC,EAAG,OAAM,QAAQ,eAAe,iDAAiD;AACtF,WAAO,WAAW,CAAC;AAAA,EACrB;AAEA,QAAM,MAAe;AAAA,IACnB;AAAA,IACA,MAAM,SAAS,SAAiB,UAAiC;AAC/D,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,OAAO,MAAM,EAAE,SAAS,GAAG;AACjC,cAAM,QACJ,OAAO,SAAS,WAAW,QAAQ,EAAE,OAAO,IAAI,IAAK;AACvD,eAAO,aAAa,SAAS,QAAQ,EAAE,OAAO,KAAK,IAAI;AAAA,MACzD,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,SAAS,SAAS,MAAM;AAC5B,YAAM,QAAQ,MAAM,IAAI,SAAS,OAAO;AACxC,YAAM,OAAO,MAAM,QAAQ,YAAY,OAAO,KAAK;AACnD,aAAO,IAAI,KAAK,CAAC,KAAiB,GAAG,EAAE,KAAK,CAAC;AAAA,IAC/C;AAAA,IACA,MAAM,cAAc,SAAS,MAAM;AACjC,YAAM,OAAO,MAAM,IAAI,SAAS,SAAS,IAAI;AAC7C,YAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,aAAO,EAAE,KAAK,QAAQ,MAAM,IAAI,gBAAgB,GAAG,EAAE;AAAA,IACvD;AAAA,IACA,MAAM,UAAU,SAAS,MAAM;AAC7B,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,EAAE,UAAU,KAAK,OAAO,SAAS,WAAW,QAAQ,EAAE,OAAO,IAAI,IAAI,IAAI;AAAA,MACjF,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,QAAQ,UAAU,IAAI;AAC1B,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,UAAU,MAAM,EAAE,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAC5D,eAAO,QAAQ;AAAA,UAAI,CAAC,MAClB,OAAO,MAAM,WACR,EAAE,MAAM,GAAG,MAAM,OAAO,IACxB,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,cAAc,IAAI,QAAQ,OAAO;AAAA,QAChE;AAAA,MACF,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,KAAK,SAAS;AAClB,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,IAAS,MAAM,EAAE,KAAK,GAAG;AAC/B,eAAO;AAAA,UACL,MAAM,EAAE,cAAc,IAAI,QAAQ;AAAA,UAClC,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AAAA,UAC5C,SAAS,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU;AAAA,QACvD;AAAA,MACF,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,OAAO,SAAS;AACpB,UAAI;AACF,cAAM,IAAI,KAAK,OAAO;AACtB,eAAO;AAAA,MACT,SAAS,GAAG;AACV,YAAK,EAAc,SAAS,YAAa,QAAO;AAChD,YAAK,EAAc,SAAS,iBAAkB,EAAc,SAAS,gBAAgB;AACnF,gBAAM;AAAA,QACR;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM,MAAM,SAAS,MAAM;AACzB,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,EAAE,MAAM,KAAK,EAAE,WAAW,MAAM,aAAa,MAAM,CAAC;AAAA,MAC5D,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,GAAG,SAAS,MAAM;AACtB,YAAM,IAAI,KAAK;AACf,YAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAI;AACF,cAAM,EAAE,GAAG,KAAK,EAAE,WAAW,MAAM,aAAa,MAAM,CAAC;AAAA,MACzD,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,MAAM,OAAO,SAAS,OAAO;AAC3B,YAAM,IAAI,KAAK;AACf,YAAM,OAAO,aAAa,MAAM,OAAO;AACvC,YAAM,KAAK,aAAa,MAAM,KAAK;AACnC,UAAI;AACF,cAAM,EAAE,OAAO,MAAM,EAAE;AAAA,MACzB,SAAS,GAAG;AACV,cAAM,SAAS,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,IACA,SAAS,UAAU,IAAI;AACrB,aAAO,WAAW,OAAO,OAAO;AAAA,IAClC;AAAA,IACA,SAAS,IAAI;AAKX,UAAI,SAAS,gBAAgB,GAAG;AAC9B,eAAO,MAAM;AAAA,QAAC;AAAA,MAChB;AACA,aAAO,WAAW,CAAC,WAAW;AAI5B,YAAI,OAAO,MAAM,WAAW,EAAG;AAC/B,WAAG,OAAO,MAAM,IAAI,CAAC,MAAM,EAAE,QAAQ,QAAQ,EAAE,CAAC,CAAC;AAAA,MACnD,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAIO,SAAS,YAAqB;AACnC,SAAO,OAAO,EAAE,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAiB;AACzE;","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 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":[]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var hostAttention_exports = {};
|
|
20
|
+
__export(hostAttention_exports, {
|
|
21
|
+
NO_HOST_ATTENTION: () => NO_HOST_ATTENTION,
|
|
22
|
+
getHostAttention: () => getHostAttention,
|
|
23
|
+
onHostAttentionChange: () => onHostAttentionChange,
|
|
24
|
+
useHostAttention: () => useHostAttention
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(hostAttention_exports);
|
|
27
|
+
var import_pushChannel = require("./pushChannel");
|
|
28
|
+
var import_protocol = require("./generated/protocol");
|
|
29
|
+
const NO_HOST_ATTENTION = { awaiting: false, kind: null, since: null };
|
|
30
|
+
const KINDS = /* @__PURE__ */ new Set(["passkey", "consent", "picker", "confirmation"]);
|
|
31
|
+
const parseHostAttention = (value) => {
|
|
32
|
+
const a = value;
|
|
33
|
+
if (!a || typeof a !== "object" || typeof a.awaiting !== "boolean") return void 0;
|
|
34
|
+
if (!a.awaiting) return NO_HOST_ATTENTION;
|
|
35
|
+
return {
|
|
36
|
+
awaiting: true,
|
|
37
|
+
kind: typeof a.kind === "string" && KINDS.has(a.kind) ? a.kind : null,
|
|
38
|
+
since: typeof a.since === "number" ? a.since : null
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const channel = (0, import_pushChannel.createPushChannel)({
|
|
42
|
+
pushType: import_protocol.HOST_ATTENTION,
|
|
43
|
+
requestType: import_protocol.REQUEST_HOST_ATTENTION,
|
|
44
|
+
initial: NO_HOST_ATTENTION,
|
|
45
|
+
parse: (msg) => parseHostAttention(msg.attention)
|
|
46
|
+
});
|
|
47
|
+
const getHostAttention = () => channel.get();
|
|
48
|
+
const onHostAttentionChange = (listener) => channel.onChange(listener);
|
|
49
|
+
const useHostAttention = () => channel.use();
|
|
50
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
51
|
+
0 && (module.exports = {
|
|
52
|
+
NO_HOST_ATTENTION,
|
|
53
|
+
getHostAttention,
|
|
54
|
+
onHostAttentionChange,
|
|
55
|
+
useHostAttention
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=hostAttention.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hostAttention.ts"],"sourcesContent":["// The host-attention channel (R3-307) — what the host is waiting for, right now.\n//\n// THE GAP THIS CLOSES. R3-298 bounded every host protocol call and classified each as\n// `attended` or `unattended`, but classification is per `(scheme, method)` and\n// attendedness is really a property of a MOMENT: `spaces:mount` is unattended when the\n// grant is already held and attended on first use, because the host raises consent INSIDE\n// the request. A per-method table cannot tell those apart, so R3-298 put anything that MAY\n// prompt on the long (attended) bound — which means the common grant-held path also waits\n// ten minutes before it reports a fault, and a caller can only say \"still waiting on\n// something that may need you\", never \"tap your passkey\".\n//\n// Only the HOST knows a prompt is on screen right now and what it is. So it says so, on a\n// channel every app can read, and two things become possible:\n//\n// 1. A surface can name what it is waiting for. \"Waiting for your passkey…\" instead of a\n// spinner is most of a user's ability to act — especially on the provider-setup\n// wizard's Test-connection step, whose whole purpose is to prove setup worked.\n// 2. The attended bounds become a fact rather than a guess. A may-prompt call runs on the\n// SHORT bound and is suspended only while the host says a person is being asked\n// (`protocolDeadline.ts`, `sandboxUtils.withDeadline`).\n//\n// WHAT IT DELIBERATELY DOES NOT CARRY. No secret value, no secret id, no app key, no\n// capability, no resource name — nothing from which a held grant could be inferred. It is\n// readable by every app at the baseline principal, so it says only that the HOST is busy\n// and with what KIND of prompt. That is information the user is already looking at: a host\n// modal is visible chrome. (The residual it does leave — one app observing that some prompt\n// is up while another app's call is in flight — is the accepted low-bandwidth L0 signalling\n// class of the threat model §7.6, and carries no identifying detail to signal WITH.)\nimport { createPushChannel } from './pushChannel';\nimport { HOST_ATTENTION, REQUEST_HOST_ATTENTION } from './generated/protocol';\n\n/**\n * What kind of host prompt is up.\n *\n * A closed set on purpose: it is the coarsest vocabulary that still lets a surface write a\n * sentence a user can act on, and adding a finer kind would start to describe the specific\n * thing being asked about — which is the disclosure this channel refuses.\n *\n * - `passkey` — a WebAuthn assertion. The user must tap their authenticator.\n * - `consent` — a capability/consent decision (mount, create, share, import, reference).\n * - `picker` — a host-drawn chooser or entry surface (the powerbox, the add-secret modal).\n * - `confirmation` — a notice or confirm the user must dismiss.\n */\nexport type HostAttentionKind = 'passkey' | 'consent' | 'picker' | 'confirmation';\n\n/** Whether the host is waiting on the user right now, and for what. */\nexport interface HostAttention {\n /** `true` while a host prompt is on screen awaiting the user. */\n awaiting: boolean;\n /** The kind of prompt on screen — the innermost one, when they nest (an add-secret modal\n * that reaches the passkey unlock reports `passkey`). `null` when not awaiting. */\n kind: HostAttentionKind | null;\n /** `Date.now()` when the current wait began — the start of the whole continuous span, not\n * of the innermost prompt. `null` when not awaiting. */\n since: number | null;\n}\n\n/** Assumed before the host reports, and the value on a host that never pushes this channel\n * at all: nobody is being asked. Defaulting the OTHER way would suspend every may-prompt\n * deadline forever on an older host, which is the hang this whole line of work removes. */\nexport const NO_HOST_ATTENTION: HostAttention = { awaiting: false, kind: null, since: null };\n\nconst KINDS: ReadonlySet<string> = new Set(['passkey', 'consent', 'picker', 'confirmation']);\n\n/** Tolerant parse: a host that pushes a malformed payload is ignored, not believed. An\n * `awaiting` frame with no valid `kind` still counts as awaiting — the wait is the\n * load-bearing bit and the copy can fall back to a generic sentence. */\nconst parseHostAttention = (value: unknown): HostAttention | undefined => {\n const a = value as Partial<HostAttention> | null | undefined;\n if (!a || typeof a !== 'object' || typeof a.awaiting !== 'boolean') return undefined;\n if (!a.awaiting) return NO_HOST_ATTENTION;\n return {\n awaiting: true,\n kind: typeof a.kind === 'string' && KINDS.has(a.kind) ? (a.kind as HostAttentionKind) : null,\n since: typeof a.since === 'number' ? a.since : null,\n };\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `host-attention` and\n// answers `request-host-attention` (wire format: site-main channelBridge.ts). Read\n// DEFENSIVELY — a host that does not yet push it simply never answers, `initial` stands,\n// and every caller behaves exactly as it did before this channel existed.\nconst channel = createPushChannel<HostAttention>({\n pushType: HOST_ATTENTION,\n requestType: REQUEST_HOST_ATTENTION,\n initial: NO_HOST_ATTENTION,\n parse: (msg) => parseHostAttention(msg.attention),\n});\n\n/** Returns what the host is waiting for right now. Poll for a one-off read. */\nexport const getHostAttention = (): HostAttention => channel.get();\n\n/**\n * Subscribe to host-attention changes. The listener is invoked immediately with the current\n * value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostAttentionChange = (listener: (attention: HostAttention) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning what the host is waiting for, re-rendering on change. */\nexport const useHostAttention = (): HostAttention => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BA,yBAAkC;AAClC,sBAAuD;AA+BhD,MAAM,oBAAmC,EAAE,UAAU,OAAO,MAAM,MAAM,OAAO,KAAK;AAE3F,MAAM,QAA6B,oBAAI,IAAI,CAAC,WAAW,WAAW,UAAU,cAAc,CAAC;AAK3F,MAAM,qBAAqB,CAAC,UAA8C;AACxE,QAAM,IAAI;AACV,MAAI,CAAC,KAAK,OAAO,MAAM,YAAY,OAAO,EAAE,aAAa,UAAW,QAAO;AAC3E,MAAI,CAAC,EAAE,SAAU,QAAO;AACxB,SAAO;AAAA,IACL,UAAU;AAAA,IACV,MAAM,OAAO,EAAE,SAAS,YAAY,MAAM,IAAI,EAAE,IAAI,IAAK,EAAE,OAA6B;AAAA,IACxF,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;AAAA,EACjD;AACF;AAMA,MAAM,cAAU,sCAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ,mBAAmB,IAAI,SAAS;AAClD,CAAC;AAGM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What kind of host prompt is up.
|
|
3
|
+
*
|
|
4
|
+
* A closed set on purpose: it is the coarsest vocabulary that still lets a surface write a
|
|
5
|
+
* sentence a user can act on, and adding a finer kind would start to describe the specific
|
|
6
|
+
* thing being asked about — which is the disclosure this channel refuses.
|
|
7
|
+
*
|
|
8
|
+
* - `passkey` — a WebAuthn assertion. The user must tap their authenticator.
|
|
9
|
+
* - `consent` — a capability/consent decision (mount, create, share, import, reference).
|
|
10
|
+
* - `picker` — a host-drawn chooser or entry surface (the powerbox, the add-secret modal).
|
|
11
|
+
* - `confirmation` — a notice or confirm the user must dismiss.
|
|
12
|
+
*/
|
|
13
|
+
type HostAttentionKind = 'passkey' | 'consent' | 'picker' | 'confirmation';
|
|
14
|
+
/** Whether the host is waiting on the user right now, and for what. */
|
|
15
|
+
interface HostAttention {
|
|
16
|
+
/** `true` while a host prompt is on screen awaiting the user. */
|
|
17
|
+
awaiting: boolean;
|
|
18
|
+
/** The kind of prompt on screen — the innermost one, when they nest (an add-secret modal
|
|
19
|
+
* that reaches the passkey unlock reports `passkey`). `null` when not awaiting. */
|
|
20
|
+
kind: HostAttentionKind | null;
|
|
21
|
+
/** `Date.now()` when the current wait began — the start of the whole continuous span, not
|
|
22
|
+
* of the innermost prompt. `null` when not awaiting. */
|
|
23
|
+
since: number | null;
|
|
24
|
+
}
|
|
25
|
+
/** Assumed before the host reports, and the value on a host that never pushes this channel
|
|
26
|
+
* at all: nobody is being asked. Defaulting the OTHER way would suspend every may-prompt
|
|
27
|
+
* deadline forever on an older host, which is the hang this whole line of work removes. */
|
|
28
|
+
declare const NO_HOST_ATTENTION: HostAttention;
|
|
29
|
+
/** Returns what the host is waiting for right now. Poll for a one-off read. */
|
|
30
|
+
declare const getHostAttention: () => HostAttention;
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to host-attention changes. The listener is invoked immediately with the current
|
|
33
|
+
* value, then again on every change. Returns an unsubscribe fn.
|
|
34
|
+
*/
|
|
35
|
+
declare const onHostAttentionChange: (listener: (attention: HostAttention) => void) => (() => void);
|
|
36
|
+
/** React hook returning what the host is waiting for, re-rendering on change. */
|
|
37
|
+
declare const useHostAttention: () => HostAttention;
|
|
38
|
+
|
|
39
|
+
export { type HostAttention, type HostAttentionKind, NO_HOST_ATTENTION, getHostAttention, onHostAttentionChange, useHostAttention };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What kind of host prompt is up.
|
|
3
|
+
*
|
|
4
|
+
* A closed set on purpose: it is the coarsest vocabulary that still lets a surface write a
|
|
5
|
+
* sentence a user can act on, and adding a finer kind would start to describe the specific
|
|
6
|
+
* thing being asked about — which is the disclosure this channel refuses.
|
|
7
|
+
*
|
|
8
|
+
* - `passkey` — a WebAuthn assertion. The user must tap their authenticator.
|
|
9
|
+
* - `consent` — a capability/consent decision (mount, create, share, import, reference).
|
|
10
|
+
* - `picker` — a host-drawn chooser or entry surface (the powerbox, the add-secret modal).
|
|
11
|
+
* - `confirmation` — a notice or confirm the user must dismiss.
|
|
12
|
+
*/
|
|
13
|
+
type HostAttentionKind = 'passkey' | 'consent' | 'picker' | 'confirmation';
|
|
14
|
+
/** Whether the host is waiting on the user right now, and for what. */
|
|
15
|
+
interface HostAttention {
|
|
16
|
+
/** `true` while a host prompt is on screen awaiting the user. */
|
|
17
|
+
awaiting: boolean;
|
|
18
|
+
/** The kind of prompt on screen — the innermost one, when they nest (an add-secret modal
|
|
19
|
+
* that reaches the passkey unlock reports `passkey`). `null` when not awaiting. */
|
|
20
|
+
kind: HostAttentionKind | null;
|
|
21
|
+
/** `Date.now()` when the current wait began — the start of the whole continuous span, not
|
|
22
|
+
* of the innermost prompt. `null` when not awaiting. */
|
|
23
|
+
since: number | null;
|
|
24
|
+
}
|
|
25
|
+
/** Assumed before the host reports, and the value on a host that never pushes this channel
|
|
26
|
+
* at all: nobody is being asked. Defaulting the OTHER way would suspend every may-prompt
|
|
27
|
+
* deadline forever on an older host, which is the hang this whole line of work removes. */
|
|
28
|
+
declare const NO_HOST_ATTENTION: HostAttention;
|
|
29
|
+
/** Returns what the host is waiting for right now. Poll for a one-off read. */
|
|
30
|
+
declare const getHostAttention: () => HostAttention;
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to host-attention changes. The listener is invoked immediately with the current
|
|
33
|
+
* value, then again on every change. Returns an unsubscribe fn.
|
|
34
|
+
*/
|
|
35
|
+
declare const onHostAttentionChange: (listener: (attention: HostAttention) => void) => (() => void);
|
|
36
|
+
/** React hook returning what the host is waiting for, re-rendering on change. */
|
|
37
|
+
declare const useHostAttention: () => HostAttention;
|
|
38
|
+
|
|
39
|
+
export { type HostAttention, type HostAttentionKind, NO_HOST_ATTENTION, getHostAttention, onHostAttentionChange, useHostAttention };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import "./chunk-VHAA22YE.js";
|
|
2
|
+
import { createPushChannel } from "./pushChannel";
|
|
3
|
+
import { HOST_ATTENTION, REQUEST_HOST_ATTENTION } from "./generated/protocol";
|
|
4
|
+
const NO_HOST_ATTENTION = { awaiting: false, kind: null, since: null };
|
|
5
|
+
const KINDS = /* @__PURE__ */ new Set(["passkey", "consent", "picker", "confirmation"]);
|
|
6
|
+
const parseHostAttention = (value) => {
|
|
7
|
+
const a = value;
|
|
8
|
+
if (!a || typeof a !== "object" || typeof a.awaiting !== "boolean") return void 0;
|
|
9
|
+
if (!a.awaiting) return NO_HOST_ATTENTION;
|
|
10
|
+
return {
|
|
11
|
+
awaiting: true,
|
|
12
|
+
kind: typeof a.kind === "string" && KINDS.has(a.kind) ? a.kind : null,
|
|
13
|
+
since: typeof a.since === "number" ? a.since : null
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
const channel = createPushChannel({
|
|
17
|
+
pushType: HOST_ATTENTION,
|
|
18
|
+
requestType: REQUEST_HOST_ATTENTION,
|
|
19
|
+
initial: NO_HOST_ATTENTION,
|
|
20
|
+
parse: (msg) => parseHostAttention(msg.attention)
|
|
21
|
+
});
|
|
22
|
+
const getHostAttention = () => channel.get();
|
|
23
|
+
const onHostAttentionChange = (listener) => channel.onChange(listener);
|
|
24
|
+
const useHostAttention = () => channel.use();
|
|
25
|
+
export {
|
|
26
|
+
NO_HOST_ATTENTION,
|
|
27
|
+
getHostAttention,
|
|
28
|
+
onHostAttentionChange,
|
|
29
|
+
useHostAttention
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=hostAttention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hostAttention.ts"],"sourcesContent":["// The host-attention channel (R3-307) — what the host is waiting for, right now.\n//\n// THE GAP THIS CLOSES. R3-298 bounded every host protocol call and classified each as\n// `attended` or `unattended`, but classification is per `(scheme, method)` and\n// attendedness is really a property of a MOMENT: `spaces:mount` is unattended when the\n// grant is already held and attended on first use, because the host raises consent INSIDE\n// the request. A per-method table cannot tell those apart, so R3-298 put anything that MAY\n// prompt on the long (attended) bound — which means the common grant-held path also waits\n// ten minutes before it reports a fault, and a caller can only say \"still waiting on\n// something that may need you\", never \"tap your passkey\".\n//\n// Only the HOST knows a prompt is on screen right now and what it is. So it says so, on a\n// channel every app can read, and two things become possible:\n//\n// 1. A surface can name what it is waiting for. \"Waiting for your passkey…\" instead of a\n// spinner is most of a user's ability to act — especially on the provider-setup\n// wizard's Test-connection step, whose whole purpose is to prove setup worked.\n// 2. The attended bounds become a fact rather than a guess. A may-prompt call runs on the\n// SHORT bound and is suspended only while the host says a person is being asked\n// (`protocolDeadline.ts`, `sandboxUtils.withDeadline`).\n//\n// WHAT IT DELIBERATELY DOES NOT CARRY. No secret value, no secret id, no app key, no\n// capability, no resource name — nothing from which a held grant could be inferred. It is\n// readable by every app at the baseline principal, so it says only that the HOST is busy\n// and with what KIND of prompt. That is information the user is already looking at: a host\n// modal is visible chrome. (The residual it does leave — one app observing that some prompt\n// is up while another app's call is in flight — is the accepted low-bandwidth L0 signalling\n// class of the threat model §7.6, and carries no identifying detail to signal WITH.)\nimport { createPushChannel } from './pushChannel';\nimport { HOST_ATTENTION, REQUEST_HOST_ATTENTION } from './generated/protocol';\n\n/**\n * What kind of host prompt is up.\n *\n * A closed set on purpose: it is the coarsest vocabulary that still lets a surface write a\n * sentence a user can act on, and adding a finer kind would start to describe the specific\n * thing being asked about — which is the disclosure this channel refuses.\n *\n * - `passkey` — a WebAuthn assertion. The user must tap their authenticator.\n * - `consent` — a capability/consent decision (mount, create, share, import, reference).\n * - `picker` — a host-drawn chooser or entry surface (the powerbox, the add-secret modal).\n * - `confirmation` — a notice or confirm the user must dismiss.\n */\nexport type HostAttentionKind = 'passkey' | 'consent' | 'picker' | 'confirmation';\n\n/** Whether the host is waiting on the user right now, and for what. */\nexport interface HostAttention {\n /** `true` while a host prompt is on screen awaiting the user. */\n awaiting: boolean;\n /** The kind of prompt on screen — the innermost one, when they nest (an add-secret modal\n * that reaches the passkey unlock reports `passkey`). `null` when not awaiting. */\n kind: HostAttentionKind | null;\n /** `Date.now()` when the current wait began — the start of the whole continuous span, not\n * of the innermost prompt. `null` when not awaiting. */\n since: number | null;\n}\n\n/** Assumed before the host reports, and the value on a host that never pushes this channel\n * at all: nobody is being asked. Defaulting the OTHER way would suspend every may-prompt\n * deadline forever on an older host, which is the hang this whole line of work removes. */\nexport const NO_HOST_ATTENTION: HostAttention = { awaiting: false, kind: null, since: null };\n\nconst KINDS: ReadonlySet<string> = new Set(['passkey', 'consent', 'picker', 'confirmation']);\n\n/** Tolerant parse: a host that pushes a malformed payload is ignored, not believed. An\n * `awaiting` frame with no valid `kind` still counts as awaiting — the wait is the\n * load-bearing bit and the copy can fall back to a generic sentence. */\nconst parseHostAttention = (value: unknown): HostAttention | undefined => {\n const a = value as Partial<HostAttention> | null | undefined;\n if (!a || typeof a !== 'object' || typeof a.awaiting !== 'boolean') return undefined;\n if (!a.awaiting) return NO_HOST_ATTENTION;\n return {\n awaiting: true,\n kind: typeof a.kind === 'string' && KINDS.has(a.kind) ? (a.kind as HostAttentionKind) : null,\n since: typeof a.since === 'number' ? a.since : null,\n };\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `host-attention` and\n// answers `request-host-attention` (wire format: site-main channelBridge.ts). Read\n// DEFENSIVELY — a host that does not yet push it simply never answers, `initial` stands,\n// and every caller behaves exactly as it did before this channel existed.\nconst channel = createPushChannel<HostAttention>({\n pushType: HOST_ATTENTION,\n requestType: REQUEST_HOST_ATTENTION,\n initial: NO_HOST_ATTENTION,\n parse: (msg) => parseHostAttention(msg.attention),\n});\n\n/** Returns what the host is waiting for right now. Poll for a one-off read. */\nexport const getHostAttention = (): HostAttention => channel.get();\n\n/**\n * Subscribe to host-attention changes. The listener is invoked immediately with the current\n * value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostAttentionChange = (listener: (attention: HostAttention) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning what the host is waiting for, re-rendering on change. */\nexport const useHostAttention = (): HostAttention => channel.use();\n"],"mappings":";AA4BA,SAAS,yBAAyB;AAClC,SAAS,gBAAgB,8BAA8B;AA+BhD,MAAM,oBAAmC,EAAE,UAAU,OAAO,MAAM,MAAM,OAAO,KAAK;AAE3F,MAAM,QAA6B,oBAAI,IAAI,CAAC,WAAW,WAAW,UAAU,cAAc,CAAC;AAK3F,MAAM,qBAAqB,CAAC,UAA8C;AACxE,QAAM,IAAI;AACV,MAAI,CAAC,KAAK,OAAO,MAAM,YAAY,OAAO,EAAE,aAAa,UAAW,QAAO;AAC3E,MAAI,CAAC,EAAE,SAAU,QAAO;AACxB,SAAO;AAAA,IACL,UAAU;AAAA,IACV,MAAM,OAAO,EAAE,SAAS,YAAY,MAAM,IAAI,EAAE,IAAI,IAAK,EAAE,OAA6B;AAAA,IACxF,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;AAAA,EACjD;AACF;AAMA,MAAM,UAAU,kBAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ,mBAAmB,IAAI,SAAS;AAClD,CAAC;AAGM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var hostTransport_exports = {};
|
|
20
|
+
__export(hostTransport_exports, {
|
|
21
|
+
addListener: () => addListener,
|
|
22
|
+
sendMessage: () => sendMessage,
|
|
23
|
+
transport: () => transport
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(hostTransport_exports);
|
|
26
|
+
var import_hostRuntime = require("./hostRuntime");
|
|
27
|
+
function transport() {
|
|
28
|
+
try {
|
|
29
|
+
const injected = module?.evaluation?.module?.bundler?.messageBus;
|
|
30
|
+
if (injected && typeof injected.sendMessage === "function") return injected;
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
const t = (0, import_hostRuntime.getHostRuntime)()?.transport;
|
|
34
|
+
if (t && typeof t.sendMessage === "function") return t;
|
|
35
|
+
throw new Error("immediately.run: no host transport (neither injected nor __immediatelyRun__)");
|
|
36
|
+
}
|
|
37
|
+
const sendMessage = (type, data = {}) => {
|
|
38
|
+
transport().sendMessage(type, data);
|
|
39
|
+
};
|
|
40
|
+
const addListener = (msgType, handler, event) => {
|
|
41
|
+
const onMessage = event ?? transport().onMessage;
|
|
42
|
+
const disposable = onMessage((msg) => {
|
|
43
|
+
if (msg.type === msgType) {
|
|
44
|
+
handler(msg);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => disposable.dispose();
|
|
48
|
+
};
|
|
49
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
50
|
+
0 && (module.exports = {
|
|
51
|
+
addListener,
|
|
52
|
+
sendMessage,
|
|
53
|
+
transport
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=hostTransport.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hostTransport.ts"],"sourcesContent":["// The host transport resolver (SDK_PACKAGING_SPEC §4, expose-transport) — the ONE place\n// that answers \"how do I reach the host from here\".\n//\n// All host comms (sendMessage / protocolRequest / onMessage) go through this resolver so\n// the SDK is transport-agnostic: it works whether it was INJECTED into the bundler's\n// evaluation context (the current path — `module.evaluation.module.bundler.messageBus`) OR\n// fetched from npm as an ordinary dependency, in which case the sandbox runtime hands it\n// the transport via the §4 discovery global (`globalThis.__immediatelyRun__`).\n//\n// Dual-mode (§8): injection wins while it's active, so existing behaviour is byte-for-byte\n// preserved; the global is the fallback the npm-fetched SDK uses. When injection is removed\n// (phase 3), only the global path remains — `bundler.*` stops being API, which is the whole\n// point.\n//\n// WHY THIS IS ITS OWN MODULE (R3-307). It was the bottom half of `sandboxUtils.ts` until\n// the deadline machinery grew a dependency on the host-attention push channel. A push\n// channel needs the transport, and `withDeadline` needs the channel, so leaving both in one\n// module made `sandboxUtils → hostAttention → pushChannel → sandboxUtils` — a cycle, and\n// this package's import graph is CI-enforced acyclic (`check:circular`; a circular SDK\n// import once made the bundler infinite-loop). Splitting the *primitives* out puts\n// `sandboxUtils` at the TOP of the graph instead of in the middle of it. `sandboxUtils`\n// re-exports `sendMessage`/`addListener`, so every existing import site is unchanged and\n// the api-snapshot entry for that module keeps all four of its names.\nimport { getHostRuntime } from './hostRuntime';\n\nexport interface HostTransport {\n sendMessage(type: string, data?: Record<string, any>): void;\n protocolRequest(protocolName: string, method: string, params: Array<any>): Promise<any>;\n onMessage(handler: (msg: any) => void): { dispose(): void };\n}\n\nexport function transport(): HostTransport {\n // Injected bundler messageBus first — the current path, unchanged.\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n const injected = module?.evaluation?.module?.bundler?.messageBus;\n if (injected && typeof injected.sendMessage === 'function') return injected;\n } catch {\n /* no injection in this realm — fall through to the §4 global */\n }\n // §4 runtime-discovery transport (the npm-fetched SDK path).\n const t = getHostRuntime()?.transport as HostTransport | undefined;\n if (t && typeof t.sendMessage === 'function') return t;\n throw new Error('immediately.run: no host transport (neither injected nor __immediatelyRun__)');\n}\n\nexport const sendMessage = (type: string, data: Record<string, any> = {}) => {\n transport().sendMessage(type, data);\n};\n\nexport const addListener = (msgType: string, handler: (msg: any) => void, event?: any): (() => void) => {\n const onMessage = event ?? transport().onMessage;\n const disposable = onMessage((msg: any) => {\n if (msg.type === msgType) {\n handler(msg);\n }\n });\n return () => disposable.dispose();\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBA,yBAA+B;AAQxB,SAAS,YAA2B;AAEzC,MAAI;AAEF,UAAM,WAAW,QAAQ,YAAY,QAAQ,SAAS;AACtD,QAAI,YAAY,OAAO,SAAS,gBAAgB,WAAY,QAAO;AAAA,EACrE,QAAQ;AAAA,EAER;AAEA,QAAM,QAAI,mCAAe,GAAG;AAC5B,MAAI,KAAK,OAAO,EAAE,gBAAgB,WAAY,QAAO;AACrD,QAAM,IAAI,MAAM,8EAA8E;AAChG;AAEO,MAAM,cAAc,CAAC,MAAc,OAA4B,CAAC,MAAM;AAC3E,YAAU,EAAE,YAAY,MAAM,IAAI;AACpC;AAEO,MAAM,cAAc,CAAC,SAAiB,SAA6B,UAA8B;AACtG,QAAM,YAAY,SAAS,UAAU,EAAE;AACvC,QAAM,aAAa,UAAU,CAAC,QAAa;AACzC,QAAI,IAAI,SAAS,SAAS;AACxB,cAAQ,GAAG;AAAA,IACb;AAAA,EACF,CAAC;AACD,SAAO,MAAM,WAAW,QAAQ;AAClC;","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface HostTransport {
|
|
2
|
+
sendMessage(type: string, data?: Record<string, any>): void;
|
|
3
|
+
protocolRequest(protocolName: string, method: string, params: Array<any>): Promise<any>;
|
|
4
|
+
onMessage(handler: (msg: any) => void): {
|
|
5
|
+
dispose(): void;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
declare function transport(): HostTransport;
|
|
9
|
+
declare const sendMessage: (type: string, data?: Record<string, any>) => void;
|
|
10
|
+
declare const addListener: (msgType: string, handler: (msg: any) => void, event?: any) => (() => void);
|
|
11
|
+
|
|
12
|
+
export { type HostTransport, addListener, sendMessage, transport };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface HostTransport {
|
|
2
|
+
sendMessage(type: string, data?: Record<string, any>): void;
|
|
3
|
+
protocolRequest(protocolName: string, method: string, params: Array<any>): Promise<any>;
|
|
4
|
+
onMessage(handler: (msg: any) => void): {
|
|
5
|
+
dispose(): void;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
declare function transport(): HostTransport;
|
|
9
|
+
declare const sendMessage: (type: string, data?: Record<string, any>) => void;
|
|
10
|
+
declare const addListener: (msgType: string, handler: (msg: any) => void, event?: any) => (() => void);
|
|
11
|
+
|
|
12
|
+
export { type HostTransport, addListener, sendMessage, transport };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "./chunk-VHAA22YE.js";
|
|
2
|
+
import { getHostRuntime } from "./hostRuntime";
|
|
3
|
+
function transport() {
|
|
4
|
+
try {
|
|
5
|
+
const injected = module?.evaluation?.module?.bundler?.messageBus;
|
|
6
|
+
if (injected && typeof injected.sendMessage === "function") return injected;
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
const t = getHostRuntime()?.transport;
|
|
10
|
+
if (t && typeof t.sendMessage === "function") return t;
|
|
11
|
+
throw new Error("immediately.run: no host transport (neither injected nor __immediatelyRun__)");
|
|
12
|
+
}
|
|
13
|
+
const sendMessage = (type, data = {}) => {
|
|
14
|
+
transport().sendMessage(type, data);
|
|
15
|
+
};
|
|
16
|
+
const addListener = (msgType, handler, event) => {
|
|
17
|
+
const onMessage = event ?? transport().onMessage;
|
|
18
|
+
const disposable = onMessage((msg) => {
|
|
19
|
+
if (msg.type === msgType) {
|
|
20
|
+
handler(msg);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return () => disposable.dispose();
|
|
24
|
+
};
|
|
25
|
+
export {
|
|
26
|
+
addListener,
|
|
27
|
+
sendMessage,
|
|
28
|
+
transport
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=hostTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hostTransport.ts"],"sourcesContent":["// The host transport resolver (SDK_PACKAGING_SPEC §4, expose-transport) — the ONE place\n// that answers \"how do I reach the host from here\".\n//\n// All host comms (sendMessage / protocolRequest / onMessage) go through this resolver so\n// the SDK is transport-agnostic: it works whether it was INJECTED into the bundler's\n// evaluation context (the current path — `module.evaluation.module.bundler.messageBus`) OR\n// fetched from npm as an ordinary dependency, in which case the sandbox runtime hands it\n// the transport via the §4 discovery global (`globalThis.__immediatelyRun__`).\n//\n// Dual-mode (§8): injection wins while it's active, so existing behaviour is byte-for-byte\n// preserved; the global is the fallback the npm-fetched SDK uses. When injection is removed\n// (phase 3), only the global path remains — `bundler.*` stops being API, which is the whole\n// point.\n//\n// WHY THIS IS ITS OWN MODULE (R3-307). It was the bottom half of `sandboxUtils.ts` until\n// the deadline machinery grew a dependency on the host-attention push channel. A push\n// channel needs the transport, and `withDeadline` needs the channel, so leaving both in one\n// module made `sandboxUtils → hostAttention → pushChannel → sandboxUtils` — a cycle, and\n// this package's import graph is CI-enforced acyclic (`check:circular`; a circular SDK\n// import once made the bundler infinite-loop). Splitting the *primitives* out puts\n// `sandboxUtils` at the TOP of the graph instead of in the middle of it. `sandboxUtils`\n// re-exports `sendMessage`/`addListener`, so every existing import site is unchanged and\n// the api-snapshot entry for that module keeps all four of its names.\nimport { getHostRuntime } from './hostRuntime';\n\nexport interface HostTransport {\n sendMessage(type: string, data?: Record<string, any>): void;\n protocolRequest(protocolName: string, method: string, params: Array<any>): Promise<any>;\n onMessage(handler: (msg: any) => void): { dispose(): void };\n}\n\nexport function transport(): HostTransport {\n // Injected bundler messageBus first — the current path, unchanged.\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n const injected = module?.evaluation?.module?.bundler?.messageBus;\n if (injected && typeof injected.sendMessage === 'function') return injected;\n } catch {\n /* no injection in this realm — fall through to the §4 global */\n }\n // §4 runtime-discovery transport (the npm-fetched SDK path).\n const t = getHostRuntime()?.transport as HostTransport | undefined;\n if (t && typeof t.sendMessage === 'function') return t;\n throw new Error('immediately.run: no host transport (neither injected nor __immediatelyRun__)');\n}\n\nexport const sendMessage = (type: string, data: Record<string, any> = {}) => {\n transport().sendMessage(type, data);\n};\n\nexport const addListener = (msgType: string, handler: (msg: any) => void, event?: any): (() => void) => {\n const onMessage = event ?? transport().onMessage;\n const disposable = onMessage((msg: any) => {\n if (msg.type === msgType) {\n handler(msg);\n }\n });\n return () => disposable.dispose();\n};\n"],"mappings":";AAuBA,SAAS,sBAAsB;AAQxB,SAAS,YAA2B;AAEzC,MAAI;AAEF,UAAM,WAAW,QAAQ,YAAY,QAAQ,SAAS;AACtD,QAAI,YAAY,OAAO,SAAS,gBAAgB,WAAY,QAAO;AAAA,EACrE,QAAQ;AAAA,EAER;AAEA,QAAM,IAAI,eAAe,GAAG;AAC5B,MAAI,KAAK,OAAO,EAAE,gBAAgB,WAAY,QAAO;AACrD,QAAM,IAAI,MAAM,8EAA8E;AAChG;AAEO,MAAM,cAAc,CAAC,MAAc,OAA4B,CAAC,MAAM;AAC3E,YAAU,EAAE,YAAY,MAAM,IAAI;AACpC;AAEO,MAAM,cAAc,CAAC,SAAiB,SAA6B,UAA8B;AACtG,QAAM,YAAY,SAAS,UAAU,EAAE;AACvC,QAAM,aAAa,UAAU,CAAC,QAAa;AACzC,QAAI,IAAI,SAAS,SAAS;AACxB,cAAQ,GAAG;AAAA,IACb;AAAA,EACF,CAAC;AACD,SAAO,MAAM,WAAW,QAAQ;AAClC;","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __reExport(index_exports, require("./theme"), module.exports);
|
|
|
39
39
|
__reExport(index_exports, require("./editorContext"), module.exports);
|
|
40
40
|
__reExport(index_exports, require("./editor"), module.exports);
|
|
41
41
|
__reExport(index_exports, require("./formFactor"), module.exports);
|
|
42
|
+
__reExport(index_exports, require("./hostAttention"), module.exports);
|
|
42
43
|
__reExport(index_exports, require("./region"), module.exports);
|
|
43
44
|
__reExport(index_exports, require("./mounts"), module.exports);
|
|
44
45
|
__reExport(index_exports, require("./contribute"), module.exports);
|
|
@@ -60,6 +61,7 @@ __reExport(index_exports, require("./irMarkers"), module.exports);
|
|
|
60
61
|
__reExport(index_exports, require("./ready"), module.exports);
|
|
61
62
|
__reExport(index_exports, require("./loading"), module.exports);
|
|
62
63
|
__reExport(index_exports, require("./protocolStream"), module.exports);
|
|
64
|
+
__reExport(index_exports, require("./protocolDeadline"), module.exports);
|
|
63
65
|
__reExport(index_exports, require("./sandboxTypes"), module.exports);
|
|
64
66
|
__reExport(index_exports, require("./safeContent"), module.exports);
|
|
65
67
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -81,6 +83,7 @@ __reExport(index_exports, require("./safeContent"), module.exports);
|
|
|
81
83
|
...require("./editorContext"),
|
|
82
84
|
...require("./editor"),
|
|
83
85
|
...require("./formFactor"),
|
|
86
|
+
...require("./hostAttention"),
|
|
84
87
|
...require("./region"),
|
|
85
88
|
...require("./mounts"),
|
|
86
89
|
...require("./contribute"),
|
|
@@ -102,6 +105,7 @@ __reExport(index_exports, require("./safeContent"), module.exports);
|
|
|
102
105
|
...require("./ready"),
|
|
103
106
|
...require("./loading"),
|
|
104
107
|
...require("./protocolStream"),
|
|
108
|
+
...require("./protocolDeadline"),
|
|
105
109
|
...require("./sandboxTypes"),
|
|
106
110
|
...require("./safeContent")
|
|
107
111
|
});
|