@immediately-run/sdk 0.27.0 → 0.28.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,0BAA0D;AAE1D,4BAA8B;AAC9B,yBAAkC;AAalC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,UAAM,qCAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AACvF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,aAAO,qCAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACpF;AAKA,MAAM,cAAU,sCAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n /**\n * JSON Schema for the method's single object argument, when the host declares one.\n * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can\n * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested\n * `{ files: [{ path, content }] }` array — instead of a permissive \"any object\".\n * Absent for methods the host advertises without a schema.\n */\n paramsSchema?: Record<string, unknown>;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,0BAA0D;AAE1D,4BAA8B;AAC9B,yBAAkC;AAqBlC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,UAAM,qCAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AACvF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,aAAO,qCAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACpF;AAKA,MAAM,cAAU,sCAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
@@ -6,6 +6,14 @@ interface ApiMethod {
6
6
  capability: string;
7
7
  /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */
8
8
  stream?: boolean;
9
+ /**
10
+ * JSON Schema for the method's single object argument, when the host declares one.
11
+ * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can
12
+ * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested
13
+ * `{ files: [{ path, content }] }` array — instead of a permissive "any object".
14
+ * Absent for methods the host advertises without a schema.
15
+ */
16
+ paramsSchema?: Record<string, unknown>;
9
17
  }
10
18
  /**
11
19
  * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.
package/dist/catalog.d.ts CHANGED
@@ -6,6 +6,14 @@ interface ApiMethod {
6
6
  capability: string;
7
7
  /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */
8
8
  stream?: boolean;
9
+ /**
10
+ * JSON Schema for the method's single object argument, when the host declares one.
11
+ * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can
12
+ * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested
13
+ * `{ files: [{ path, content }] }` array — instead of a permissive "any object".
14
+ * Absent for methods the host advertises without a schema.
15
+ */
16
+ paramsSchema?: Record<string, unknown>;
9
17
  }
10
18
  /**
11
19
  * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":"AAKA,SAAS,iBAAiB,aAAa,mBAAmB;AAE1D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAalC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,MAAM,gBAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,YAChB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AACvF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,SAAO,cAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACpF;AAKA,MAAM,UAAU,kBAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n /**\n * JSON Schema for the method's single object argument, when the host declares one.\n * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can\n * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested\n * `{ files: [{ path, content }] }` array — instead of a permissive \"any object\".\n * Absent for methods the host advertises without a schema.\n */\n paramsSchema?: Record<string, unknown>;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":"AAKA,SAAS,iBAAiB,aAAa,mBAAmB;AAE1D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAqBlC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,MAAM,gBAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,YAChB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AACvF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,SAAO,cAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACpF;AAKA,MAAM,UAAU,kBAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
