@immediately-run/sdk 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/contribute.cjs +32 -0
  2. package/dist/contribute.cjs.map +1 -0
  3. package/dist/contribute.d.cts +100 -0
  4. package/dist/contribute.d.ts +100 -0
  5. package/dist/contribute.js +8 -0
  6. package/dist/contribute.js.map +1 -0
  7. package/dist/editorContext.cjs +46 -0
  8. package/dist/editorContext.cjs.map +1 -0
  9. package/dist/editorContext.d.cts +32 -0
  10. package/dist/editorContext.d.ts +32 -0
  11. package/dist/editorContext.js +20 -0
  12. package/dist/editorContext.js.map +1 -0
  13. package/dist/formFactor.cjs +46 -0
  14. package/dist/formFactor.cjs.map +1 -0
  15. package/dist/formFactor.d.cts +29 -0
  16. package/dist/formFactor.d.ts +29 -0
  17. package/dist/formFactor.js +20 -0
  18. package/dist/formFactor.js.map +1 -0
  19. package/dist/index.cjs +10 -0
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +6 -1
  22. package/dist/index.d.ts +6 -1
  23. package/dist/index.js +5 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/mounts.cjs +42 -6
  26. package/dist/mounts.cjs.map +1 -1
  27. package/dist/mounts.d.cts +57 -3
  28. package/dist/mounts.d.ts +57 -3
  29. package/dist/mounts.js +33 -6
  30. package/dist/mounts.js.map +1 -1
  31. package/dist/protocolStream.cjs +87 -0
  32. package/dist/protocolStream.cjs.map +1 -0
  33. package/dist/protocolStream.d.cts +44 -0
  34. package/dist/protocolStream.d.ts +44 -0
  35. package/dist/protocolStream.js +61 -0
  36. package/dist/protocolStream.js.map +1 -0
  37. package/dist/theme.cjs +57 -0
  38. package/dist/theme.cjs.map +1 -0
  39. package/dist/theme.d.cts +38 -0
  40. package/dist/theme.d.ts +38 -0
  41. package/dist/theme.js +30 -0
  42. package/dist/theme.js.map +1 -0
  43. package/package.json +1 -1
package/dist/mounts.cjs CHANGED
@@ -21,11 +21,20 @@ __export(mounts_exports, {
21
21
  createSpace: () => createSpace,
22
22
  findMount: () => findMount,
23
23
  getMounts: () => getMounts,
24
+ getSpaceMembers: () => getSpaceMembers,
25
+ listAllSpaces: () => listAllSpaces,
24
26
  listSpaces: () => listSpaces,
27
+ lookupUser: () => lookupUser,
28
+ mount: () => mount,
25
29
  mountSpace: () => mountSpace,
26
30
  onMountsChange: () => onMountsChange,
27
31
  openAppSpace: () => openAppSpace,
32
+ requestMount: () => requestMount,
33
+ requestSpace: () => requestSpace,
34
+ setSpaceRole: () => setSpaceRole,
35
+ shareSpace: () => shareSpace,
28
36
  unmountSpace: () => unmountSpace,
37
+ unshareSpace: () => unshareSpace,
29
38
  useMounts: () => useMounts,
30
39
  waitForMount: () => waitForMount
31
40
  });
@@ -35,7 +44,7 @@ var import_sandboxUtils = require("./sandboxUtils");
35
44
  const mountService = () => {
36
45
  return module.evaluation.module.bundler.mounts;
37
46
  };
38
- const matches = (mount, query) => (query.type === void 0 || mount.type === query.type) && (query.id === void 0 || mount.id === query.id) && (query.path === void 0 || mount.path === query.path);
47
+ const matches = (mount2, query) => (query.type === void 0 || mount2.type === query.type) && (query.id === void 0 || mount2.id === query.id) && (query.path === void 0 || mount2.path === query.path);
39
48
  const getMounts = () => mountService().getMounts();
40
49
  const findMount = (query) => getMounts().find((m) => matches(m, query));
41
50
  const onMountsChange = (listener) => {
@@ -65,24 +74,51 @@ const request = async (method, query = {}) => {
65
74
  }
66
75
  return res.data;
67
76
  };
68
- const awaitReady = (descriptor) => waitForMount({ id: descriptor.id ?? descriptor.path });
69
- const openAppSpace = async (slot = "default") => awaitReady(await request("open", { slot }));
70
- const mountSpace = async (query) => awaitReady(await request("mount", query));
71
- const createSpace = async (opts = {}) => awaitReady(await request("create", opts));
72
- const listSpaces = async (opts = {}) => await request("list", opts);
77
+ const requestMountInternal = async (method, query) => {
78
+ const mount2 = await request(method, query);
79
+ return waitForMount({ id: mount2.id ?? mount2.path });
80
+ };
81
+ const openAppSpace = (slot = "default") => requestMountInternal("open", { slot });
82
+ const mount = (mountId) => requestMountInternal("mount", { mount: mountId });
83
+ const mountSpace = (query) => mount(`space:${query.spaceId}`);
84
+ const requestMount = () => requestMountInternal("request", {});
85
+ const requestSpace = requestMount;
86
+ const createSpace = (opts = {}) => requestMountInternal("create", opts);
87
+ const listSpaces = (opts = {}) => request("list", opts);
73
88
  const unmountSpace = async (query) => {
74
89
  await request("unmount", query);
75
90
  };
