@immediately-run/sdk 0.2.8 → 0.4.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.
Files changed (55) hide show
  1. package/dist/auth.cjs +13 -13
  2. package/dist/auth.cjs.map +1 -1
  3. package/dist/auth.js +13 -13
  4. package/dist/auth.js.map +1 -1
  5. package/dist/catalog.cjs +14 -26
  6. package/dist/catalog.cjs.map +1 -1
  7. package/dist/catalog.js +15 -27
  8. package/dist/catalog.js.map +1 -1
  9. package/dist/editorContext.cjs +10 -14
  10. package/dist/editorContext.cjs.map +1 -1
  11. package/dist/editorContext.js +10 -14
  12. package/dist/editorContext.js.map +1 -1
  13. package/dist/formFactor.cjs +18 -12
  14. package/dist/formFactor.cjs.map +1 -1
  15. package/dist/formFactor.js +18 -12
  16. package/dist/formFactor.js.map +1 -1
  17. package/dist/hostRuntime.cjs.map +1 -1
  18. package/dist/hostRuntime.d.cts +3 -0
  19. package/dist/hostRuntime.d.ts +3 -0
  20. package/dist/hostRuntime.js.map +1 -1
  21. package/dist/index.cjs +2 -0
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +2 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/mounts.cjs +4 -0
  28. package/dist/mounts.cjs.map +1 -1
  29. package/dist/mounts.d.cts +10 -1
  30. package/dist/mounts.d.ts +10 -1
  31. package/dist/mounts.js +3 -0
  32. package/dist/mounts.js.map +1 -1
  33. package/dist/netFetch.cjs +40 -0
  34. package/dist/netFetch.cjs.map +1 -0
  35. package/dist/netFetch.d.cts +28 -0
  36. package/dist/netFetch.d.ts +28 -0
  37. package/dist/netFetch.js +16 -0
  38. package/dist/netFetch.js.map +1 -0
  39. package/dist/pushChannel.cjs +70 -0
  40. package/dist/pushChannel.cjs.map +1 -0
  41. package/dist/pushChannel.d.cts +25 -0
  42. package/dist/pushChannel.d.ts +25 -0
  43. package/dist/pushChannel.js +46 -0
  44. package/dist/pushChannel.js.map +1 -0
  45. package/dist/runtime.cjs +1 -1
  46. package/dist/runtime.cjs.map +1 -1
  47. package/dist/runtime.d.cts +1 -1
  48. package/dist/runtime.d.ts +1 -1
  49. package/dist/runtime.js +1 -1
  50. package/dist/runtime.js.map +1 -1
  51. package/dist/theme.cjs +10 -14
  52. package/dist/theme.cjs.map +1 -1
  53. package/dist/theme.js +10 -14
  54. package/dist/theme.js.map +1 -1
  55. package/package.json +5 -2