package/dist/fs.cjs CHANGED
@@ -26,6 +26,7 @@ __export(fs_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(fs_exports);
28
28
  var import_mounts = require("./mounts");
29
+ var import_onFsChange = require("./onFsChange");
29
30
  const hasFs = (fs) => typeof fs?.promises?.readFile === "function" || typeof fs?.readFile === "function";
30
31
  function sandboxFs() {
31
32
  try {
@@ -226,6 +227,16 @@ function openFs(mount) {
226
227
  },
227
228
  canWrite(relPath = "") {
228
229
  return writableAt(mount, relPath);
230
+ },
231
+ onChange(cb) {
232
+ if (root !== (0, import_mounts.getAppMountPath)()) {
233
+ return () => {
234
+ };
235
+ }
236
+ return (0, import_onFsChange.onFsChange)((change) => {
237
+ if (change.paths.length === 0) return;
238
+ cb(change.paths.map((p) => p.replace(/^\/+/, "")));
239
+ });
229
240
  }
230
241
  };
231
242
  return api;
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';\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}\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 };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\nimport { onFsChange } from './onFsChange';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\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":"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","names":[]}
package/dist/fs.d.cts CHANGED
@@ -106,6 +106,18 @@ interface MountFs {
106
106
  * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade
107
107
  * flips it. EROFS from the host stays authoritative. */
108
108
  canWrite(relPath?: string): boolean;
109
+ /** Subscribe to changes to files in this mount — the mount-scoped projection of
110
+ * the host working-tree change stream (`onFsChange`), so a viewer re-reads an
111
+ * affected file instead of polling (SDK_FS_SURFACE_SPEC §5). The callback gets
112
+ * the changed paths RELATIVE to this mount (feed them straight back into
113
+ * `readFile`/`stat`/…). Returns an unsubscribe fn.
114
+ *
115
+ * **Working-tree-only in v1 (an honest gap, O2):** the host push channel carries
116
+ * only working-tree changes, so `onChange` on a NON-working-tree mount (a space)
117
+ * is an inert subscription that never fires until that channel lands. Like
118
+ * `onFsChange`, origin-exclusion (ignoring the echo of your own write) is the
119
+ * caller's responsibility. */
120
+ onChange(cb: (changedRelPaths: string[]) => void): () => void;
109
121
  }
110
122
  /**
111
123
  * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:
package/dist/fs.d.ts CHANGED
@@ -106,6 +106,18 @@ interface MountFs {
106
106
  * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade
107
107
  * flips it. EROFS from the host stays authoritative. */
108
108
  canWrite(relPath?: string): boolean;
109
+ /** Subscribe to changes to files in this mount — the mount-scoped projection of
110
+ * the host working-tree change stream (`onFsChange`), so a viewer re-reads an
111
+ * affected file instead of polling (SDK_FS_SURFACE_SPEC §5). The callback gets
112
+ * the changed paths RELATIVE to this mount (feed them straight back into
113
+ * `readFile`/`stat`/…). Returns an unsubscribe fn.
114
+ *
115
+ * **Working-tree-only in v1 (an honest gap, O2):** the host push channel carries
116
+ * only working-tree changes, so `onChange` on a NON-working-tree mount (a space)
117
+ * is an inert subscription that never fires until that channel lands. Like
118
+ * `onFsChange`, origin-exclusion (ignoring the echo of your own write) is the
119
+ * caller's responsibility. */
120
+ onChange(cb: (changedRelPaths: string[]) => void): () => void;
109
121
  }
110
122
  /**
111
123
  * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:
package/dist/fs.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getAppMountPath } from "./mounts";
2
+ import { onFsChange } from "./onFsChange";
2
3
  const hasFs = (fs) => typeof fs?.promises?.readFile === "function" || typeof fs?.readFile === "function";
3
4
  function sandboxFs() {
4
5
  try {
@@ -199,6 +200,16 @@ function openFs(mount) {
199
200
  },
200
201
  canWrite(relPath = "") {
201
202
  return writableAt(mount, relPath);
203
+ },
204
+ onChange(cb) {
205
+ if (root !== getAppMountPath()) {
206
+ return () => {
207
+ };
208
+ }
209
+ return onFsChange((change) => {
210
+ if (change.paths.length === 0) return;
211
+ cb(change.paths.map((p) => p.replace(/^\/+/, "")));
212
+ });
202
213
  }
203
214
  };
204
215
  return api;
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';\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}\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 };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\nimport { onFsChange } from './onFsChange';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\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":[]}
package/dist/version.cjs CHANGED
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  SDK_VERSION: () => SDK_VERSION
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const SDK_VERSION = "0.27.0";
24
+ const SDK_VERSION = "0.28.0";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  SDK_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.27.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.28.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.27.0";
2
+ declare const SDK_VERSION = "0.28.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.27.0";
2
+ declare const SDK_VERSION = "0.28.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const SDK_VERSION = "0.27.0";
1
+ const SDK_VERSION = "0.28.0";
2
2
  export {
3
3
  SDK_VERSION
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.27.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.28.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sdk",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Runtime SDK for code executing inside an immediately.run sandbox.",
5
5
  "license": "MIT",
6
6
  "repository": "github:immediately-run/immediately-run-sdk",