91
+ const listAllSpaces = () => request("listAll", {});
92
+ const getSpaceMembers = (spaceId) => request("members", { spaceId });
93
+ const shareSpace = async (spaceId, login, role) => {
94
+ await request("share", { spaceId, login, role });
95
+ };
96
+ const unshareSpace = async (spaceId, uid) => {
97
+ await request("unshare", { spaceId, uid });
98
+ };
99
+ const setSpaceRole = async (spaceId, uid, role) => {
100
+ await request("setRole", { spaceId, uid, role });
101
+ };
102
+ const lookupUser = (login) => request("lookupUser", { login });
76
103
  // Annotate the CommonJS export names for ESM import in node:
77
104
  0 && (module.exports = {
78
105
  createSpace,
79
106
  findMount,
80
107
  getMounts,
108
+ getSpaceMembers,
109
+ listAllSpaces,
81
110
  listSpaces,
111
+ lookupUser,
112
+ mount,
82
113
  mountSpace,
83
114
  onMountsChange,
84
115
  openAppSpace,
116
+ requestMount,
117
+ requestSpace,
118
+ setSpaceRole,
119
+ shareSpace,
85
120
  unmountSpace,
121
+ unshareSpace,
86
122
  useMounts,
87
123
  waitForMount
88
124
  });
@@ -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: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'unknown';\n}\n\ntype SpaceResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst request = async (method: string, query: Record<string, unknown> = {}): Promise<unknown> => {\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;\n};\n\n// The host announces the mount (`mount-add`) separately from the protocol reply,\n// so wait until the ZenFS port mount is actually registered before returning —\n// otherwise an immediate read could race the mount.\nconst awaitReady = (descriptor: SandboxMount): Promise<SandboxMount> =>\n waitForMount({ id: descriptor.id ?? descriptor.path });\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 = async (slot = 'default'): Promise<SandboxMount> =>\n awaitReady((await request('open', { slot })) as SandboxMount);\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). */\nexport const mountSpace = async (query: { spaceId: string }): Promise<SandboxMount> =>\n awaitReady((await request('mount', query)) as SandboxMount);\n\n/** Create a brand-new space, optionally binding it to this app (a slot). */\nexport const createSpace = async (\n opts: { name?: string; slot?: string; bindToApp?: boolean } = {}\n): Promise<SandboxMount> => awaitReady((await request('create', opts)) as SandboxMount);\n\n/** List spaces you can access — all of them, or just those bound to this app. */\nexport const listSpaces = async (opts: { app?: boolean } = {}): Promise<SpaceInfo[]> =>\n (await request('list', opts)) as SpaceInfo[];\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"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAoC;AACpC,0BAAgC;AA0BhC,MAAM,eAAe,MAAoB;AAEvC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAKA,MAAM,UAAU,CAAC,OAAqB,WACnC,MAAM,SAAS,UAAa,MAAM,SAAS,MAAM,UACjD,MAAM,OAAO,UAAa,MAAM,OAAO,MAAM,QAC7C,MAAM,SAAS,UAAa,MAAM,SAAS,MAAM;AAM7C,MAAM,YAAY,MAAsB,aAAa,EAAE,UAAU;AAGjE,MAAM,YAAY,CAAC,UACxB,UAAU,EAAE,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAMpC,MAAM,iBAAiB,CAAC,aAA6D;AAC1F,QAAM,aAAa,aAAa,EAAE,SAAS,QAAQ;AACnD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,eAAe,CAAC,UAC3B,IAAI,QAAQ,CAAC,YAAY;AACvB,QAAM,cAAc,eAAe,CAAC,WAAW;AAC7C,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAClD,QAAI,OAAO;AAET,cAAQ,QAAQ,EAAE,KAAK,WAAW;AAClC,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,CAAC;AACH,CAAC;AAGI,MAAM,YAAY,MAAsB;AAC7C,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAyB,SAAS;AAC9D,8BAAU,MAAM,eAAe,SAAS,GAAG,CAAC,CAAC;AAC7C,SAAO;AACT;AA0BA,MAAM,UAAU,OAAO,QAAgB,QAAiC,CAAC,MAAwB;AAC/F,QAAM,MAAO,UAAM,qCAAgB,UAAU,QAAQ,CAAC,KAAK,CAAC;AAC5D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAA+B;AAChD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,aAAa,CAAC,eAClB,aAAa,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,CAAC;AAQhD,MAAM,eAAe,OAAO,OAAO,cACxC,WAAY,MAAM,QAAQ,QAAQ,EAAE,KAAK,CAAC,CAAkB;AAGvD,MAAM,aAAa,OAAO,UAC/B,WAAY,MAAM,QAAQ,SAAS,KAAK,CAAkB;AAGrD,MAAM,cAAc,OACzB,OAA8D,CAAC,MACrC,WAAY,MAAM,QAAQ,UAAU,IAAI,CAAkB;AAG/E,MAAM,aAAa,OAAO,OAA0B,CAAC,MACzD,MAAM,QAAQ,QAAQ,IAAI;AAGtB,MAAM,eAAe,OAAO,UAA8C;AAC/E,QAAM,QAAQ,WAAW,KAAK;AAChC;","names":[]}
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"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAoC;AACpC,0BAAgC;AA0BhC,MAAM,eAAe,MAAoB;AAEvC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAKA,MAAM,UAAU,CAACA,QAAqB,WACnC,MAAM,SAAS,UAAaA,OAAM,SAAS,MAAM,UACjD,MAAM,OAAO,UAAaA,OAAM,OAAO,MAAM,QAC7C,MAAM,SAAS,UAAaA,OAAM,SAAS,MAAM;AAM7C,MAAM,YAAY,MAAsB,aAAa,EAAE,UAAU;AAGjE,MAAM,YAAY,CAAC,UACxB,UAAU,EAAE,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAMpC,MAAM,iBAAiB,CAAC,aAA6D;AAC1F,QAAM,aAAa,aAAa,EAAE,SAAS,QAAQ;AACnD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,eAAe,CAAC,UAC3B,IAAI,QAAQ,CAAC,YAAY;AACvB,QAAM,cAAc,eAAe,CAAC,WAAW;AAC7C,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAClD,QAAI,OAAO;AAET,cAAQ,QAAQ,EAAE,KAAK,WAAW;AAClC,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,CAAC;AACH,CAAC;AAGI,MAAM,YAAY,MAAsB;AAC7C,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAyB,SAAS;AAC9D,8BAAU,MAAM,eAAe,SAAS,GAAG,CAAC,CAAC;AAC7C,SAAO;AACT;AAkCA,MAAM,UAAU,OACd,QACA,QAAiC,CAAC,MACnB;AACf,QAAM,MAAO,UAAM,qCAAgB,UAAU,QAAQ,CAAC,KAAK,CAAC;AAC5D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAA+B;AAChD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,uBAAuB,OAC3B,QACA,UAC0B;AAC1B,QAAMA,SAAQ,MAAM,QAAsB,QAAQ,KAAK;AACvD,SAAO,aAAa,EAAE,IAAIA,OAAM,MAAMA,OAAM,KAAK,CAAC;AACpD;AAQO,MAAM,eAAe,CAAC,OAAO,cAClC,qBAAqB,QAAQ,EAAE,KAAK,CAAC;AAQhC,MAAM,QAAQ,CAAC,YACpB,qBAAqB,SAAS,EAAE,OAAO,QAAQ,CAAC;AAI3C,MAAM,aAAa,CAAC,UACzB,MAAM,SAAS,MAAM,OAAO,EAAE;AAazB,MAAM,eAAe,MAC1B,qBAAqB,WAAW,CAAC,CAAC;AAG7B,MAAM,eAAe;AAGrB,MAAM,cAAc,CACzB,OAA8D,CAAC,MACrC,qBAAqB,UAAU,IAAI;AAGxD,MAAM,aAAa,CAAC,OAA0B,CAAC,MACpD,QAAqB,QAAQ,IAAI;AAG5B,MAAM,eAAe,OAAO,UAA8C;AAC/E,QAAM,QAAQ,WAAW,KAAK;AAChC;AA8BO,MAAM,gBAAgB,MAA4B,QAAqB,WAAW,CAAC,CAAC;AAGpF,MAAM,kBAAkB,CAAC,YAC9B,QAAkB,WAAW,EAAE,QAAQ,CAAC;AAKnC,MAAM,aAAa,OAAO,SAAiB,OAAe,SAA8B;AAC7F,QAAM,QAAQ,SAAS,EAAE,SAAS,OAAO,KAAK,CAAC;AACjD;AAIO,MAAM,eAAe,OAAO,SAAiB,QAA+B;AACjF,QAAM,QAAQ,WAAW,EAAE,SAAS,IAAI,CAAC;AAC3C;AAIO,MAAM,eAAe,OAAO,SAAiB,KAAa,SAA8B;AAC7F,QAAM,QAAQ,WAAW,EAAE,SAAS,KAAK,KAAK,CAAC;AACjD;AAIO,MAAM,aAAa,CAAC,UACzB,QAAsB,cAAc,EAAE,MAAM,CAAC;","names":["mount"]}
package/dist/mounts.d.cts CHANGED
@@ -49,7 +49,7 @@ interface SpaceInfo {
49
49
  }