package/dist/index.d.cts CHANGED
@@ -8,10 +8,11 @@ export { AuthState, AuthStatus, SandboxUser, getAuthState, onAuthChange, useAuth
8
8
  export { HostTheme, getHostTheme, onHostThemeChange, setHostTheme, useHostTheme } from './theme.cjs';
9
9
  export { EditorContext, getEditorContext, onEditorContextChange, useEditorContext } from './editorContext.cjs';
10
10
  export { FormFactor, FormFactorClass, Orientation, getFormFactor, onFormFactorChange, useFormFactor } from './formFactor.cjs';
11
- export { GrantRecord, Member, MountQuery, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, createSpace, findMount, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.cjs';
11
+ export { GrantRecord, Member, MountQuery, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, createSpace, findMount, getAppMountPath, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.cjs';
12
12
  export { ContributeMode, ContributeOptions, ContributionEvent, ContributionResult, contribute } from './contribute.cjs';
13
13
  export { ApiMethod, getCatalog, invoke, invokeStream, onCatalogChange, useCatalog } from './catalog.cjs';
14
14
  export { RegionMessage, onRegionMessage, postToRegion, useRegionMessage } from './ipc.cjs';
15
+ export { HostFetchInit, HostFetchResponse, hostFetch } from './netFetch.cjs';
15
16
  export { FileCap, TaskInput, cancelTask, capFile, completeTask, getTaskInput, invokeTask, useTaskInput } from './tasks.cjs';
16
17
  export { SDK_PROTOCOL_VERSION, SDK_VERSION, SdkHandshake, announceHandshake, sdkHandshake } from './runtime.cjs';
17
18
  export { StreamError, StreamFrame, StreamTransport, consumeStream, protocolStream } from './protocolStream.cjs';
package/dist/index.d.ts CHANGED
@@ -8,10 +8,11 @@ export { AuthState, AuthStatus, SandboxUser, getAuthState, onAuthChange, useAuth
8
8
  export { HostTheme, getHostTheme, onHostThemeChange, setHostTheme, useHostTheme } from './theme.js';
9
9
  export { EditorContext, getEditorContext, onEditorContextChange, useEditorContext } from './editorContext.js';
10
10
  export { FormFactor, FormFactorClass, Orientation, getFormFactor, onFormFactorChange, useFormFactor } from './formFactor.js';
11
- export { GrantRecord, Member, MountQuery, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, createSpace, findMount, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.js';
11
+ export { GrantRecord, Member, MountQuery, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, createSpace, findMount, getAppMountPath, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.js';
12
12
  export { ContributeMode, ContributeOptions, ContributionEvent, ContributionResult, contribute } from './contribute.js';
13
13
  export { ApiMethod, getCatalog, invoke, invokeStream, onCatalogChange, useCatalog } from './catalog.js';
14
14
  export { RegionMessage, onRegionMessage, postToRegion, useRegionMessage } from './ipc.js';
15
+ export { HostFetchInit, HostFetchResponse, hostFetch } from './netFetch.js';
15
16
  export { FileCap, TaskInput, cancelTask, capFile, completeTask, getTaskInput, invokeTask, useTaskInput } from './tasks.js';
16
17
  export { SDK_PROTOCOL_VERSION, SDK_VERSION, SdkHandshake, announceHandshake, sdkHandshake } from './runtime.js';
17
18
  export { StreamError, StreamFrame, StreamTransport, consumeStream, protocolStream } from './protocolStream.js';
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export * from "./mounts";
12
12
  export * from "./contribute";
13
13
  export * from "./catalog";
14
14
  export * from "./ipc";
15
+ export * from "./netFetch";
15
16
  export * from "./tasks";
16
17
  export * from "./runtime";
17
18
  export * from "./protocolStream";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './formFactor';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './formFactor';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './netFetch';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
package/dist/mounts.cjs CHANGED
@@ -20,6 +20,7 @@ var mounts_exports = {};
20
20
  __export(mounts_exports, {
21
21
  createSpace: () => createSpace,
22
22
  findMount: () => findMount,
23
+ getAppMountPath: () => getAppMountPath,
23
24
  getMounts: () => getMounts,
24
25
  getSpaceMembers: () => getSpaceMembers,
25
26
  listAllSpaces: () => listAllSpaces,
@@ -43,6 +44,8 @@ __export(mounts_exports, {
43
44
  module.exports = __toCommonJS(mounts_exports);
44
45
  var import_react = require("react");
45
46
  var import_sandboxUtils = require("./sandboxUtils");
47
+ var import_hostRuntime = require("./hostRuntime");
48
+ const getAppMountPath = () => (0, import_hostRuntime.getHostRuntime)()?.appMountPath ?? "/app";
46
49
  const mountService = () => {
47
50
  return module.evaluation.module.bundler.mounts;
48
51
  };
@@ -110,6 +113,7 @@ const revokeGrant = async (appKey, spaceId) => {
110
113
  0 && (module.exports = {
111
114
  createSpace,
112
115
  findMount,
116
+ getAppMountPath,
113
117
  getMounts,
114
118
  getSpaceMembers,
115
119
  listAllSpaces,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openAppSpace} / {@link mountSpace} to ask\n * the host to mount a Firestore-backed \"space\"; it appears at `/spaces/{id}`.\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[]) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler` is the sandbox bundler injected into the\n// evaluation context (same path the other SDK helpers reach for `messageBus`).\nconst mountService = (): MountService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.mounts;\n};\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. */\nexport type MountQuery = { type?: string; id?: string; path?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n (query.type === undefined || mount.type === query.type) &&\n (query.id === undefined || mount.id === query.id) &&\n (query.path === undefined || mount.path === query.path);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts, then again on every change. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n */\nexport const waitForMount = (query: MountQuery): Promise<SandboxMount> =>\n new Promise((resolve) => {\n const unsubscribe = onMountsChange((mounts) => {\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n // Defer unsubscribe so we don't dispose during the initial replay call.\n Promise.resolve().then(unsubscribe);\n resolve(found);\n }\n });\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** Summary of a space, as returned by {@link listSpaces}. */\nexport interface SpaceInfo {\n spaceId: string;\n role?: 'owner' | 'writer' | 'reader';\n owner?: string;\n name?: string;\n}\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n | 'unknown';\n}\n\ntype SpaceResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('spaces', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (\n method: string,\n query: Record<string, unknown>,\n): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Open this app's workspace for the signed-in user (the zero-config path). The\n * `slot` names which workspace (default `'default'`); pass distinct slots for\n * multiple filesystems in one app. On a missing slot the host shows a\n * create-or-pick dialog. Rejects with a {@link SpaceError} (`.code`) on cancel.\n */\nexport const openAppSpace = (slot = 'default'): Promise<SandboxMount> =>\n requestMountInternal('open', { slot });\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> =>\n requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> =>\n mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their mounts and the access choice (which mount,\n * an optional subtree, read-only vs read-write); the USER picks or declines. The\n * app never sees the list — it resolves with the single granted mount, or rejects\n * with a {@link SpaceError} (`cancelled`) if declined. The granted scope is\n * enforced host-side: the mount is chroot'd / `ro`-limited accordingly.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n/** Create a brand-new space, optionally binding it to this app (a slot). */\nexport const createSpace = (\n opts: { name?: string; slot?: string; bindToApp?: boolean } = {}\n): Promise<SandboxMount> => requestMountInternal('create', opts);\n\n/** List spaces you can access — all of them, or just those bound to this app. */\nexport const listSpaces = (opts: { app?: boolean } = {}): Promise<SpaceInfo[]> =>\n request<SpaceInfo[]>('list', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /** `user:{uid}` | `group:{gid}`. */\n principal: string;\n role: Role;\n login?: string;\n avatarUrl?: string;\n}\n\n/** A handle resolved to a principal (handle → who). */\nexport interface ResolvedUser {\n uid: string;\n login: string;\n avatarUrl?: string;\n}\n\n/** Enumerate ALL the user's spaces (not just this app's) — `spaces:user`. */\nexport const listAllSpaces = (): Promise<SpaceInfo[]> => request<SpaceInfo[]>('listAll', {});\n\n/** Read a space's members one-shot — `spaces:admin`. */\nexport const getSpaceMembers = (spaceId: string): Promise<Member[]> =>\n request<Member[]>('members', { spaceId });\n\n/** Invite a user (by provider handle) to a space at a role — `spaces:admin`. The\n * host resolves the handle, so the app never sees other users' uids except the\n * one it invited. */\nexport const shareSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('share', { spaceId, login, role });\n};\n\n/** Remove a member from a space — `spaces:admin`. Refused if it would orphan the\n * space (owner-lockout, T41). */\nexport const unshareSpace = async (spaceId: string, uid: string): Promise<void> => {\n await request('unshare', { spaceId, uid });\n};\n\n/** Change a member's role — `spaces:admin`. Refused if it would drop the sole\n * owner (owner-lockout, T41). */\nexport const setSpaceRole = async (spaceId: string, uid: string, role: Role): Promise<void> => {\n await request('setRole', { spaceId, uid, role });\n};\n\n/** Resolve a provider handle to a principal (for the invite flow) — `spaces:admin`,\n * rate-limited host-side. */\nexport const lookupUser = (login: string): Promise<ResolvedUser> =>\n request<ResolvedUser>('lookupUser', { login });\n\n/** One durable grant an app holds, for the §8.11 capability audit view. */\nexport interface GrantRecord {\n /** The app's provider-qualified identity (`provider__namespace__repository`). */\n appKey: string;\n spaceId: string;\n /** Universal mount id (§3.5). */\n mountId: string;\n subtree?: string;\n mode: 'ro' | 'rw';\n name?: string;\n}\n\n/** Enumerate every (app, mount) grant the user holds — the audit view\n * (§8.11). Elevated `spaces:admin`. */\nexport const listGrants = (): Promise<GrantRecord[]> => request<GrantRecord[]>('grants', {});\n\n/** Revoke one app's grant on a space — durable (the app can't re-mount) plus a\n * best-effort live teardown. Elevated `spaces:admin`. */\nexport const revokeGrant = async (appKey: string, spaceId: string): Promise<void> => {\n await request('revokeGrant', { appKey, spaceId });\n};\n"],"mappings":"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","names":["mount"]}
1
+ {"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\nimport { getHostRuntime } from './hostRuntime';\n\n/**\n * The absolute path where this app's own repository filesystem is mounted\n * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is\n * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`\n * address, and this returns the canonical one the host reports. Falls back to\n * `/app` when the host hasn't reported a canonical path (older host / before the\n * report arrives) — both paths are live, so either resolves the same files.\n */\nexport const getAppMountPath = (): string => getHostRuntime()?.appMountPath ?? '/app';\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openAppSpace} / {@link mountSpace} to ask\n * the host to mount a Firestore-backed \"space\"; it appears at `/spaces/{id}`.\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[]) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler` is the sandbox bundler injected into the\n// evaluation context (same path the other SDK helpers reach for `messageBus`).\nconst mountService = (): MountService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.mounts;\n};\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. */\nexport type MountQuery = { type?: string; id?: string; path?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n (query.type === undefined || mount.type === query.type) &&\n (query.id === undefined || mount.id === query.id) &&\n (query.path === undefined || mount.path === query.path);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts, then again on every change. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n */\nexport const waitForMount = (query: MountQuery): Promise<SandboxMount> =>\n new Promise((resolve) => {\n const unsubscribe = onMountsChange((mounts) => {\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n // Defer unsubscribe so we don't dispose during the initial replay call.\n Promise.resolve().then(unsubscribe);\n resolve(found);\n }\n });\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** Summary of a space, as returned by {@link listSpaces}. */\nexport interface SpaceInfo {\n spaceId: string;\n role?: 'owner' | 'writer' | 'reader';\n owner?: string;\n name?: string;\n}\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n | 'unknown';\n}\n\ntype SpaceResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('spaces', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (\n method: string,\n query: Record<string, unknown>,\n): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Open this app's workspace for the signed-in user (the zero-config path). The\n * `slot` names which workspace (default `'default'`); pass distinct slots for\n * multiple filesystems in one app. On a missing slot the host shows a\n * create-or-pick dialog. Rejects with a {@link SpaceError} (`.code`) on cancel.\n */\nexport const openAppSpace = (slot = 'default'): Promise<SandboxMount> =>\n requestMountInternal('open', { slot });\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> =>\n requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> =>\n mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their mounts and the access choice (which mount,\n * an optional subtree, read-only vs read-write); the USER picks or declines. The\n * app never sees the list — it resolves with the single granted mount, or rejects\n * with a {@link SpaceError} (`cancelled`) if declined. The granted scope is\n * enforced host-side: the mount is chroot'd / `ro`-limited accordingly.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n/** Create a brand-new space, optionally binding it to this app (a slot). */\nexport const createSpace = (\n opts: { name?: string; slot?: string; bindToApp?: boolean } = {}\n): Promise<SandboxMount> => requestMountInternal('create', opts);\n\n/** List spaces you can access — all of them, or just those bound to this app. */\nexport const listSpaces = (opts: { app?: boolean } = {}): Promise<SpaceInfo[]> =>\n request<SpaceInfo[]>('list', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /** `user:{uid}` | `group:{gid}`. */\n principal: string;\n role: Role;\n login?: string;\n avatarUrl?: string;\n}\n\n/** A handle resolved to a principal (handle → who). */\nexport interface ResolvedUser {\n uid: string;\n login: string;\n avatarUrl?: string;\n}\n\n/** Enumerate ALL the user's spaces (not just this app's) — `spaces:user`. */\nexport const listAllSpaces = (): Promise<SpaceInfo[]> => request<SpaceInfo[]>('listAll', {});\n\n/** Read a space's members one-shot — `spaces:admin`. */\nexport const getSpaceMembers = (spaceId: string): Promise<Member[]> =>\n request<Member[]>('members', { spaceId });\n\n/** Invite a user (by provider handle) to a space at a role — `spaces:admin`. The\n * host resolves the handle, so the app never sees other users' uids except the\n * one it invited. */\nexport const shareSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('share', { spaceId, login, role });\n};\n\n/** Remove a member from a space — `spaces:admin`. Refused if it would orphan the\n * space (owner-lockout, T41). */\nexport const unshareSpace = async (spaceId: string, uid: string): Promise<void> => {\n await request('unshare', { spaceId, uid });\n};\n\n/** Change a member's role — `spaces:admin`. Refused if it would drop the sole\n * owner (owner-lockout, T41). */\nexport const setSpaceRole = async (spaceId: string, uid: string, role: Role): Promise<void> => {\n await request('setRole', { spaceId, uid, role });\n};\n\n/** Resolve a provider handle to a principal (for the invite flow) — `spaces:admin`,\n * rate-limited host-side. */\nexport const lookupUser = (login: string): Promise<ResolvedUser> =>\n request<ResolvedUser>('lookupUser', { login });\n\n/** One durable grant an app holds, for the §8.11 capability audit view. */\nexport interface GrantRecord {\n /** The app's provider-qualified identity (`provider__namespace__repository`). */\n appKey: string;\n spaceId: string;\n /** Universal mount id (§3.5). */\n mountId: string;\n subtree?: string;\n mode: 'ro' | 'rw';\n name?: string;\n}\n\n/** Enumerate every (app, mount) grant the user holds — the audit view\n * (§8.11). Elevated `spaces:admin`. */\nexport const listGrants = (): Promise<GrantRecord[]> => request<GrantRecord[]>('grants', {});\n\n/** Revoke one app's grant on a space — durable (the app can't re-mount) plus a\n * best-effort live teardown. Elevated `spaces:admin`. */\nexport const revokeGrant = async (appKey: string, spaceId: string): Promise<void> => {\n await request('revokeGrant', { appKey, spaceId });\n};\n"],"mappings":"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","names":["mount"]}
package/dist/mounts.d.cts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The absolute path where this app's own repository filesystem is mounted
3
+ * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is
4
+ * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`
5
+ * address, and this returns the canonical one the host reports. Falls back to
6
+ * `/app` when the host hasn't reported a canonical path (older host / before the
7
+ * report arrives) — both paths are live, so either resolves the same files.
8
+ */
9
+ declare const getAppMountPath: () => string;
1
10
  /**
2
11
  * A filesystem mount available to the sandbox, mirrored from the host window.
3
12
  *
@@ -148,4 +157,4 @@ declare const listGrants: () => Promise<GrantRecord[]>;
148
157
  * best-effort live teardown. Elevated `spaces:admin`. */
149
158
  declare const revokeGrant: (appKey: string, spaceId: string) => Promise<void>;
150
159
 
151
- export { type GrantRecord, type Member, type MountQuery, type ResolvedUser, type Role, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount };
160
+ export { type GrantRecord, type Member, type MountQuery, type ResolvedUser, type Role, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getAppMountPath, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount };
package/dist/mounts.d.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The absolute path where this app's own repository filesystem is mounted
3
+ * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is
4
+ * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`
5
+ * address, and this returns the canonical one the host reports. Falls back to
6
+ * `/app` when the host hasn't reported a canonical path (older host / before the
7
+ * report arrives) — both paths are live, so either resolves the same files.
8
+ */
9
+ declare const getAppMountPath: () => string;
1
10
  /**
2
11
  * A filesystem mount available to the sandbox, mirrored from the host window.
3
12
  *
@@ -148,4 +157,4 @@ declare const listGrants: () => Promise<GrantRecord[]>;
148
157
  * best-effort live teardown. Elevated `spaces:admin`. */
149
158
  declare const revokeGrant: (appKey: string, spaceId: string) => Promise<void>;
150
159
 
151
- export { type GrantRecord, type Member, type MountQuery, type ResolvedUser, type Role, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount };
160
+ export { type GrantRecord, type Member, type MountQuery, type ResolvedUser, type Role, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getAppMountPath, getMounts, getSpaceMembers, listAllSpaces, listGrants, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount };
package/dist/mounts.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { protocolRequest } from "./sandboxUtils";
3
+ import { getHostRuntime } from "./hostRuntime";
4
+ const getAppMountPath = () => getHostRuntime()?.appMountPath ?? "/app";
3
5
  const mountService = () => {
4
6
  return module.evaluation.module.bundler.mounts;
5
7
  };
@@ -66,6 +68,7 @@ const revokeGrant = async (appKey, spaceId) => {
66
68
  export {
67
69
  createSpace,
68
70
  findMount,
71
+ getAppMountPath,
69
72
  getMounts,
70
73
  getSpaceMembers,
71
74
  listAllSpaces,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openAppSpace} / {@link mountSpace} to ask\n * the host to mount a Firestore-backed \"space\"; it appears at `/spaces/{id}`.\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[]) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler` is the sandbox bundler injected into the\n// evaluation context (same path the other SDK helpers reach for `messageBus`).\nconst mountService = (): MountService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.mounts;\n};\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. */\nexport type MountQuery = { type?: string; id?: string; path?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n (query.type === undefined || mount.type === query.type) &&\n (query.id === undefined || mount.id === query.id) &&\n (query.path === undefined || mount.path === query.path);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts, then again on every change. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n */\nexport const waitForMount = (query: MountQuery): Promise<SandboxMount> =>\n new Promise((resolve) => {\n const unsubscribe = onMountsChange((mounts) => {\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n // Defer unsubscribe so we don't dispose during the initial replay call.\n Promise.resolve().then(unsubscribe);\n resolve(found);\n }\n });\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** Summary of a space, as returned by {@link listSpaces}. */\nexport interface SpaceInfo {\n spaceId: string;\n role?: 'owner' | 'writer' | 'reader';\n owner?: string;\n name?: string;\n}\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n | 'unknown';\n}\n\ntype SpaceResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('spaces', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (\n method: string,\n query: Record<string, unknown>,\n): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Open this app's workspace for the signed-in user (the zero-config path). The\n * `slot` names which workspace (default `'default'`); pass distinct slots for\n * multiple filesystems in one app. On a missing slot the host shows a\n * create-or-pick dialog. Rejects with a {@link SpaceError} (`.code`) on cancel.\n */\nexport const openAppSpace = (slot = 'default'): Promise<SandboxMount> =>\n requestMountInternal('open', { slot });\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> =>\n requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> =>\n mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their mounts and the access choice (which mount,\n * an optional subtree, read-only vs read-write); the USER picks or declines. The\n * app never sees the list — it resolves with the single granted mount, or rejects\n * with a {@link SpaceError} (`cancelled`) if declined. The granted scope is\n * enforced host-side: the mount is chroot'd / `ro`-limited accordingly.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n/** Create a brand-new space, optionally binding it to this app (a slot). */\nexport const createSpace = (\n opts: { name?: string; slot?: string; bindToApp?: boolean } = {}\n): Promise<SandboxMount> => requestMountInternal('create', opts);\n\n/** List spaces you can access — all of them, or just those bound to this app. */\nexport const listSpaces = (opts: { app?: boolean } = {}): Promise<SpaceInfo[]> =>\n request<SpaceInfo[]>('list', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /** `user:{uid}` | `group:{gid}`. */\n principal: string;\n role: Role;\n login?: string;\n avatarUrl?: string;\n}\n\n/** A handle resolved to a principal (handle → who). */\nexport interface ResolvedUser {\n uid: string;\n login: string;\n avatarUrl?: string;\n}\n\n/** Enumerate ALL the user's spaces (not just this app's) — `spaces:user`. */\nexport const listAllSpaces = (): Promise<SpaceInfo[]> => request<SpaceInfo[]>('listAll', {});\n\n/** Read a space's members one-shot — `spaces:admin`. */\nexport const getSpaceMembers = (spaceId: string): Promise<Member[]> =>\n request<Member[]>('members', { spaceId });\n\n/** Invite a user (by provider handle) to a space at a role — `spaces:admin`. The\n * host resolves the handle, so the app never sees other users' uids except the\n * one it invited. */\nexport const shareSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('share', { spaceId, login, role });\n};\n\n/** Remove a member from a space — `spaces:admin`. Refused if it would orphan the\n * space (owner-lockout, T41). */\nexport const unshareSpace = async (spaceId: string, uid: string): Promise<void> => {\n await request('unshare', { spaceId, uid });\n};\n\n/** Change a member's role — `spaces:admin`. Refused if it would drop the sole\n * owner (owner-lockout, T41). */\nexport const setSpaceRole = async (spaceId: string, uid: string, role: Role): Promise<void> => {\n await request('setRole', { spaceId, uid, role });\n};\n\n/** Resolve a provider handle to a principal (for the invite flow) — `spaces:admin`,\n * rate-limited host-side. */\nexport const lookupUser = (login: string): Promise<ResolvedUser> =>\n request<ResolvedUser>('lookupUser', { login });\n\n/** One durable grant an app holds, for the §8.11 capability audit view. */\nexport interface GrantRecord {\n /** The app's provider-qualified identity (`provider__namespace__repository`). */\n appKey: string;\n spaceId: string;\n /** Universal mount id (§3.5). */\n mountId: string;\n subtree?: string;\n mode: 'ro' | 'rw';\n name?: string;\n}\n\n/** Enumerate every (app, mount) grant the user holds — the audit view\n * (§8.11). Elevated `spaces:admin`. */\nexport const listGrants = (): Promise<GrantRecord[]> => request<GrantRecord[]>('grants', {});\n\n/** Revoke one app's grant on a space — durable (the app can't re-mount) plus a\n * best-effort live teardown. Elevated `spaces:admin`. */\nexport const revokeGrant = async (appKey: string, spaceId: string): Promise<void> => {\n await request('revokeGrant', { appKey, spaceId });\n};\n"],"mappings":"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","names":["mount"]}
1
+ {"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\nimport { getHostRuntime } from './hostRuntime';\n\n/**\n * The absolute path where this app's own repository filesystem is mounted\n * (FILE_SHARING_SPEC §11.2). Prefer this over hardcoding `/app`: the repo is\n * dual-mounted at both `/app` (back-compat) and its canonical `/mnt/{hash}`\n * address, and this returns the canonical one the host reports. Falls back to\n * `/app` when the host hasn't reported a canonical path (older host / before the\n * report arrives) — both paths are live, so either resolves the same files.\n */\nexport const getAppMountPath = (): string => getHostRuntime()?.appMountPath ?? '/app';\n\n/**\n * A filesystem mount available to the sandbox, mirrored from the host window.\n *\n * Mounts appear on demand — call {@link openAppSpace} / {@link mountSpace} to ask\n * the host to mount a Firestore-backed \"space\"; it appears at `/spaces/{id}`.\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(listener: (mounts: SandboxMount[]) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler` is the sandbox bundler injected into the\n// evaluation context (same path the other SDK helpers reach for `messageBus`).\nconst mountService = (): MountService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.mounts;\n};\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. */\nexport type MountQuery = { type?: string; id?: string; path?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n (query.type === undefined || mount.type === query.type) &&\n (query.id === undefined || mount.id === query.id) &&\n (query.path === undefined || mount.path === query.path);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts, then again on every change. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (listener: (mounts: SandboxMount[]) => void): (() => void) => {\n const disposable = mountService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * Resolves once a mount matching `query` is present (immediately if it already\n * is). Handy for \"use it when it appears\" — e.g.\n * `await waitForMount({ type: 'firestore' })` before reading `/firestore`.\n */\nexport const waitForMount = (query: MountQuery): Promise<SandboxMount> =>\n new Promise((resolve) => {\n const unsubscribe = onMountsChange((mounts) => {\n const found = mounts.find((m) => matches(m, query));\n if (found) {\n // Defer unsubscribe so we don't dispose during the initial replay call.\n Promise.resolve().then(unsubscribe);\n resolve(found);\n }\n });\n });\n\n/** React hook returning the mounts currently available, re-rendering on change. */\nexport const useMounts = (): SandboxMount[] => {\n const [mounts, setMounts] = useState<SandboxMount[]>(getMounts);\n useEffect(() => onMountsChange(setMounts), []);\n return mounts;\n};\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** Summary of a space, as returned by {@link listSpaces}. */\nexport interface SpaceInfo {\n spaceId: string;\n role?: 'owner' | 'writer' | 'reader';\n owner?: string;\n name?: string;\n}\n\n/** An error from a space operation, carrying a machine-readable `code`. */\nexport interface SpaceError extends Error {\n code:\n | 'auth-required'\n | 'cancelled'\n | 'forbidden'\n | 'not-found'\n | 'unsupported-scheme'\n | 'unknown';\n}\n\ntype SpaceResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a spaces protocol request, unwrapping the host's {ok,data} envelope and\n// throwing a typed SpaceError on failure.\nconst request = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('spaces', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'space request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Request a space mount, then wait until the host actually registers it. The\n// host announces the mount (`mount-add`) separately from the protocol reply, so\n// an immediate read could otherwise race the mount.\nconst requestMountInternal = async (\n method: string,\n query: Record<string, unknown>,\n): Promise<SandboxMount> => {\n const mount = await request<SandboxMount>(method, query);\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * Open this app's workspace for the signed-in user (the zero-config path). The\n * `slot` names which workspace (default `'default'`); pass distinct slots for\n * multiple filesystems in one app. On a missing slot the host shows a\n * create-or-pick dialog. Rejects with a {@link SpaceError} (`.code`) on cancel.\n */\nexport const openAppSpace = (slot = 'default'): Promise<SandboxMount> =>\n requestMountInternal('open', { slot });\n\n/**\n * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —\n * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:\n * the host resolves the scheme. A scheme with no resolver rejects with\n * {@link SpaceError} `unsupported-scheme`.\n */\nexport const mount = (mountId: string): Promise<SandboxMount> =>\n requestMountInternal('mount', { mount: mountId });\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). A thin\n * shim over {@link mount} with the `space:` scheme. */\nexport const mountSpace = (query: { spaceId: string }): Promise<SandboxMount> =>\n mount(`space:${query.spaceId}`);\n\n/**\n * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app\n * asks; the HOST shows the user their mounts and the access choice (which mount,\n * an optional subtree, read-only vs read-write); the USER picks or declines. The\n * app never sees the list — it resolves with the single granted mount, or rejects\n * with a {@link SpaceError} (`cancelled`) if declined. The granted scope is\n * enforced host-side: the mount is chroot'd / `ro`-limited accordingly.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n/** Create a brand-new space, optionally binding it to this app (a slot). */\nexport const createSpace = (\n opts: { name?: string; slot?: string; bindToApp?: boolean } = {}\n): Promise<SandboxMount> => requestMountInternal('create', opts);\n\n/** List spaces you can access — all of them, or just those bound to this app. */\nexport const listSpaces = (opts: { app?: boolean } = {}): Promise<SpaceInfo[]> =>\n request<SpaceInfo[]>('list', opts);\n\n/** Release a mounted space (stops its listener on the host). */\nexport const unmountSpace = async (query: { spaceId: string }): Promise<void> => {\n await request('unmount', query);\n};\n\n// ---------------------------------------------------------------------------\n// Space management (the space-manager app) — UI_AS_APPS_SPEC §5.2. These are\n// ELEVATED: enumerating all the user's spaces is `spaces:user`; mutating\n// membership (share/unshare/setRole) and resolving handles is `spaces:admin`.\n// The host enforces the owner-lockout invariant (a space always keeps an owner,\n// T41) and rate-limits handle lookups (L1); the OAuth/identity token never\n// crosses to the app.\n// ---------------------------------------------------------------------------\n\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /** `user:{uid}` | `group:{gid}`. */\n principal: string;\n role: Role;\n login?: string;\n avatarUrl?: string;\n}\n\n/** A handle resolved to a principal (handle → who). */\nexport interface ResolvedUser {\n uid: string;\n login: string;\n avatarUrl?: string;\n}\n\n/** Enumerate ALL the user's spaces (not just this app's) — `spaces:user`. */\nexport const listAllSpaces = (): Promise<SpaceInfo[]> => request<SpaceInfo[]>('listAll', {});\n\n/** Read a space's members one-shot — `spaces:admin`. */\nexport const getSpaceMembers = (spaceId: string): Promise<Member[]> =>\n request<Member[]>('members', { spaceId });\n\n/** Invite a user (by provider handle) to a space at a role — `spaces:admin`. The\n * host resolves the handle, so the app never sees other users' uids except the\n * one it invited. */\nexport const shareSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('share', { spaceId, login, role });\n};\n\n/** Remove a member from a space — `spaces:admin`. Refused if it would orphan the\n * space (owner-lockout, T41). */\nexport const unshareSpace = async (spaceId: string, uid: string): Promise<void> => {\n await request('unshare', { spaceId, uid });\n};\n\n/** Change a member's role — `spaces:admin`. Refused if it would drop the sole\n * owner (owner-lockout, T41). */\nexport const setSpaceRole = async (spaceId: string, uid: string, role: Role): Promise<void> => {\n await request('setRole', { spaceId, uid, role });\n};\n\n/** Resolve a provider handle to a principal (for the invite flow) — `spaces:admin`,\n * rate-limited host-side. */\nexport const lookupUser = (login: string): Promise<ResolvedUser> =>\n request<ResolvedUser>('lookupUser', { login });\n\n/** One durable grant an app holds, for the §8.11 capability audit view. */\nexport interface GrantRecord {\n /** The app's provider-qualified identity (`provider__namespace__repository`). */\n appKey: string;\n spaceId: string;\n /** Universal mount id (§3.5). */\n mountId: string;\n subtree?: string;\n mode: 'ro' | 'rw';\n name?: string;\n}\n\n/** Enumerate every (app, mount) grant the user holds — the audit view\n * (§8.11). Elevated `spaces:admin`. */\nexport const listGrants = (): Promise<GrantRecord[]> => request<GrantRecord[]>('grants', {});\n\n/** Revoke one app's grant on a space — durable (the app can't re-mount) plus a\n * best-effort live teardown. Elevated `spaces:admin`. */\nexport const revokeGrant = async (appKey: string, spaceId: string): Promise<void> => {\n await request('revokeGrant', { appKey, spaceId });\n};\n"],"mappings":"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","names":["mount"]}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var netFetch_exports = {};
20
+ __export(netFetch_exports, {
21
+ hostFetch: () => hostFetch
22
+ });
23
+ module.exports = __toCommonJS(netFetch_exports);
24
+ var import_sandboxUtils = require("./sandboxUtils");
25
+ const hostFetch = async (url, init = {}) => {
26
+ const res = await (0, import_sandboxUtils.protocolRequest)("fetch", "fetch", [
27
+ { url, method: init.method, headers: init.headers, body: init.body }
28
+ ]);
29
+ if (!res || res.ok !== true) {
30
+ const err = new Error(res?.message ?? "hostFetch failed");
31
+ err.code = (res && "code" in res ? res.code : void 0) ?? "unknown";
32
+ throw err;
33
+ }
34
+ return res.data;
35
+ };
36
+ // Annotate the CommonJS export names for ESM import in node:
37
+ 0 && (module.exports = {
38
+ hostFetch
39
+ });
40
+ //# sourceMappingURL=netFetch.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/netFetch.ts"],"sourcesContent":["// hostFetch — the app-facing side of the §5.11 parent-fetch proxy. The app calls\n// `hostFetch(url, init)`; the HOST performs the fetch with its real origin, but\n// only after validating `url` against your manifest's\n// `requests.\"net:fetch\".hosts` ∩ the user's consented hosts, blocking SSRF\n// targets, omitting immediately.run credentials, refusing redirects, and bounding\n// the response size. No raw network handle ever crosses the boundary (§8.10) —\n// only the serialized response.\n\nimport { protocolRequest } from './sandboxUtils';\n\nexport interface HostFetchInit {\n method?: string;\n headers?: Record<string, string>;\n /** Request body for non-GET/HEAD methods (string). */\n body?: string;\n}\n\nexport interface HostFetchResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: string;\n /** True if the body hit the host's size cap and was truncated. */\n truncated: boolean;\n}\n\n/**\n * Fetch through the host's parent-fetch proxy (§5.11). Requires the `net:fetch`\n * capability with `url`'s origin in your effective allowlist (manifest ∩ the\n * user's consent) — both are arranged at load via the consent screen.\n *\n * A reachable server's reply (including a non-2xx status) RESOLVES — inspect\n * `.status`. A gate/SSRF/transport failure REJECTS with an {@link Error} carrying\n * a machine `.code`: `forbidden` (outside the allowlist), `blocked` (SSRF target),\n * `invalid` (bad url/scheme), `redirect` (the host refuses to follow redirects),\n * `too-large`, or `network`.\n */\nexport const hostFetch = async (\n url: string,\n init: HostFetchInit = {},\n): Promise<HostFetchResponse> => {\n const res = (await protocolRequest('fetch', 'fetch', [\n { url, method: init.method, headers: init.headers, body: init.body },\n ])) as\n | { ok: true; data: HostFetchResponse }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'hostFetch failed') as Error & { code?: string };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,0BAAgC;AA6BzB,MAAM,YAAY,OACvB,KACA,OAAsB,CAAC,MACQ;AAC/B,QAAM,MAAO,UAAM,qCAAgB,SAAS,SAAS;AAAA,IACnD,EAAE,KAAK,QAAQ,KAAK,QAAQ,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK;AAAA,EACrE,CAAC;AAID,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,kBAAkB;AACxD,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;","names":[]}
@@ -0,0 +1,28 @@
1
+ interface HostFetchInit {
2
+ method?: string;
3
+ headers?: Record<string, string>;
4
+ /** Request body for non-GET/HEAD methods (string). */
5
+ body?: string;
6
+ }
7
+ interface HostFetchResponse {
8
+ status: number;
9
+ statusText: string;
10
+ headers: Record<string, string>;
11
+ body: string;
12
+ /** True if the body hit the host's size cap and was truncated. */
13
+ truncated: boolean;
14
+ }
15
+ /**
16
+ * Fetch through the host's parent-fetch proxy (§5.11). Requires the `net:fetch`
17
+ * capability with `url`'s origin in your effective allowlist (manifest ∩ the
18
+ * user's consent) — both are arranged at load via the consent screen.
19
+ *
20
+ * A reachable server's reply (including a non-2xx status) RESOLVES — inspect
21
+ * `.status`. A gate/SSRF/transport failure REJECTS with an {@link Error} carrying
22
+ * a machine `.code`: `forbidden` (outside the allowlist), `blocked` (SSRF target),
23
+ * `invalid` (bad url/scheme), `redirect` (the host refuses to follow redirects),
24
+ * `too-large`, or `network`.
25
+ */
26
+ declare const hostFetch: (url: string, init?: HostFetchInit) => Promise<HostFetchResponse>;
27
+
28
+ export { type HostFetchInit, type HostFetchResponse, hostFetch };
@@ -0,0 +1,28 @@
1
+ interface HostFetchInit {
2
+ method?: string;
3
+ headers?: Record<string, string>;
4
+ /** Request body for non-GET/HEAD methods (string). */
5
+ body?: string;
6
+ }
7
+ interface HostFetchResponse {
8
+ status: number;
9
+ statusText: string;
10
+ headers: Record<string, string>;
11
+ body: string;
12
+ /** True if the body hit the host's size cap and was truncated. */
13
+ truncated: boolean;
14
+ }
15
+ /**
16
+ * Fetch through the host's parent-fetch proxy (§5.11). Requires the `net:fetch`
17
+ * capability with `url`'s origin in your effective allowlist (manifest ∩ the
18
+ * user's consent) — both are arranged at load via the consent screen.
19
+ *
20
+ * A reachable server's reply (including a non-2xx status) RESOLVES — inspect
21
+ * `.status`. A gate/SSRF/transport failure REJECTS with an {@link Error} carrying
22
+ * a machine `.code`: `forbidden` (outside the allowlist), `blocked` (SSRF target),
23
+ * `invalid` (bad url/scheme), `redirect` (the host refuses to follow redirects),
24
+ * `too-large`, or `network`.
25
+ */
26
+ declare const hostFetch: (url: string, init?: HostFetchInit) => Promise<HostFetchResponse>;
27
+
28
+ export { type HostFetchInit, type HostFetchResponse, hostFetch };
@@ -0,0 +1,16 @@
1
+ import { protocolRequest } from "./sandboxUtils";
2
+ const hostFetch = async (url, init = {}) => {
3
+ const res = await protocolRequest("fetch", "fetch", [
4
+ { url, method: init.method, headers: init.headers, body: init.body }
5
+ ]);
6
+ if (!res || res.ok !== true) {
7
+ const err = new Error(res?.message ?? "hostFetch failed");
8
+ err.code = (res && "code" in res ? res.code : void 0) ?? "unknown";
9
+ throw err;
10
+ }
11
+ return res.data;
12
+ };
13
+ export {
14
+ hostFetch
15
+ };
16
+ //# sourceMappingURL=netFetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/netFetch.ts"],"sourcesContent":["// hostFetch — the app-facing side of the §5.11 parent-fetch proxy. The app calls\n// `hostFetch(url, init)`; the HOST performs the fetch with its real origin, but\n// only after validating `url` against your manifest's\n// `requests.\"net:fetch\".hosts` ∩ the user's consented hosts, blocking SSRF\n// targets, omitting immediately.run credentials, refusing redirects, and bounding\n// the response size. No raw network handle ever crosses the boundary (§8.10) —\n// only the serialized response.\n\nimport { protocolRequest } from './sandboxUtils';\n\nexport interface HostFetchInit {\n method?: string;\n headers?: Record<string, string>;\n /** Request body for non-GET/HEAD methods (string). */\n body?: string;\n}\n\nexport interface HostFetchResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: string;\n /** True if the body hit the host's size cap and was truncated. */\n truncated: boolean;\n}\n\n/**\n * Fetch through the host's parent-fetch proxy (§5.11). Requires the `net:fetch`\n * capability with `url`'s origin in your effective allowlist (manifest ∩ the\n * user's consent) — both are arranged at load via the consent screen.\n *\n * A reachable server's reply (including a non-2xx status) RESOLVES — inspect\n * `.status`. A gate/SSRF/transport failure REJECTS with an {@link Error} carrying\n * a machine `.code`: `forbidden` (outside the allowlist), `blocked` (SSRF target),\n * `invalid` (bad url/scheme), `redirect` (the host refuses to follow redirects),\n * `too-large`, or `network`.\n */\nexport const hostFetch = async (\n url: string,\n init: HostFetchInit = {},\n): Promise<HostFetchResponse> => {\n const res = (await protocolRequest('fetch', 'fetch', [\n { url, method: init.method, headers: init.headers, body: init.body },\n ])) as\n | { ok: true; data: HostFetchResponse }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'hostFetch failed') as Error & { code?: string };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n"],"mappings":"AAQA,SAAS,uBAAuB;AA6BzB,MAAM,YAAY,OACvB,KACA,OAAsB,CAAC,MACQ;AAC/B,QAAM,MAAO,MAAM,gBAAgB,SAAS,SAAS;AAAA,IACnD,EAAE,KAAK,QAAQ,KAAK,QAAQ,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK;AAAA,EACrE,CAAC;AAID,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,kBAAkB;AACxD,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;","names":[]}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var pushChannel_exports = {};
20
+ __export(pushChannel_exports, {
21
+ createPushChannel: () => createPushChannel
22
+ });
23
+ module.exports = __toCommonJS(pushChannel_exports);
24
+ var import_react = require("react");
25
+ var import_sandboxUtils = require("./sandboxUtils");
26
+ function createPushChannel(opts, transport = { sendMessage: import_sandboxUtils.sendMessage, addListener: import_sandboxUtils.addListener }) {
27
+ let current = opts.initial;
28
+ const listeners = /* @__PURE__ */ new Set();
29
+ let started = false;
30
+ const start = () => {
31
+ if (started) return;
32
+ started = true;
33
+ transport.addListener(opts.pushType, (msg) => {
34
+ const next = opts.parse(msg);
35
+ if (next !== void 0) {
36
+ current = next;
37
+ listeners.forEach((l) => l(current));
38
+ }
39
+ });
40
+ if (opts.requestType) {
41
+ try {
42
+ transport.sendMessage(opts.requestType);
43
+ } catch {
44
+ }
45
+ }
46
+ };
47
+ const get = () => {
48
+ start();
49
+ return current;
50
+ };
51
+ const onChange = (listener) => {
52
+ start();
53
+ listeners.add(listener);
54
+ listener(current);
55
+ return () => {
56
+ listeners.delete(listener);
57
+ };
58
+ };
59
+ const use = () => {
60
+ const [value, setValue] = (0, import_react.useState)(get);
61
+ (0, import_react.useEffect)(() => onChange(setValue), []);
62
+ return value;
63
+ };
64
+ return { get, onChange, use };
65
+ }
66
+ // Annotate the CommonJS export names for ESM import in node:
67
+ 0 && (module.exports = {
68
+ createPushChannel
69
+ });
70
+ //# sourceMappingURL=pushChannel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './sandboxUtils';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n started = true;\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,mBAAoC;AACpC,0BAA8E;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,oBAAAA,aAAa,aAAa,oBAAAC,YAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,cAAU;AACV,cAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,YAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,UAAI,SAAS,QAAW;AACtB,kBAAU;AACV,kBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,MACrC;AAAA,IACF,CAAC;AACD,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,QAAI,uBAAY,GAAG;AACzC,gCAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":["defaultSend","defaultAddListener"]}
@@ -0,0 +1,25 @@
1
+ interface PushChannel<T> {
2
+ /** Pollable snapshot of the current value. */
3
+ get(): T;
4
+ /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */
5
+ onChange(listener: (value: T) => void): () => void;
6
+ /** React hook returning the current value, re-rendering on change. */
7
+ use(): T;
8
+ }
9
+ /** Injectable transport — defaults to the real one; overridden in tests. */
10
+ interface ChannelTransport {
11
+ sendMessage: (type: string, data?: Record<string, unknown>) => void;
12
+ addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;
13
+ }
14
+ declare function createPushChannel<T>(opts: {
15
+ /** Host→sandbox push message type (e.g. `form-factor`). */
16
+ pushType: string;
17
+ /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */
18
+ requestType?: string;
19
+ /** Value assumed before the host answers — also the value when the app may not read the channel. */
20
+ initial: T;
21
+ /** Extract + validate the value from a push message; return `undefined` to ignore the message. */
22
+ parse: (msg: Record<string, unknown>) => T | undefined;
23
+ }, transport?: ChannelTransport): PushChannel<T>;
24
+
25
+ export { type ChannelTransport, type PushChannel, createPushChannel };
@@ -0,0 +1,25 @@
1
+ interface PushChannel<T> {
2
+ /** Pollable snapshot of the current value. */
3
+ get(): T;
4
+ /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */
5
+ onChange(listener: (value: T) => void): () => void;
6
+ /** React hook returning the current value, re-rendering on change. */
7
+ use(): T;
8
+ }
9
+ /** Injectable transport — defaults to the real one; overridden in tests. */
10
+ interface ChannelTransport {
11
+ sendMessage: (type: string, data?: Record<string, unknown>) => void;
12
+ addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;
13
+ }
14
+ declare function createPushChannel<T>(opts: {
15
+ /** Host→sandbox push message type (e.g. `form-factor`). */
16
+ pushType: string;
17
+ /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */
18
+ requestType?: string;
19
+ /** Value assumed before the host answers — also the value when the app may not read the channel. */
20
+ initial: T;
21
+ /** Extract + validate the value from a push message; return `undefined` to ignore the message. */
22
+ parse: (msg: Record<string, unknown>) => T | undefined;
23
+ }, transport?: ChannelTransport): PushChannel<T>;
24
+
25
+ export { type ChannelTransport, type PushChannel, createPushChannel };
@@ -0,0 +1,46 @@
1
+ import { useEffect, useState } from "react";
2
+ import { sendMessage as defaultSend, addListener as defaultAddListener } from "./sandboxUtils";
3
+ function createPushChannel(opts, transport = { sendMessage: defaultSend, addListener: defaultAddListener }) {
4
+ let current = opts.initial;
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ let started = false;
7
+ const start = () => {
8
+ if (started) return;
9
+ started = true;
10
+ transport.addListener(opts.pushType, (msg) => {
11
+ const next = opts.parse(msg);
12
+ if (next !== void 0) {
13
+ current = next;
14
+ listeners.forEach((l) => l(current));
15
+ }
16
+ });
17
+ if (opts.requestType) {
18
+ try {
19
+ transport.sendMessage(opts.requestType);
20
+ } catch {
21
+ }
22
+ }
23
+ };
24
+ const get = () => {
25
+ start();
26
+ return current;
27
+ };
28
+ const onChange = (listener) => {
29
+ start();
30
+ listeners.add(listener);
31
+ listener(current);
32
+ return () => {
33
+ listeners.delete(listener);
34
+ };
35
+ };
36
+ const use = () => {
37
+ const [value, setValue] = useState(get);
38
+ useEffect(() => onChange(setValue), []);
39
+ return value;
40
+ };
41
+ return { get, onChange, use };
42
+ }
43
+ export {
44
+ createPushChannel
45
+ };
46
+ //# sourceMappingURL=pushChannel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './sandboxUtils';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n started = true;\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":"AAWA,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe,aAAa,eAAe,0BAA0B;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,aAAa,aAAa,mBAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,cAAU;AACV,cAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,YAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,UAAI,SAAS,QAAW;AACtB,kBAAU;AACV,kBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,MACrC;AAAA,IACF,CAAC;AACD,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,GAAG;AACzC,cAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":[]}
package/dist/runtime.cjs CHANGED
@@ -28,7 +28,7 @@ module.exports = __toCommonJS(runtime_exports);
28
28
  var import_sandboxUtils = require("./sandboxUtils");
29
29
  var import_hostRuntime = require("./hostRuntime");
30
30
  const SDK_PROTOCOL_VERSION = "1.0.0";
31
- const SDK_VERSION = "0.2.8";
31
+ const SDK_VERSION = "0.4.0";
32
32
  const sdkHandshake = () => ({
33
33
  sdkVersion: SDK_VERSION,
34
34
  protocolVersion: SDK_PROTOCOL_VERSION