50
50
  /** An error from a space operation, carrying a machine-readable `code`. */
51
51
  interface SpaceError extends Error {
52
- code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'unknown';
52
+ code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'unsupported-scheme' | 'unknown';
53
53
  }
54
54
  /**
55
55
  * Open this app's workspace for the signed-in user (the zero-config path). The
@@ -58,10 +58,32 @@ interface SpaceError extends Error {
58
58
  * create-or-pick dialog. Rejects with a {@link SpaceError} (`.code`) on cancel.
59
59
  */
60
60
  declare const openAppSpace: (slot?: string) => Promise<SandboxMount>;
61
- /** Mount a specific space by id (e.g. one shared with you, or from a link). */
61
+ /**
62
+ * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —
63
+ * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:
64
+ * the host resolves the scheme. A scheme with no resolver rejects with
65
+ * {@link SpaceError} `unsupported-scheme`.
66
+ */
67
+ declare const mount: (mountId: string) => Promise<SandboxMount>;
68
+ /** Mount a specific space by id (e.g. one shared with you, or from a link). A thin
69
+ * shim over {@link mount} with the `space:` scheme. */
62
70
  declare const mountSpace: (query: {
63
71
  spaceId: string;
64
72
  }) => Promise<SandboxMount>;
73
+ /**
74
+ * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app
75
+ * asks; the HOST shows the user their mounts and the access choice (which mount,
76
+ * an optional subtree, read-only vs read-write); the USER picks or declines. The
77
+ * app never sees the list — it resolves with the single granted mount, or rejects
78
+ * with a {@link SpaceError} (`cancelled`) if declined. The granted scope is
79
+ * enforced host-side: the mount is chroot'd / `ro`-limited accordingly.
80
+ *
81
+ * Backend-general (§3.5): the picker offers whatever mounts the user has (today,
82
+ * their spaces). Returns the granted mount by its universal id.
83
+ */
84
+ declare const requestMount: () => Promise<SandboxMount>;
85
+ /** @deprecated renamed to {@link requestMount} (backend-general, §3.5). */
86
+ declare const requestSpace: () => Promise<SandboxMount>;
65
87
  /** Create a brand-new space, optionally binding it to this app (a slot). */
66
88
  declare const createSpace: (opts?: {
67
89
  name?: string;
@@ -76,5 +98,37 @@ declare const listSpaces: (opts?: {
76
98
  declare const unmountSpace: (query: {
77
99
  spaceId: string;
78
100
  }) => Promise<void>;
101
+ type Role = 'owner' | 'writer' | 'reader';
102
+ /** A member of a space (for the share/manage UI). */
103
+ interface Member {
104
+ /** `user:{uid}` | `group:{gid}`. */
105
+ principal: string;
106
+ role: Role;
107
+ login?: string;
108
+ avatarUrl?: string;
109
+ }
110
+ /** A handle resolved to a principal (handle → who). */
111
+ interface ResolvedUser {
112
+ uid: string;
113
+ login: string;
114
+ avatarUrl?: string;
115
+ }
116
+ /** Enumerate ALL the user's spaces (not just this app's) — `spaces:user`. */
117
+ declare const listAllSpaces: () => Promise<SpaceInfo[]>;
118
+ /** Read a space's members one-shot — `spaces:admin`. */
119
+ declare const getSpaceMembers: (spaceId: string) => Promise<Member[]>;
120
+ /** Invite a user (by provider handle) to a space at a role — `spaces:admin`. The
121
+ * host resolves the handle, so the app never sees other users' uids except the
122
+ * one it invited. */
123
+ declare const shareSpace: (spaceId: string, login: string, role: Role) => Promise<void>;
124
+ /** Remove a member from a space — `spaces:admin`. Refused if it would orphan the
125
+ * space (owner-lockout, T41). */
126
+ declare const unshareSpace: (spaceId: string, uid: string) => Promise<void>;
127
+ /** Change a member's role — `spaces:admin`. Refused if it would drop the sole
128
+ * owner (owner-lockout, T41). */
129
+ declare const setSpaceRole: (spaceId: string, uid: string, role: Role) => Promise<void>;
130
+ /** Resolve a provider handle to a principal (for the invite flow) — `spaces:admin`,
131
+ * rate-limited host-side. */
132
+ declare const lookupUser: (login: string) => Promise<ResolvedUser>;
79
133
 
80
- export { type MountQuery, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getMounts, listSpaces, mountSpace, onMountsChange, openAppSpace, unmountSpace, useMounts, waitForMount };
134
+ export { type Member, type MountQuery, type ResolvedUser, type Role, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getMounts, getSpaceMembers, listAllSpaces, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount };
package/dist/mounts.d.ts CHANGED
@@ -49,7 +49,7 @@ interface SpaceInfo {
49
49
  }
50
50
  /** An error from a space operation, carrying a machine-readable `code`. */
51
51
  interface SpaceError extends Error {
52
- code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'unknown';
52
+ code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'unsupported-scheme' | 'unknown';
53
53
  }
54
54
  /**
55
55
  * Open this app's workspace for the signed-in user (the zero-config path). The
@@ -58,10 +58,32 @@ interface SpaceError extends Error {
58
58
  * create-or-pick dialog. Rejects with a {@link SpaceError} (`.code`) on cancel.
59
59
  */
60
60
  declare const openAppSpace: (slot?: string) => Promise<SandboxMount>;
61
- /** Mount a specific space by id (e.g. one shared with you, or from a link). */
61
+ /**
62
+ * Mount a filesystem by its **universal mount id** (UI_AS_APPS_SPEC §3.5) —
63
+ * `scheme:locator`, e.g. `space:{spaceId}` or `github:owner/repo@ref`. Backend-blind:
64
+ * the host resolves the scheme. A scheme with no resolver rejects with
65
+ * {@link SpaceError} `unsupported-scheme`.
66
+ */
67
+ declare const mount: (mountId: string) => Promise<SandboxMount>;
68
+ /** Mount a specific space by id (e.g. one shared with you, or from a link). A thin
69
+ * shim over {@link mount} with the `space:` scheme. */
62
70
  declare const mountSpace: (query: {
63
71
  spaceId: string;
64
72
  }) => Promise<SandboxMount>;
73
+ /**
74
+ * Ask the user to grant a filesystem to this app — the §8.6 powerbox. The app
75
+ * asks; the HOST shows the user their mounts and the access choice (which mount,
76
+ * an optional subtree, read-only vs read-write); the USER picks or declines. The
77
+ * app never sees the list — it resolves with the single granted mount, or rejects
78
+ * with a {@link SpaceError} (`cancelled`) if declined. The granted scope is
79
+ * enforced host-side: the mount is chroot'd / `ro`-limited accordingly.
80
+ *
81
+ * Backend-general (§3.5): the picker offers whatever mounts the user has (today,
82
+ * their spaces). Returns the granted mount by its universal id.
83
+ */
84
+ declare const requestMount: () => Promise<SandboxMount>;
85
+ /** @deprecated renamed to {@link requestMount} (backend-general, §3.5). */
86
+ declare const requestSpace: () => Promise<SandboxMount>;
65
87
  /** Create a brand-new space, optionally binding it to this app (a slot). */
66
88
  declare const createSpace: (opts?: {
67
89
  name?: string;
@@ -76,5 +98,37 @@ declare const listSpaces: (opts?: {
76
98
  declare const unmountSpace: (query: {
77
99
  spaceId: string;
78
100
  }) => Promise<void>;
101
+ type Role = 'owner' | 'writer' | 'reader';
102
+ /** A member of a space (for the share/manage UI). */
103
+ interface Member {
104
+ /** `user:{uid}` | `group:{gid}`. */
105
+ principal: string;
106
+ role: Role;
107
+ login?: string;
108
+ avatarUrl?: string;
109
+ }
110
+ /** A handle resolved to a principal (handle → who). */
111
+ interface ResolvedUser {
112
+ uid: string;
113
+ login: string;
114
+ avatarUrl?: string;
115
+ }
116
+ /** Enumerate ALL the user's spaces (not just this app's) — `spaces:user`. */
117
+ declare const listAllSpaces: () => Promise<SpaceInfo[]>;
118
+ /** Read a space's members one-shot — `spaces:admin`. */
119
+ declare const getSpaceMembers: (spaceId: string) => Promise<Member[]>;
120
+ /** Invite a user (by provider handle) to a space at a role — `spaces:admin`. The
121
+ * host resolves the handle, so the app never sees other users' uids except the
122
+ * one it invited. */
123
+ declare const shareSpace: (spaceId: string, login: string, role: Role) => Promise<void>;
124
+ /** Remove a member from a space — `spaces:admin`. Refused if it would orphan the
125
+ * space (owner-lockout, T41). */
126
+ declare const unshareSpace: (spaceId: string, uid: string) => Promise<void>;
127
+ /** Change a member's role — `spaces:admin`. Refused if it would drop the sole
128
+ * owner (owner-lockout, T41). */
129
+ declare const setSpaceRole: (spaceId: string, uid: string, role: Role) => Promise<void>;
130
+ /** Resolve a provider handle to a principal (for the invite flow) — `spaces:admin`,
131
+ * rate-limited host-side. */
132
+ declare const lookupUser: (login: string) => Promise<ResolvedUser>;
79
133
 
80
- export { type MountQuery, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getMounts, listSpaces, mountSpace, onMountsChange, openAppSpace, unmountSpace, useMounts, waitForMount };
134
+ export { type Member, type MountQuery, type ResolvedUser, type Role, type SandboxMount, type SpaceError, type SpaceInfo, createSpace, findMount, getMounts, getSpaceMembers, listAllSpaces, listSpaces, lookupUser, mount, mountSpace, onMountsChange, openAppSpace, requestMount, requestSpace, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount };
package/dist/mounts.js CHANGED
@@ -3,7 +3,7 @@ import { protocolRequest } from "./sandboxUtils";
3
3
  const mountService = () => {
4
4
  return module.evaluation.module.bundler.mounts;
5
5
  };
6
- const matches = (mount, query) => (query.type === void 0 || mount.type === query.type) && (query.id === void 0 || mount.id === query.id) && (query.path === void 0 || mount.path === query.path);
6
+ const matches = (mount2, query) => (query.type === void 0 || mount2.type === query.type) && (query.id === void 0 || mount2.id === query.id) && (query.path === void 0 || mount2.path === query.path);
7
7
  const getMounts = () => mountService().getMounts();
8
8
  const findMount = (query) => getMounts().find((m) => matches(m, query));
9
9
  const onMountsChange = (listener) => {
@@ -33,23 +33,50 @@ const request = async (method, query = {}) => {
33
33
  }
34
34
  return res.data;
35
35
  };
36
- const awaitReady = (descriptor) => waitForMount({ id: descriptor.id ?? descriptor.path });
37
- const openAppSpace = async (slot = "default") => awaitReady(await request("open", { slot }));
38
- const mountSpace = async (query) => awaitReady(await request("mount", query));
39
- const createSpace = async (opts = {}) => awaitReady(await request("create", opts));
40
- const listSpaces = async (opts = {}) => await request("list", opts);
36
+ const requestMountInternal = async (method, query) => {
37
+ const mount2 = await request(method, query);
38
+ return waitForMount({ id: mount2.id ?? mount2.path });
39
+ };
40
+ const openAppSpace = (slot = "default") => requestMountInternal("open", { slot });
41
+ const mount = (mountId) => requestMountInternal("mount", { mount: mountId });
42
+ const mountSpace = (query) => mount(`space:${query.spaceId}`);
43
+ const requestMount = () => requestMountInternal("request", {});
44
+ const requestSpace = requestMount;
45
+ const createSpace = (opts = {}) => requestMountInternal("create", opts);
46
+ const listSpaces = (opts = {}) => request("list", opts);
41
47
  const unmountSpace = async (query) => {
42
48
  await request("unmount", query);
43
49
  };
50
+ const listAllSpaces = () => request("listAll", {});
51
+ const getSpaceMembers = (spaceId) => request("members", { spaceId });
52
+ const shareSpace = async (spaceId, login, role) => {
53
+ await request("share", { spaceId, login, role });
54
+ };
55
+ const unshareSpace = async (spaceId, uid) => {
56
+ await request("unshare", { spaceId, uid });
57
+ };
58
+ const setSpaceRole = async (spaceId, uid, role) => {
59
+ await request("setRole", { spaceId, uid, role });
60
+ };
61
+ const lookupUser = (login) => request("lookupUser", { login });
44
62
  export {
45
63
  createSpace,
46
64
  findMount,
47
65
  getMounts,
66
+ getSpaceMembers,
67
+ listAllSpaces,
48
68
  listSpaces,
69
+ lookupUser,
70
+ mount,
49
71
  mountSpace,
50
72
  onMountsChange,
51
73
  openAppSpace,
74
+ requestMount,
75
+ requestSpace,
76
+ setSpaceRole,
77
+ shareSpace,
52
78
  unmountSpace,
79
+ unshareSpace,
53
80
  useMounts,
54
81
  waitForMount
55
82
  };
@@ -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: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'unknown';\n}\n\ntype SpaceResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst request = async (method: string, query: Record<string, unknown> = {}): Promise<unknown> => {\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;\n};\n\n// The host announces the mount (`mount-add`) separately from the protocol reply,\n// so wait until the ZenFS port mount is actually registered before returning —\n// otherwise an immediate read could race the mount.\nconst awaitReady = (descriptor: SandboxMount): Promise<SandboxMount> =>\n waitForMount({ id: descriptor.id ?? descriptor.path });\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 = async (slot = 'default'): Promise<SandboxMount> =>\n awaitReady((await request('open', { slot })) as SandboxMount);\n\n/** Mount a specific space by id (e.g. one shared with you, or from a link). */\nexport const mountSpace = async (query: { spaceId: string }): Promise<SandboxMount> =>\n awaitReady((await request('mount', query)) as SandboxMount);\n\n/** Create a brand-new space, optionally binding it to this app (a slot). */\nexport const createSpace = async (\n opts: { name?: string; slot?: string; bindToApp?: boolean } = {}\n): Promise<SandboxMount> => awaitReady((await request('create', opts)) as SandboxMount);\n\n/** List spaces you can access — all of them, or just those bound to this app. */\nexport const listSpaces = async (opts: { app?: boolean } = {}): Promise<SpaceInfo[]> =>\n (await request('list', opts)) as SpaceInfo[];\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"],"mappings":"AAAA,SAAS,WAAW,gBAAgB;AACpC,SAAS,uBAAuB;AA0BhC,MAAM,eAAe,MAAoB;AAEvC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAKA,MAAM,UAAU,CAAC,OAAqB,WACnC,MAAM,SAAS,UAAa,MAAM,SAAS,MAAM,UACjD,MAAM,OAAO,UAAa,MAAM,OAAO,MAAM,QAC7C,MAAM,SAAS,UAAa,MAAM,SAAS,MAAM;AAM7C,MAAM,YAAY,MAAsB,aAAa,EAAE,UAAU;AAGjE,MAAM,YAAY,CAAC,UACxB,UAAU,EAAE,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAMpC,MAAM,iBAAiB,CAAC,aAA6D;AAC1F,QAAM,aAAa,aAAa,EAAE,SAAS,QAAQ;AACnD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,eAAe,CAAC,UAC3B,IAAI,QAAQ,CAAC,YAAY;AACvB,QAAM,cAAc,eAAe,CAAC,WAAW;AAC7C,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAClD,QAAI,OAAO;AAET,cAAQ,QAAQ,EAAE,KAAK,WAAW;AAClC,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,CAAC;AACH,CAAC;AAGI,MAAM,YAAY,MAAsB;AAC7C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyB,SAAS;AAC9D,YAAU,MAAM,eAAe,SAAS,GAAG,CAAC,CAAC;AAC7C,SAAO;AACT;AA0BA,MAAM,UAAU,OAAO,QAAgB,QAAiC,CAAC,MAAwB;AAC/F,QAAM,MAAO,MAAM,gBAAgB,UAAU,QAAQ,CAAC,KAAK,CAAC;AAC5D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAA+B;AAChD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,aAAa,CAAC,eAClB,aAAa,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,CAAC;AAQhD,MAAM,eAAe,OAAO,OAAO,cACxC,WAAY,MAAM,QAAQ,QAAQ,EAAE,KAAK,CAAC,CAAkB;AAGvD,MAAM,aAAa,OAAO,UAC/B,WAAY,MAAM,QAAQ,SAAS,KAAK,CAAkB;AAGrD,MAAM,cAAc,OACzB,OAA8D,CAAC,MACrC,WAAY,MAAM,QAAQ,UAAU,IAAI,CAAkB;AAG/E,MAAM,aAAa,OAAO,OAA0B,CAAC,MACzD,MAAM,QAAQ,QAAQ,IAAI;AAGtB,MAAM,eAAe,OAAO,UAA8C;AAC/E,QAAM,QAAQ,WAAW,KAAK;AAChC;","names":[]}
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"],"mappings":"AAAA,SAAS,WAAW,gBAAgB;AACpC,SAAS,uBAAuB;AA0BhC,MAAM,eAAe,MAAoB;AAEvC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAKA,MAAM,UAAU,CAACA,QAAqB,WACnC,MAAM,SAAS,UAAaA,OAAM,SAAS,MAAM,UACjD,MAAM,OAAO,UAAaA,OAAM,OAAO,MAAM,QAC7C,MAAM,SAAS,UAAaA,OAAM,SAAS,MAAM;AAM7C,MAAM,YAAY,MAAsB,aAAa,EAAE,UAAU;AAGjE,MAAM,YAAY,CAAC,UACxB,UAAU,EAAE,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAMpC,MAAM,iBAAiB,CAAC,aAA6D;AAC1F,QAAM,aAAa,aAAa,EAAE,SAAS,QAAQ;AACnD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,eAAe,CAAC,UAC3B,IAAI,QAAQ,CAAC,YAAY;AACvB,QAAM,cAAc,eAAe,CAAC,WAAW;AAC7C,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAClD,QAAI,OAAO;AAET,cAAQ,QAAQ,EAAE,KAAK,WAAW;AAClC,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,CAAC;AACH,CAAC;AAGI,MAAM,YAAY,MAAsB;AAC7C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyB,SAAS;AAC9D,YAAU,MAAM,eAAe,SAAS,GAAG,CAAC,CAAC;AAC7C,SAAO;AACT;AAkCA,MAAM,UAAU,OACd,QACA,QAAiC,CAAC,MACnB;AACf,QAAM,MAAO,MAAM,gBAAgB,UAAU,QAAQ,CAAC,KAAK,CAAC;AAC5D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAA+B;AAChD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,uBAAuB,OAC3B,QACA,UAC0B;AAC1B,QAAMA,SAAQ,MAAM,QAAsB,QAAQ,KAAK;AACvD,SAAO,aAAa,EAAE,IAAIA,OAAM,MAAMA,OAAM,KAAK,CAAC;AACpD;AAQO,MAAM,eAAe,CAAC,OAAO,cAClC,qBAAqB,QAAQ,EAAE,KAAK,CAAC;AAQhC,MAAM,QAAQ,CAAC,YACpB,qBAAqB,SAAS,EAAE,OAAO,QAAQ,CAAC;AAI3C,MAAM,aAAa,CAAC,UACzB,MAAM,SAAS,MAAM,OAAO,EAAE;AAazB,MAAM,eAAe,MAC1B,qBAAqB,WAAW,CAAC,CAAC;AAG7B,MAAM,eAAe;AAGrB,MAAM,cAAc,CACzB,OAA8D,CAAC,MACrC,qBAAqB,UAAU,IAAI;AAGxD,MAAM,aAAa,CAAC,OAA0B,CAAC,MACpD,QAAqB,QAAQ,IAAI;AAG5B,MAAM,eAAe,OAAO,UAA8C;AAC/E,QAAM,QAAQ,WAAW,KAAK;AAChC;AA8BO,MAAM,gBAAgB,MAA4B,QAAqB,WAAW,CAAC,CAAC;AAGpF,MAAM,kBAAkB,CAAC,YAC9B,QAAkB,WAAW,EAAE,QAAQ,CAAC;AAKnC,MAAM,aAAa,OAAO,SAAiB,OAAe,SAA8B;AAC7F,QAAM,QAAQ,SAAS,EAAE,SAAS,OAAO,KAAK,CAAC;AACjD;AAIO,MAAM,eAAe,OAAO,SAAiB,QAA+B;AACjF,QAAM,QAAQ,WAAW,EAAE,SAAS,IAAI,CAAC;AAC3C;AAIO,MAAM,eAAe,OAAO,SAAiB,KAAa,SAA8B;AAC7F,QAAM,QAAQ,WAAW,EAAE,SAAS,KAAK,KAAK,CAAC;AACjD;AAIO,MAAM,aAAa,CAAC,UACzB,QAAsB,cAAc,EAAE,MAAM,CAAC;","names":["mount"]}
@@ -0,0 +1,87 @@
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 protocolStream_exports = {};
20
+ __export(protocolStream_exports, {
21
+ StreamError: () => StreamError,
22
+ consumeStream: () => consumeStream,
23
+ protocolStream: () => protocolStream
24
+ });
25
+ module.exports = __toCommonJS(protocolStream_exports);
26
+ var import_sandboxUtils = require("./sandboxUtils");
27
+ class StreamError extends Error {
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.name = "StreamError";
31
+ this.code = code;
32
+ }
33
+ }
34
+ let streamCounter = 0;
35
+ const nextMsgId = () => {
36
+ streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;
37
+ return streamCounter;
38
+ };
39
+ async function* consumeStream(transport, type, method, params, msgId = nextMsgId()) {
40
+ const queue = [];
41
+ let wake = null;
42
+ const push = (frame) => {
43
+ queue.push(frame);
44
+ const w = wake;
45
+ wake = null;
46
+ w?.();
47
+ };
48
+ const unsubscribe = transport.subscribe(type, (msg) => {
49
+ if (msg.msgId !== msgId || !msg.stream) return;
50
+ push(msg.stream);
51
+ });
52
+ try {
53
+ transport.send({ type, method, params, msgId, stream: true });
54
+ while (true) {
55
+ if (queue.length === 0) {
56
+ await new Promise((resolve) => {
57
+ wake = resolve;
58
+ });
59
+ continue;
60
+ }
61
+ const frame = queue.shift();
62
+ if (frame.kind === "event") {
63
+ yield frame.value;
64
+ } else if (frame.kind === "done") {
65
+ return frame.value;
66
+ } else {
67
+ throw new StreamError(frame.code, frame.message);
68
+ }
69
+ }
70
+ } finally {
71
+ unsubscribe();
72
+ }
73
+ }
74
+ const bundlerTransport = {
75
+ send: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg),
76
+ subscribe: (type, handler) => (0, import_sandboxUtils.addListener)(type, (msg) => handler(msg))
77
+ };
78
+ function protocolStream(protocolName, method, params) {
79
+ return consumeStream(bundlerTransport, protocolName, method, params);
80
+ }
81
+ // Annotate the CommonJS export names for ESM import in node:
82
+ 0 && (module.exports = {
83
+ StreamError,
84
+ consumeStream,
85
+ protocolStream
86
+ });
87
+ //# sourceMappingURL=protocolStream.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\n\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n}\n\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId()\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n try {\n transport.send({ type, method, params, msgId, stream: true });\n while (true) {\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n continue;\n }\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n return frame.value as R;\n } else {\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers.\nconst bundlerTransport: 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/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[]\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAAyC;AAkBlC,MAAM,oBAAoB,MAAM;AAAA,EAErC,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,gBAAgB;AACpB,MAAM,YAAY,MAAc;AAI9B,mBAAiB,gBAAgB,KAAK,OAAO;AAC7C,SAAO;AACT;AASA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GACE;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAChC,QAAM,OAAO,CAAC,UAAuB;AACnC,UAAM,KAAK,KAAK;AAChB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AAEA,QAAM,cAAc,UAAU,UAAU,MAAM,CAAC,QAAQ;AACrD,QAAI,IAAI,UAAU,SAAS,CAAC,IAAI,OAAQ;AACxC,SAAK,IAAI,MAAM;AAAA,EACjB,CAAC;AAED,MAAI;AACF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,WAAO,MAAM;AACX,UAAI,MAAM,WAAW,GAAG;AACtB,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,iBAAO;AAAA,QACT,CAAC;AACD;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,MAAM;AAC1B,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,MAAM;AAAA,MACd,WAAW,MAAM,SAAS,QAAQ;AAChC,eAAO,MAAM;AAAA,MACf,OAAO;AACL,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AAAA,EACd;AACF;AAGA,MAAM,mBAAoC;AAAA,EACxC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AACvF;AAOO,SAAS,eACd,cACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,MAAM;AAC3E;","names":[]}
@@ -0,0 +1,44 @@
1
+ type StreamFrame = {
2
+ kind: 'event';
3
+ value: unknown;
4
+ } | {
5
+ kind: 'done';
6
+ value: unknown;
7
+ } | {
8
+ kind: 'error';
9
+ code: string;
10
+ message: string;
11
+ };
12
+ interface StreamTransport {
13
+ send: (msg: {
14
+ type: string;
15
+ method: string;
16
+ params: unknown[];
17
+ msgId: number;
18
+ stream: true;
19
+ }) => void;
20
+ subscribe: (type: string, handler: (msg: {
21
+ msgId?: number;
22
+ stream?: StreamFrame;
23
+ }) => void) => () => void;
24
+ }
25
+ declare class StreamError extends Error {
26
+ code: string;
27
+ constructor(code: string, message: string);
28
+ }
29
+ /**
30
+ * Drive one streamed request to completion over an injected transport.
31
+ *
32
+ * Yields each event value; returns the `done` value; throws `StreamError` on an
33
+ * error frame. Always unsubscribes (via the generator's `finally`) so an early
34
+ * `break` in the consumer doesn't leak the listener.
35
+ */
36
+ declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number): AsyncGenerator<T, R, void>;
37
+ /**
38
+ * Consume an elevated streaming protocol method from app code.
39
+ *
40
+ * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`
41
+ */
42
+ declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[]): AsyncGenerator<T, R, void>;
43
+
44
+ export { StreamError, type StreamFrame, type StreamTransport, consumeStream, protocolStream };
@@ -0,0 +1,44 @@
1
+ type StreamFrame = {
2
+ kind: 'event';
3
+ value: unknown;
4
+ } | {
5
+ kind: 'done';
6
+ value: unknown;
7
+ } | {
8
+ kind: 'error';
9
+ code: string;
10
+ message: string;
11
+ };
12
+ interface StreamTransport {
13
+ send: (msg: {
14
+ type: string;
15
+ method: string;
16
+ params: unknown[];
17
+ msgId: number;
18
+ stream: true;
19
+ }) => void;
20
+ subscribe: (type: string, handler: (msg: {
21
+ msgId?: number;
22
+ stream?: StreamFrame;
23
+ }) => void) => () => void;
24
+ }
25
+ declare class StreamError extends Error {
26
+ code: string;
27
+ constructor(code: string, message: string);
28
+ }
29
+ /**
30
+ * Drive one streamed request to completion over an injected transport.
31
+ *
32
+ * Yields each event value; returns the `done` value; throws `StreamError` on an
33
+ * error frame. Always unsubscribes (via the generator's `finally`) so an early
34
+ * `break` in the consumer doesn't leak the listener.
35
+ */
36
+ declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number): AsyncGenerator<T, R, void>;
37
+ /**
38
+ * Consume an elevated streaming protocol method from app code.
39
+ *
40
+ * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`
41
+ */
42
+ declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[]): AsyncGenerator<T, R, void>;
43
+
44
+ export { StreamError, type StreamFrame, type StreamTransport, consumeStream, protocolStream };