@immediately-run/sdk 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mounts.cjs +0 -5
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.d.cts +1 -10
- package/dist/mounts.d.ts +1 -10
- package/dist/mounts.js +0 -4
- package/dist/mounts.js.map +1 -1
- package/dist/vcs.cjs +74 -0
- package/dist/vcs.cjs.map +1 -0
- package/dist/vcs.d.cts +63 -0
- package/dist/vcs.d.ts +63 -0
- package/dist/vcs.js +45 -0
- package/dist/vcs.js.map +1 -0
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -65,6 +65,14 @@ single request:
|
|
|
65
65
|
The installed npm package also ships `.d.ts` carrying the same JSDoc, so your
|
|
66
66
|
editor/agent tooling can read the typed API inline without any network access.
|
|
67
67
|
|
|
68
|
+
## Verify (the CI/deploy gate)
|
|
69
|
+
|
|
70
|
+
`npm run verify` runs this repo's full CI gate in one command —
|
|
71
|
+
`check:circular` → `test` → `build` → `api:check` (the additive-only API-stability
|
|
72
|
+
check against the committed snapshot). Run it before pushing; it is the same set of
|
|
73
|
+
checks CI enforces, so a local green equals a green CI. (Ways of working §4: the local
|
|
74
|
+
verify gate must equal the deploy gate — one `npm run verify` per repo.)
|
|
75
|
+
|
|
68
76
|
## License
|
|
69
77
|
|
|
70
78
|
[MIT](./LICENSE)
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __reExport(index_exports, require("./netFetch"), module.exports);
|
|
|
38
38
|
__reExport(index_exports, require("./secrets"), module.exports);
|
|
39
39
|
__reExport(index_exports, require("./llm"), module.exports);
|
|
40
40
|
__reExport(index_exports, require("./diagnostics"), module.exports);
|
|
41
|
+
__reExport(index_exports, require("./vcs"), module.exports);
|
|
41
42
|
__reExport(index_exports, require("./onFsChange"), module.exports);
|
|
42
43
|
__reExport(index_exports, require("./fs"), module.exports);
|
|
43
44
|
__reExport(index_exports, require("./debug"), module.exports);
|
|
@@ -73,6 +74,7 @@ __reExport(index_exports, require("./sandboxTypes"), module.exports);
|
|
|
73
74
|
...require("./secrets"),
|
|
74
75
|
...require("./llm"),
|
|
75
76
|
...require("./diagnostics"),
|
|
77
|
+
...require("./vcs"),
|
|
76
78
|
...require("./onFsChange"),
|
|
77
79
|
...require("./fs"),
|
|
78
80
|
...require("./debug"),
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAIA,0BAAc,uCAJd;AAKA,0BAAc,oCALd;AAMA,0BAAc,gCANd;AAOA,0BAAc,oBAPd;AAQA,0BAAc,mBARd;AASA,0BAAc,oBATd;AAUA,0BAAc,4BAVd;AAWA,0BAAc,qBAXd;AAYA,0BAAc,yBAZd;AAaA,0BAAc,qBAbd;AAcA,0BAAc,qBAdd;AAeA,0BAAc,yBAfd;AAgBA,0BAAc,sBAhBd;AAiBA,0BAAc,kBAjBd;AAkBA,0BAAc,kBAlBd;AAmBA,0BAAc,uBAnBd;AAoBA,0BAAc,sBApBd;AAqBA,0BAAc,kBArBd;AAsBA,0BAAc,0BAtBd;AAuBA,0BAAc,
|
|
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 './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAIA,0BAAc,uCAJd;AAKA,0BAAc,oCALd;AAMA,0BAAc,gCANd;AAOA,0BAAc,oBAPd;AAQA,0BAAc,mBARd;AASA,0BAAc,oBATd;AAUA,0BAAc,4BAVd;AAWA,0BAAc,qBAXd;AAYA,0BAAc,yBAZd;AAaA,0BAAc,qBAbd;AAcA,0BAAc,qBAdd;AAeA,0BAAc,yBAfd;AAgBA,0BAAc,sBAhBd;AAiBA,0BAAc,kBAjBd;AAkBA,0BAAc,kBAlBd;AAmBA,0BAAc,uBAnBd;AAoBA,0BAAc,sBApBd;AAqBA,0BAAc,kBArBd;AAsBA,0BAAc,0BAtBd;AAuBA,0BAAc,kBAvBd;AAwBA,0BAAc,yBAxBd;AAyBA,0BAAc,iBAzBd;AA0BA,0BAAc,oBA1Bd;AA2BA,0BAAc,oBA3Bd;AA4BA,0BAAc,sBA5Bd;AA6BA,0BAAc,wBA7Bd;AA8BA,0BAAc,oBA9Bd;AA+BA,0BAAc,sBA/Bd;AAgCA,0BAAc,6BAhCd;AAiCA,0BAAc,2BAjCd;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -12,7 +12,7 @@ export { EditorContext, getEditorContext, onEditorContextChange, useEditorContex
|
|
|
12
12
|
export { EditTarget, EditorOpenError, EditorSessionError, EditorWriteError, RequestEditError, closeFile, createFile, createFolder, deleteEntry, openInEditor, renameEntry, requestEdit, setActiveFile, uploadFile } from './editor.cjs';
|
|
13
13
|
export { FormFactor, FormFactorClass, Orientation, getFormFactor, onFormFactorChange, useFormFactor } from './formFactor.cjs';
|
|
14
14
|
export { getRegion, useRegion } from './region.cjs';
|
|
15
|
-
export { GrantRecord, Invite, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SessionMount, SpaceError, SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole,
|
|
15
|
+
export { GrantRecord, Invite, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SessionMount, SpaceError, SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole, unmountSpace, unshareSpace, useInvites, useMounts, useSessionMounts, waitForMount } from './mounts.cjs';
|
|
16
16
|
export { ContributeMode, ContributeOptions, ContributionEvent, ContributionResult, contribute } from './contribute.cjs';
|
|
17
17
|
export { ApiMethod, getCatalog, invoke, invokeStream, onCatalogChange, useCatalog } from './catalog.cjs';
|
|
18
18
|
export { RegionMessage, onRegionMessage, postToRegion, useRegionMessage } from './ipc.cjs';
|
|
@@ -21,6 +21,7 @@ export { HostFetchInit, HostFetchResponse, HostFetchStreamEvent, HostFetchStream
|
|
|
21
21
|
export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretView, getSecrets, onSecretsChange, requestAddSecret, requestSecret, revokeSecret, useSecrets } from './secrets.cjs';
|
|
22
22
|
export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, onChatProviderChange, useChatProvider } from './llm.cjs';
|
|
23
23
|
export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.cjs';
|
|
24
|
+
export { VcsActionError, VcsBranch, VcsChange, VcsPR, VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState } from './vcs.cjs';
|
|
24
25
|
export { FsChange, getFsChange, onFsChange, useFsChange } from './onFsChange.cjs';
|
|
25
26
|
export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, mimeTypeFor, openAppFs, openFs, sandboxFs } from './fs.cjs';
|
|
26
27
|
export { DebugLevel, debug, isDebugEnabled, log, useDebugEnabled } from './debug.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { EditorContext, getEditorContext, onEditorContextChange, useEditorContex
|
|
|
12
12
|
export { EditTarget, EditorOpenError, EditorSessionError, EditorWriteError, RequestEditError, closeFile, createFile, createFolder, deleteEntry, openInEditor, renameEntry, requestEdit, setActiveFile, uploadFile } from './editor.js';
|
|
13
13
|
export { FormFactor, FormFactorClass, Orientation, getFormFactor, onFormFactorChange, useFormFactor } from './formFactor.js';
|
|
14
14
|
export { getRegion, useRegion } from './region.js';
|
|
15
|
-
export { GrantRecord, Invite, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SessionMount, SpaceError, SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole,
|
|
15
|
+
export { GrantRecord, Invite, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SessionMount, SpaceError, SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole, unmountSpace, unshareSpace, useInvites, useMounts, useSessionMounts, waitForMount } from './mounts.js';
|
|
16
16
|
export { ContributeMode, ContributeOptions, ContributionEvent, ContributionResult, contribute } from './contribute.js';
|
|
17
17
|
export { ApiMethod, getCatalog, invoke, invokeStream, onCatalogChange, useCatalog } from './catalog.js';
|
|
18
18
|
export { RegionMessage, onRegionMessage, postToRegion, useRegionMessage } from './ipc.js';
|
|
@@ -21,6 +21,7 @@ export { HostFetchInit, HostFetchResponse, HostFetchStreamEvent, HostFetchStream
|
|
|
21
21
|
export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretView, getSecrets, onSecretsChange, requestAddSecret, requestSecret, revokeSecret, useSecrets } from './secrets.js';
|
|
22
22
|
export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, onChatProviderChange, useChatProvider } from './llm.js';
|
|
23
23
|
export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.js';
|
|
24
|
+
export { VcsActionError, VcsBranch, VcsChange, VcsPR, VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState } from './vcs.js';
|
|
24
25
|
export { FsChange, getFsChange, onFsChange, useFsChange } from './onFsChange.js';
|
|
25
26
|
export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, mimeTypeFor, openAppFs, openFs, sandboxFs } from './fs.js';
|
|
26
27
|
export { DebugLevel, debug, isDebugEnabled, log, useDebugEnabled } from './debug.js';
|
package/dist/index.js
CHANGED
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 './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\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;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 './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\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;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
|
@@ -51,7 +51,6 @@ __export(mounts_exports, {
|
|
|
51
51
|
revokeGrant: () => revokeGrant,
|
|
52
52
|
revokeInvite: () => revokeInvite,
|
|
53
53
|
setSpaceRole: () => setSpaceRole,
|
|
54
|
-
shareSpace: () => shareSpace,
|
|
55
54
|
unmountSpace: () => unmountSpace,
|
|
56
55
|
unshareSpace: () => unshareSpace,
|
|
57
56
|
useInvites: () => useInvites,
|
|
@@ -234,9 +233,6 @@ const invitesChannel = (0, import_pushChannel.createPushChannel)({
|
|
|
234
233
|
const getInvites = () => invitesChannel.get();
|
|
235
234
|
const onInvitesChange = (listener) => invitesChannel.onChange(listener);
|
|
236
235
|
const useInvites = () => invitesChannel.use();
|
|
237
|
-
const shareSpace = async (spaceId, login, role) => {
|
|
238
|
-
await request("invite", { spaceId, login, role });
|
|
239
|
-
};
|
|
240
236
|
const unshareSpace = async (spaceId, uid) => {
|
|
241
237
|
await request("unshare", { spaceId, uid });
|
|
242
238
|
};
|
|
@@ -283,7 +279,6 @@ const revokeGrant = async (appKey, spaceId) => {
|
|
|
283
279
|
revokeGrant,
|
|
284
280
|
revokeInvite,
|
|
285
281
|
setSpaceRole,
|
|
286
|
-
shareSpace,
|
|
287
282
|
unmountSpace,
|
|
288
283
|
unshareSpace,
|
|
289
284
|
useInvites,
|
package/dist/mounts.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\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 openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: \"ro\" | \"rw\";\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason =\n | \"revoked\"\n | \"unshared\"\n | \"signed-out\"\n | \"unmounted\"\n | \"deleted\";\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n ): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value)\n ? (value as MountRemoveReason)\n : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener('mount-add', (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener('mount-remove', (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage('request-mounts');\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n): (() => 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// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: 'session-mounts',\n requestType: 'request-session-mounts',\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (\n listener: (mounts: SessionMount[]) => void,\n): (() => void) => sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** 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 * 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 spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): FileCap => ({ $cap: 'file', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('settings', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> =>\n settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (\n opts: { name?: string } = {}\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\n/** A collaborator's role on a shared space: full `owner`, read-write `writer`, or read-only `reader`. */\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /**\n * The **grantee** — `user:{uid}` | `group:{gid}`. This is the canonical name\n * (core_concepts §4: \"principal\" is reserved for the authority context; a space\n * member is a *grantee*). The host populates this on every member row.\n */\n grantee: string;\n /**\n * @deprecated Use {@link Member.grantee}. Kept as an alias (same value) for\n * back-compat during the `principal`→`grantee` migration; will be removed in a\n * future major. The host still populates both.\n */\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/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** 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 one\n * it invited. Pull-based (FILE_SHARING_SPEC §6.4): this writes an INVITATION, not\n * membership — the recipient must {@link acceptInvite}. Re-inviting an already-\n * invited/member user is idempotent. */\nexport const inviteToSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('invite', { spaceId, login, role });\n};\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: 'invitations',\n requestType: 'request-invitations',\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n\n/**\n * Invite a user (by provider handle) to a space at a role — `spaces:admin`.\n *\n * @deprecated Use {@link inviteToSpace}. As of FILE_SHARING_SPEC §6.4 this no longer\n * writes membership directly — it creates an invitation the recipient must accept\n * (it now routes to the same `invite` verb). Kept for back-compat; removed in a\n * future major.\n */\nexport const shareSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('invite', { 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, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\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 openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: \"ro\" | \"rw\";\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason =\n | \"revoked\"\n | \"unshared\"\n | \"signed-out\"\n | \"unmounted\"\n | \"deleted\";\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n ): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value)\n ? (value as MountRemoveReason)\n : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener('mount-add', (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener('mount-remove', (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage('request-mounts');\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n): (() => 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// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: 'session-mounts',\n requestType: 'request-session-mounts',\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (\n listener: (mounts: SessionMount[]) => void,\n): (() => void) => sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** 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 * 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 spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): FileCap => ({ $cap: 'file', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('settings', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> =>\n settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (\n opts: { name?: string } = {}\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\n/** A collaborator's role on a shared space: full `owner`, read-write `writer`, or read-only `reader`. */\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /**\n * The **grantee** — `user:{uid}` | `group:{gid}`. This is the canonical name\n * (core_concepts §4: \"principal\" is reserved for the authority context; a space\n * member is a *grantee*). The host populates this on every member row.\n */\n grantee: string;\n /**\n * @deprecated Use {@link Member.grantee}. Kept as an alias (same value) for\n * back-compat during the `principal`→`grantee` migration; will be removed in a\n * future major. The host still populates both.\n */\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/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** 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 one\n * it invited. Pull-based (FILE_SHARING_SPEC §6.4): this writes an INVITATION, not\n * membership — the recipient must {@link acceptInvite}. Re-inviting an already-\n * invited/member user is idempotent. */\nexport const inviteToSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('invite', { spaceId, login, role });\n};\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: 'invitations',\n requestType: 'request-invitations',\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n\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
|
@@ -363,15 +363,6 @@ declare const onInvitesChange: (listener: (invites: Invite[]) => void) => (() =>
|
|
|
363
363
|
/** React hook returning the caller's live invitation inbox, re-rendering on change
|
|
364
364
|
* (the space-manager Invitations inbox, §9.8). */
|
|
365
365
|
declare const useInvites: () => Invite[];
|
|
366
|
-
/**
|
|
367
|
-
* Invite a user (by provider handle) to a space at a role — `spaces:admin`.
|
|
368
|
-
*
|
|
369
|
-
* @deprecated Use {@link inviteToSpace}. As of FILE_SHARING_SPEC §6.4 this no longer
|
|
370
|
-
* writes membership directly — it creates an invitation the recipient must accept
|
|
371
|
-
* (it now routes to the same `invite` verb). Kept for back-compat; removed in a
|
|
372
|
-
* future major.
|
|
373
|
-
*/
|
|
374
|
-
declare const shareSpace: (spaceId: string, login: string, role: Role) => Promise<void>;
|
|
375
366
|
/** Remove a member from a space — `spaces:admin`. Refused if it would orphan the
|
|
376
367
|
* space (owner-lockout, T41). */
|
|
377
368
|
declare const unshareSpace: (spaceId: string, uid: string) => Promise<void>;
|
|
@@ -399,4 +390,4 @@ declare const listGrants: () => Promise<GrantRecord[]>;
|
|
|
399
390
|
* best-effort live teardown. Elevated `spaces:admin`. */
|
|
400
391
|
declare const revokeGrant: (appKey: string, spaceId: string) => Promise<void>;
|
|
401
392
|
|
|
402
|
-
export { type GrantRecord, type Invite, type Member, type MountQuery, type MountRemoveReason, type MountRule, type RemovedMount, type ResolvedUser, type Role, type SandboxMount, type SessionMount, type SpaceError, type SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole,
|
|
393
|
+
export { type GrantRecord, type Invite, type Member, type MountQuery, type MountRemoveReason, type MountRule, type RemovedMount, type ResolvedUser, type Role, type SandboxMount, type SessionMount, type SpaceError, type SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole, unmountSpace, unshareSpace, useInvites, useMounts, useSessionMounts, waitForMount };
|
package/dist/mounts.d.ts
CHANGED
|
@@ -363,15 +363,6 @@ declare const onInvitesChange: (listener: (invites: Invite[]) => void) => (() =>
|
|
|
363
363
|
/** React hook returning the caller's live invitation inbox, re-rendering on change
|
|
364
364
|
* (the space-manager Invitations inbox, §9.8). */
|
|
365
365
|
declare const useInvites: () => Invite[];
|
|
366
|
-
/**
|
|
367
|
-
* Invite a user (by provider handle) to a space at a role — `spaces:admin`.
|
|
368
|
-
*
|
|
369
|
-
* @deprecated Use {@link inviteToSpace}. As of FILE_SHARING_SPEC §6.4 this no longer
|
|
370
|
-
* writes membership directly — it creates an invitation the recipient must accept
|
|
371
|
-
* (it now routes to the same `invite` verb). Kept for back-compat; removed in a
|
|
372
|
-
* future major.
|
|
373
|
-
*/
|
|
374
|
-
declare const shareSpace: (spaceId: string, login: string, role: Role) => Promise<void>;
|
|
375
366
|
/** Remove a member from a space — `spaces:admin`. Refused if it would orphan the
|
|
376
367
|
* space (owner-lockout, T41). */
|
|
377
368
|
declare const unshareSpace: (spaceId: string, uid: string) => Promise<void>;
|
|
@@ -399,4 +390,4 @@ declare const listGrants: () => Promise<GrantRecord[]>;
|
|
|
399
390
|
* best-effort live teardown. Elevated `spaces:admin`. */
|
|
400
391
|
declare const revokeGrant: (appKey: string, spaceId: string) => Promise<void>;
|
|
401
392
|
|
|
402
|
-
export { type GrantRecord, type Invite, type Member, type MountQuery, type MountRemoveReason, type MountRule, type RemovedMount, type ResolvedUser, type Role, type SandboxMount, type SessionMount, type SpaceError, type SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole,
|
|
393
|
+
export { type GrantRecord, type Invite, type Member, type MountQuery, type MountRemoveReason, type MountRule, type RemovedMount, type ResolvedUser, type Role, type SandboxMount, type SessionMount, type SpaceError, type SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getInvites, getMounts, getSessionMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onInvitesChange, onMountsChange, onSessionMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole, unmountSpace, unshareSpace, useInvites, useMounts, useSessionMounts, waitForMount };
|
package/dist/mounts.js
CHANGED
|
@@ -172,9 +172,6 @@ const invitesChannel = createPushChannel({
|
|
|
172
172
|
const getInvites = () => invitesChannel.get();
|
|
173
173
|
const onInvitesChange = (listener) => invitesChannel.onChange(listener);
|
|
174
174
|
const useInvites = () => invitesChannel.use();
|
|
175
|
-
const shareSpace = async (spaceId, login, role) => {
|
|
176
|
-
await request("invite", { spaceId, login, role });
|
|
177
|
-
};
|
|
178
175
|
const unshareSpace = async (spaceId, uid) => {
|
|
179
176
|
await request("unshare", { spaceId, uid });
|
|
180
177
|
};
|
|
@@ -220,7 +217,6 @@ export {
|
|
|
220
217
|
revokeGrant,
|
|
221
218
|
revokeInvite,
|
|
222
219
|
setSpaceRole,
|
|
223
|
-
shareSpace,
|
|
224
220
|
unmountSpace,
|
|
225
221
|
unshareSpace,
|
|
226
222
|
useInvites,
|
package/dist/mounts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mounts.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\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 openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: \"ro\" | \"rw\";\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason =\n | \"revoked\"\n | \"unshared\"\n | \"signed-out\"\n | \"unmounted\"\n | \"deleted\";\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n ): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value)\n ? (value as MountRemoveReason)\n : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener('mount-add', (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener('mount-remove', (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage('request-mounts');\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n): (() => 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// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: 'session-mounts',\n requestType: 'request-session-mounts',\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (\n listener: (mounts: SessionMount[]) => void,\n): (() => void) => sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** 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 * 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 spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): FileCap => ({ $cap: 'file', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('settings', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> =>\n settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (\n opts: { name?: string } = {}\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\n/** A collaborator's role on a shared space: full `owner`, read-write `writer`, or read-only `reader`. */\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /**\n * The **grantee** — `user:{uid}` | `group:{gid}`. This is the canonical name\n * (core_concepts §4: \"principal\" is reserved for the authority context; a space\n * member is a *grantee*). The host populates this on every member row.\n */\n grantee: string;\n /**\n * @deprecated Use {@link Member.grantee}. Kept as an alias (same value) for\n * back-compat during the `principal`→`grantee` migration; will be removed in a\n * future major. The host still populates both.\n */\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/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** 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 one\n * it invited. Pull-based (FILE_SHARING_SPEC §6.4): this writes an INVITATION, not\n * membership — the recipient must {@link acceptInvite}. Re-inviting an already-\n * invited/member user is idempotent. */\nexport const inviteToSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('invite', { spaceId, login, role });\n};\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: 'invitations',\n requestType: 'request-invitations',\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n\n/**\n * Invite a user (by provider handle) to a space at a role — `spaces:admin`.\n *\n * @deprecated Use {@link inviteToSpace}. As of FILE_SHARING_SPEC §6.4 this no longer\n * writes membership directly — it creates an invitation the recipient must accept\n * (it now routes to the same `invite` verb). Kept for back-compat; removed in a\n * future major.\n */\nexport const shareSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('invite', { 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, sendMessage, addListener } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { getHostRuntime } from './hostRuntime';\nimport { mountMatches } from './mountMatch';\n// Type-only: `tasks.ts` registers a host listener at module load, so we reuse the\n// FileCap SHAPE without pulling that side effect into every `mounts` importer.\nimport type { FileCap } from './tasks';\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 openSettings} for this app's own settings,\n * or {@link mountSpace} / {@link requestMount} to mount a Firestore-backed \"space\".\n * Read or subscribe to the set, then access the files through the `fs` module at\n * the mount's `path`.\n */\nexport interface SandboxMount {\n /** Absolute path where the mount is reachable (e.g. `/spaces/{id}`). */\n path: string;\n /** Backend kind, e.g. `'firestore'`. */\n type: string;\n /** Optional stable identifier (the spaceId, for spaces). */\n id?: string;\n /**\n * Access mode of the granted view: `'rw'` (read-write) or `'ro'` (read-only).\n * A live role downgrade re-announces the same mount with `mode: 'ro'`; apps\n * observing `onMountsChange` see the change and writes start failing `EROFS`.\n * Absent on the primary repo mount (treated as read-write).\n */\n mode?: \"ro\" | \"rw\";\n /**\n * Human-readable label for the mount — the space's display name, or the repo\n * label for the primary working-tree mount (R3-69). Use this to show users and\n * agents *what* a mount is: the `path` (`/mnt/{hash}`) and `id` (the spaceId)\n * are opaque, and space names are not unique, so neither alone tells you which\n * filesystem you're looking at. Absent when the host can't resolve a name\n * (older host, or a name it never learned) — fall back to `id`/`path`.\n */\n name?: string;\n /**\n * The granted scopes of this mount (plan 12 §8.7 / §F): each `{subtree, mode}`\n * is a path prefix you hold and at what access, at the mount's backend-natural\n * paths. Use it to reason about per-path writability — which subtree is `rw` —\n * WITHOUT probing `EROFS`. A single whole-mount grant is `[{ subtree: '/', mode }]`.\n * Absent on the primary repo mount and on an older host that doesn't report it.\n */\n rules?: MountRule[];\n}\n\n/** One granted scope of a mount (plan 12 §F): a backend-natural path prefix and\n * the access mode there. The most specific (longest) matching rule governs a path. */\nexport interface MountRule {\n subtree: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Why a mounted filesystem was removed, surfaced on the removed descriptor so an\n * app can say *why* it vanished instead of failing mutely (auth-mount §\"mount-remove\"\n * / AM2-4):\n * - `revoked` — a durable grant was revoked (revokeGrant / consent withdrawal);\n * - `unshared` — the granting user's membership was removed (or downgraded out);\n * - `signed-out` — sign-out tore down every mount;\n * - `unmounted` — the app's own `unmountSpace` (or region teardown);\n * - `deleted` — the space was soft-deleted.\n * An older host that sends no reason is read as `'revoked'` (most conservative).\n */\nexport type MountRemoveReason =\n | \"revoked\"\n | \"unshared\"\n | \"signed-out\"\n | \"unmounted\"\n | \"deleted\";\n\n/** A descriptor delivered as REMOVED to a mounts-change listener: the mount that\n * went away, plus the `reason` it did. */\nexport interface RemovedMount extends SandboxMount {\n reason: MountRemoveReason;\n}\n\ninterface MountService {\n getMounts(): SandboxMount[];\n onChange(\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n ): { dispose(): void };\n}\n\n// The stable key of a mount: its `id` (spaceId) when present, else its `path`.\n// Matches the sandbox `MountService.mountKey` so add/replace/remove agree on both\n// sides of the wire (a role downgrade re-announces the SAME key with `mode: 'ro'`).\nconst mountKey = (m: SandboxMount): string => m.id ?? m.path;\n\nconst MOUNT_REMOVE_REASONS: ReadonlySet<string> = new Set<MountRemoveReason>([\n 'revoked',\n 'unshared',\n 'signed-out',\n 'unmounted',\n 'deleted',\n]);\n\n// Normalize an over-the-wire `mount-remove` reason; an absent/unknown value (older\n// host) reads as `'revoked'`, the most conservative reading (mirrors the sandbox).\nconst asMountRemoveReason = (value: unknown): MountRemoveReason =>\n typeof value === 'string' && MOUNT_REMOVE_REASONS.has(value)\n ? (value as MountRemoveReason)\n : 'revoked';\n\n// The injected sandbox-bundler mount service (`module.evaluation.module.bundler.mounts`),\n// or null when the SDK is npm-fetched with no injection — same dual-mode shape as\n// `sandboxUtils.transport()` and the metadata emitter (SDK_PACKAGING_SPEC §4/§8).\nconst injectedMountService = (): MountService | null => {\n try {\n // @ts-ignore - injected by the sandbox runtime\n const svc = module?.evaluation?.module?.bundler?.mounts;\n return svc && typeof svc.getMounts === 'function' ? svc : null;\n } catch {\n return null;\n }\n};\n\n// Transport-backed descriptor cache (R3-51b): the npm-fetched fallback that builds\n// the same `getMounts()`/`onChange()` view the injected `bundler.mounts` provides,\n// directly from the host's `mount-add`/`mount-remove` messages over the §4 transport.\n// The host already posts these (it's how the in-iframe bundler service is populated);\n// the `MessagePort` a `mount-add` transfers is consumed by the sandbox runtime to wire\n// ZenFS and is irrelevant here — the SDK only mirrors the *descriptors*. A lazy\n// singleton so `getMounts`/`onMountsChange` share one cache, one subscription, and one\n// `request-mounts` replay (the host re-announces every current mount, like a poll).\nlet transportSvc: MountService | null = null;\n\nconst transportMountService = (): MountService => {\n if (transportSvc) return transportSvc;\n let mounts: SandboxMount[] = [];\n const listeners = new Set<(m: SandboxMount[], r: RemovedMount[]) => void>();\n const fire = (removed: RemovedMount[]) => {\n for (const l of [...listeners]) l(mounts, removed);\n };\n\n addListener('mount-add', (msg: Record<string, any>) => {\n const mount: SandboxMount | undefined = msg.mount;\n if (!mount) return;\n const key = mountKey(mount);\n mounts = [...mounts.filter((m) => mountKey(m) !== key), mount];\n fire([]);\n });\n addListener('mount-remove', (msg: Record<string, any>) => {\n const key: string | undefined = msg.id ?? msg.path;\n if (key == null) return;\n const reason = asMountRemoveReason(msg.reason);\n const removed = mounts.filter((m) => mountKey(m) === key).map((m) => ({ ...m, reason }));\n if (removed.length === 0) return;\n mounts = mounts.filter((m) => mountKey(m) !== key);\n fire(removed);\n });\n\n // Ask the host to replay the current set (the matching `mount-add`s may have been\n // sent before this SDK subscribed). Best-effort: a transport not yet ready throws.\n try {\n sendMessage('request-mounts');\n } catch {\n /* transport not ready — the live mount-add stream still populates the cache */\n }\n\n transportSvc = {\n getMounts: () => mounts,\n onChange: (listener) => {\n listeners.add(listener);\n listener(mounts, []); // immediate replay to the new subscriber\n return { dispose: () => listeners.delete(listener) };\n },\n };\n return transportSvc;\n};\n\n// Phase-5 dual mode: prefer the injected bundler service (the live path, behaviour\n// byte-for-byte unchanged); fall back to the transport-built cache when npm-fetched.\nconst mountService = (): MountService => injectedMountService() ?? transportMountService();\n\n/** A predicate-style matcher for {@link findMount} / {@link waitForMount}. Any\n * combination of coordinates; `name` matches the human-readable mount label. */\nexport type MountQuery = { type?: string; id?: string; path?: string; name?: string };\n\nconst matches = (mount: SandboxMount, query: MountQuery): boolean =>\n mountMatches(mount, query);\n\n/**\n * Returns the mounts currently available. Poll this whenever you need a one-off\n * read; use {@link onMountsChange} or {@link useMounts} to react to changes.\n * Each descriptor carries its `id` (the spaceId), `path` (`/mnt/{hash}`) and —\n * when the host can resolve it — a human-readable `name` (R3-69), so this doubles\n * as a queryable mount→space mapping for showing or locating a mount by name.\n */\nexport const getMounts = (): SandboxMount[] => mountService().getMounts();\n\n/** Returns the first mount matching `query`, or `undefined`. */\nexport const findMount = (query: MountQuery): SandboxMount | undefined =>\n getMounts().find((m) => matches(m, query));\n\n/**\n * Subscribe to mount changes. The listener is invoked immediately with the\n * current mounts (and an empty `removed`), then again on every change. The second\n * argument carries the descriptors REMOVED by that change, each with its `reason`\n * (AM2-4) — so an app can react to *why* a mount vanished (e.g. tell the user a\n * shared space was `unshared` vs `deleted`). It is empty on adds and on the\n * initial replay. Returns an unsubscribe fn.\n */\nexport const onMountsChange = (\n listener: (mounts: SandboxMount[], removed: RemovedMount[]) => void,\n): (() => 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// Session-scope mounts — the first-party \"App | Session\" lens (PRINCIPALS §9 B2).\n// ---------------------------------------------------------------------------\n\n/** A mount as seen through the first-party **Session** lens (PRINCIPALS_SPEC §9 B2):\n * the session's mounts BEYOND this app's own (the editor/agent session's). This is\n * a metadata view — no filesystem port — so it extends {@link SandboxMount} with only\n * {@link forwardedToApp}. */\nexport interface SessionMount extends SandboxMount {\n /** True iff this mount is ALSO in the app's own {@link useMounts} (the App lens);\n * `false` for a session-export-only mount visible only to the editor/agent + the\n * Session lens. */\n forwardedToApp: boolean;\n}\n\n// The host pushes the session mount list ONLY to a FIRST-PARTY frame — the channel\n// is gated by the first-party-only `mounts:registry` capability (§8.9.1 / D-PRIN-4).\n// A URL-loaded/previewed app (or a fork of the File Explorer) never holds it, so the\n// push never arrives and `initial: []` stands — the Session lens is simply absent,\n// fail-closed. Mirrors the host's `session-mounts`/`request-session-mounts` wiring.\nconst sessionMountsChannel = createPushChannel<SessionMount[]>({\n pushType: 'session-mounts',\n requestType: 'request-session-mounts',\n initial: [],\n parse: (msg) => (Array.isArray(msg.mounts) ? (msg.mounts as SessionMount[]) : undefined),\n});\n\n/** The session's mounts (the \"Session\" lens superset), or `[]` when this frame is\n * not first-party. One-off read; use {@link onSessionMountsChange}/{@link useSessionMounts}\n * to react live. First-party only (`mounts:registry`) — a fork always sees `[]`. */\nexport const getSessionMounts = (): SessionMount[] => sessionMountsChannel.get();\n\n/** Subscribe to Session-lens mount changes. Invoked immediately with the current\n * list (`[]` for a non-first-party frame), then on every change. Returns an\n * unsubscribe. */\nexport const onSessionMountsChange = (\n listener: (mounts: SessionMount[]) => void,\n): (() => void) => sessionMountsChannel.onChange(listener);\n\n/** React hook returning the live \"Session\" lens mount list, re-rendering on change.\n * Empty for any non-first-party frame (the host withholds the channel), so a URL-\n * loaded File Explorer fork renders no Session lens. */\nexport const useSessionMounts = (): SessionMount[] => sessionMountsChannel.use();\n\n// ---------------------------------------------------------------------------\n// Spaces — on-demand, shareable Firestore-backed filesystems.\n// The host owns all UX: if you aren't signed in, or the space doesn't exist or\n// isn't accessible, the parent window presents sign-in / create / request-access\n// and only then resolves these calls. See docs/specs/FILE_SHARING_SPEC.md.\n// ---------------------------------------------------------------------------\n\n/** 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 * 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 spaces and, for the chosen one, its PROJECT\n * FOLDERS (§8.7). The user picks ONE project — so a shared space opens scoped to\n * just that project, never the whole space — and makes an EXPLICIT read-only vs\n * read-write decision (there is no default). The app never sees the list; it\n * resolves with the single granted mount, or rejects with a {@link SpaceError}\n * (`cancelled`) if declined. The granted scope is enforced host-side: the mount\n * is chroot'd to the project folder and `ro`-limited accordingly, so paths\n * outside the project are unnameable and writes on a `ro` grant fail `EROFS`.\n *\n * A project folder is the macOS-bundle-like unit an app works in inside a space;\n * the host records which app a folder belongs to (a `.immediately.run/` sidecar),\n * so the picker can surface the app's own projects or let the user create a new\n * one. Observe the granted access via {@link SandboxMount.mode}.\n *\n * Backend-general (§3.5): the picker offers whatever mounts the user has (today,\n * their spaces). Returns the granted mount by its universal id.\n */\nexport const requestMount = (): Promise<SandboxMount> =>\n requestMountInternal('request', {});\n\n/** Prompt the user to grant a mount, returning the granted {@link SandboxMount}.\n * @deprecated renamed to {@link requestMount} (backend-general, §3.5). */\nexport const requestSpace = requestMount;\n\n// ── content references (plan 12 §E / FILE_SHARING §7) ────────────────────────\n\n/**\n * Build a persisted CONTENT REFERENCE to a file in a mount — a `{mountId, relPath}`\n * pointer your app serializes into ITS OWN content (a board's JSON, an MDX file's\n * frontmatter, an album manifest — the platform doesn't dictate the container) so a\n * later viewer can resolve it. It is exactly the §5.7 {@link capFile} shape: ONE\n * capability, two delivery modes — runtime delegation (a task param, authorized by\n * the caller) vs a durable reference (authorized per-viewer by {@link resolveContentRef}).\n * `relPath` is BACKEND-NATURAL, so the reference resolves to the SAME path for every\n * viewer. Cross-app/cross-project references default to `ro`.\n *\n * const ref = makeContentRef({ mountId: 'space:ACME', relPath: 'office-seating/desk.mdx' }, { mode: 'ro' });\n */\nexport const makeContentRef = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): FileCap => ({ $cap: 'file', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/**\n * Resolve a content reference your app found in content it ALREADY holds\n * (FILE_SHARING §7 / UI_AS_APPS §8.7; \"plan 12 §E\"). This is a RELAY, not a\n * fabrication: the host honors it ONLY when your app\n * already holds a grant to `ref.mountId` (else `forbidden`) — apps follow\n * writer-authored links inside granted content; they cannot name a space from\n * nothing (T27). The host runs a per-VIEWER consent prompt (named via the owning\n * app's project sidecar), and existence is never leaked — a decline and a\n * non-existent path are indistinguishable.\n *\n * On allow, the host APPENDS a read scope for the referenced path to your grant\n * (durable; same §8.15 lifecycle) and returns the STABLE absolute `path` the file\n * is mounted at — identical for every viewer, so a path the author stored resolves\n * the same for you. Read it through the `fs` module at that path. Rejects with a\n * {@link SpaceError}: `forbidden` (you don't hold the referenced mount) or\n * `cancelled` (the viewer declined / the path doesn't exist — no oracle).\n *\n * const { path } = await resolveContentRef(ref);\n * const text = await fs.promises.readFile(path, 'utf8');\n */\nexport const resolveContentRef = async (ref: FileCap): Promise<{ path: string }> => {\n const path = await request<string>('resolveRef', { ref });\n return { path };\n};\n\n/**\n * Resolve a BATCH of content references in ONE consent round (FILE_SHARING §7 /\n * UI_AS_APPS §8.7; \"plan 12 §E\"). When a\n * board opens with several embedded references, pass them all here: the host\n * coalesces them into a SINGLE consent prompt listing every target, instead of one\n * prompt per reference. Same relay gate and per-viewer semantics as\n * {@link resolveContentRef} (each ref's mount must already be held), applied to the\n * whole set — it is all-or-nothing: the user allows the batch or declines it.\n *\n * Resolves `{ paths }` with the STABLE absolute path of each ref, in input order.\n * Rejects with a {@link SpaceError}: `forbidden` (a referenced mount isn't held) or\n * `cancelled` (the viewer declined).\n *\n * const { paths } = await resolveContentRefs(board.references);\n */\nexport const resolveContentRefs = async (refs: FileCap[]): Promise<{ paths: string[] }> => {\n const paths = await request<string[]>('resolveRefs', { refs });\n return { paths };\n};\n\n// ---------------------------------------------------------------------------\n// Settings — the per-user \"~/.config\"-style space (UI_AS_APPS_SPEC §3.3/§3.5/§8.2).\n// Each app gets its OWN settings subdir, auto-provisioned and chroot'd by the host\n// (no dialog, no powerbox). Read/write it through the returned mount's filesystem\n// port — there is deliberately no key/value get/set API; settings are just files.\n// ---------------------------------------------------------------------------\n\n// Issue a `protocol-settings` request, unwrapping {ok,data} and throwing a typed\n// SpaceError on failure (mirrors `request` for the spaces surface).\nconst settingsRequest = async <T = unknown>(\n method: string,\n query: Record<string, unknown> = {},\n): Promise<T> => {\n const res = (await protocolRequest('settings', method, [query])) as SpaceResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'settings request failed') as SpaceError;\n err.code = (res?.code as SpaceError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Mount this app's per-user settings — a private `~/.config`-style filesystem,\n * auto-provisioned for the signed-in user and isolated to THIS app (the host\n * chroots it; a different app can never name it). Read/write config files through\n * the returned mount. Rejects with a {@link SpaceError} (`auth-required`) when\n * signed out. Capability: baseline `settings:app`.\n */\nexport const openSettings = async (): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('open');\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * One-time SEED of this app's settings from the parent it declares as `forkOf`\n * (its `package.json` `immediately.run.forkOf`) — so a fork inherits your\n * preferences from the original app (UI_AS_APPS_SPEC §3.4). The host asks the user\n * to confirm (a full consent when the apps have different owners, a light confirm\n * when the same owner publishes both) and copies the parent's settings into this\n * app's own subdir, skipping any file you already have. Non-throwing: resolves\n * `{ ok:false, code }` on decline (`cancelled`), no declared parent (`forbidden`),\n * or signed-out (`auth-required`). After `{ ok:true }`, read {@link openSettings}.\n * Capability: baseline `settings:fork`.\n */\nexport const importSettingsFromParent = async (): Promise<\n { ok: true; copied: number } | { ok: false; code: string }\n> => {\n try {\n const data = await settingsRequest<{ copied: number }>('importFromParent');\n return { ok: true, copied: data.copied };\n } catch (e) {\n return { ok: false, code: (e as SpaceError).code ?? 'unknown' };\n }\n};\n\n/**\n * Mount ANOTHER app's per-user settings by its `appKey` — the elevated \"file\n * commander\" surface. Rejects `forbidden` unless this app holds the first-party-\n * only `settings:all` capability. Most apps want {@link openSettings} instead.\n */\nexport const openSettingsOf = async (appKey: string): Promise<SandboxMount> => {\n const mount = await settingsRequest<SandboxMount>('openOf', { appKey });\n return waitForMount({ id: mount.id ?? mount.path });\n};\n\n/**\n * List every app that has per-user settings — the elevated \"file commander\"\n * enumeration. Pair with {@link openSettingsOf} to mount any of them. Rejects\n * `forbidden` unless this app holds the first-party-only `settings:all`.\n */\nexport const listSettingsApps = (): Promise<string[]> =>\n settingsRequest<string[]>('list');\n\n/** Create a brand-new, empty platform-hosted space. The app reaches it (or any\n * other space) afterward through the {@link requestMount} powerbox or\n * {@link mountSpace}; there is no implicit per-app binding. */\nexport const createSpace = (\n opts: { name?: string } = {}\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\n/** A collaborator's role on a shared space: full `owner`, read-write `writer`, or read-only `reader`. */\nexport type Role = 'owner' | 'writer' | 'reader';\n\n/** A member of a space (for the share/manage UI). */\nexport interface Member {\n /**\n * The **grantee** — `user:{uid}` | `group:{gid}`. This is the canonical name\n * (core_concepts §4: \"principal\" is reserved for the authority context; a space\n * member is a *grantee*). The host populates this on every member row.\n */\n grantee: string;\n /**\n * @deprecated Use {@link Member.grantee}. Kept as an alias (same value) for\n * back-compat during the `principal`→`grantee` migration; will be removed in a\n * future major. The host still populates both.\n */\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/** A pending invitation to a space (pull-based sharing, FILE_SHARING_SPEC §6.4).\n * It grants NO access until accepted — the recipient accepts it from their inbox\n * ({@link listMyInvites} → {@link acceptInvite}), materializing membership. The\n * display fields (`name`/`login`/`avatarUrl`) are untrusted for rendering. */\nexport interface Invite {\n spaceId: string;\n /** The invitee's uid — carried so the owner's pending list can\n * {@link revokeInvite}(spaceId, uid). */\n uid: string;\n role: Role;\n owner: string;\n name?: string;\n invitedBy: string;\n /** epoch ms (server-stamped); absent until the write settles. */\n invitedAt?: number;\n login?: string;\n avatarUrl?: string;\n}\n\n/** 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 one\n * it invited. Pull-based (FILE_SHARING_SPEC §6.4): this writes an INVITATION, not\n * membership — the recipient must {@link acceptInvite}. Re-inviting an already-\n * invited/member user is idempotent. */\nexport const inviteToSpace = async (spaceId: string, login: string, role: Role): Promise<void> => {\n await request('invite', { spaceId, login, role });\n};\n\n/** The owner's outstanding invitations for a space — `spaces:admin`. */\nexport const listPendingInvites = (spaceId: string): Promise<Invite[]> =>\n request<Invite[]>('pendingInvites', { spaceId });\n\n/** Withdraw a pending invitation (distinct from {@link unshareSpace}, which removes\n * an ACCEPTED member) — `spaces:admin`. */\nexport const revokeInvite = async (spaceId: string, uid: string): Promise<void> => {\n await request('revokeInvite', { spaceId, uid });\n};\n\n/** The caller's OWN invitation inbox — `spaces:user`. */\nexport const listMyInvites = (): Promise<Invite[]> => request<Invite[]>('listInvites', {});\n\n/** Accept an invitation: materialize your membership at the invited role and clear\n * the invite — `spaces:user`. An invitation the caller doesn't hold rejects with\n * `forbidden` (indistinguishable from a nonexistent space; no existence oracle). */\nexport const acceptInvite = async (spaceId: string): Promise<void> => {\n await request('acceptInvite', { spaceId });\n};\n\n/** Decline (dismiss) an invitation from your inbox; writes no membership —\n * `spaces:user`. */\nexport const declineInvite = async (spaceId: string): Promise<void> => {\n await request('declineInvite', { spaceId });\n};\n\n// The live invitations inbox (FILE_SHARING §6.4/§9.8): the host pushes the caller's\n// current invitations on change and replays on register-frame; gated `spaces:user`.\n// So an invite that arrives (or an accepted/declined one leaving) reflects within one\n// snapshot — no poll. Mirrors the host's `invitations`/`request-invitations` wiring.\nconst invitesChannel = createPushChannel<Invite[]>({\n pushType: 'invitations',\n requestType: 'request-invitations',\n initial: [],\n parse: (msg) => (Array.isArray(msg.invites) ? (msg.invites as Invite[]) : undefined),\n});\n\n/** The caller's current invitations (`spaces:user`). One-off read; use\n * {@link onInvitesChange}/{@link useInvites} to react live. */\nexport const getInvites = (): Invite[] => invitesChannel.get();\n\n/** Subscribe to invitation-inbox changes (arrived / accepted / declined). Invoked\n * immediately with the current list, then on every change. Returns an unsubscribe. */\nexport const onInvitesChange = (listener: (invites: Invite[]) => void): (() => void) =>\n invitesChannel.onChange(listener);\n\n/** React hook returning the caller's live invitation inbox, re-rendering on change\n * (the space-manager Invitations inbox, §9.8). */\nexport const useInvites = (): Invite[] => invitesChannel.use();\n\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":"AAAA,SAAS,WAAW,gBAAgB;AACpC,SAAS,iBAAiB,aAAa,mBAAmB;AAC1D,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAatB,MAAM,kBAAkB,MAAc,eAAe,GAAG,gBAAgB;AAoF/E,MAAM,WAAW,CAAC,MAA4B,EAAE,MAAM,EAAE;AAExD,MAAM,uBAA4C,oBAAI,IAAuB;AAAA,EAC3E;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAID,MAAM,sBAAsB,CAAC,UAC3B,OAAO,UAAU,YAAY,qBAAqB,IAAI,KAAK,IACtD,QACD;AAKN,MAAM,uBAAuB,MAA2B;AACtD,MAAI;AAEF,UAAM,MAAM,QAAQ,YAAY,QAAQ,SAAS;AACjD,WAAO,OAAO,OAAO,IAAI,cAAc,aAAa,MAAM;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUA,IAAI,eAAoC;AAExC,MAAM,wBAAwB,MAAoB;AAChD,MAAI,aAAc,QAAO;AACzB,MAAI,SAAyB,CAAC;AAC9B,QAAM,YAAY,oBAAI,IAAoD;AAC1E,QAAM,OAAO,CAAC,YAA4B;AACxC,eAAW,KAAK,CAAC,GAAG,SAAS,EAAG,GAAE,QAAQ,OAAO;AAAA,EACnD;AAEA,cAAY,aAAa,CAAC,QAA6B;AACrD,UAAMA,SAAkC,IAAI;AAC5C,QAAI,CAACA,OAAO;AACZ,UAAM,MAAM,SAASA,MAAK;AAC1B,aAAS,CAAC,GAAG,OAAO,OAAO,CAAC,MAAM,SAAS,CAAC,MAAM,GAAG,GAAGA,MAAK;AAC7D,SAAK,CAAC,CAAC;AAAA,EACT,CAAC;AACD,cAAY,gBAAgB,CAAC,QAA6B;AACxD,UAAM,MAA0B,IAAI,MAAM,IAAI;AAC9C,QAAI,OAAO,KAAM;AACjB,UAAM,SAAS,oBAAoB,IAAI,MAAM;AAC7C,UAAM,UAAU,OAAO,OAAO,CAAC,MAAM,SAAS,CAAC,MAAM,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,EAAE;AACvF,QAAI,QAAQ,WAAW,EAAG;AAC1B,aAAS,OAAO,OAAO,CAAC,MAAM,SAAS,CAAC,MAAM,GAAG;AACjD,SAAK,OAAO;AAAA,EACd,CAAC;AAID,MAAI;AACF,gBAAY,gBAAgB;AAAA,EAC9B,QAAQ;AAAA,EAER;AAEA,iBAAe;AAAA,IACb,WAAW,MAAM;AAAA,IACjB,UAAU,CAAC,aAAa;AACtB,gBAAU,IAAI,QAAQ;AACtB,eAAS,QAAQ,CAAC,CAAC;AACnB,aAAO,EAAE,SAAS,MAAM,UAAU,OAAO,QAAQ,EAAE;AAAA,IACrD;AAAA,EACF;AACA,SAAO;AACT;AAIA,MAAM,eAAe,MAAoB,qBAAqB,KAAK,sBAAsB;AAMzF,MAAM,UAAU,CAACA,QAAqB,UACpC,aAAaA,QAAO,KAAK;AASpB,MAAM,YAAY,MAAsB,aAAa,EAAE,UAAU;AAGjE,MAAM,YAAY,CAAC,UACxB,UAAU,EAAE,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAUpC,MAAM,iBAAiB,CAC5B,aACiB;AACjB,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;AAsBA,MAAM,uBAAuB,kBAAkC;AAAA,EAC7D,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,MAAM,IAAK,IAAI,SAA4B;AAChF,CAAC;AAKM,MAAM,mBAAmB,MAAsB,qBAAqB,IAAI;AAKxE,MAAM,wBAAwB,CACnC,aACiB,qBAAqB,SAAS,QAAQ;AAKlD,MAAM,mBAAmB,MAAsB,qBAAqB,IAAI;AAkC/E,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,QAAQ,CAAC,YACpB,qBAAqB,SAAS,EAAE,OAAO,QAAQ,CAAC;AAI3C,MAAM,aAAa,CAAC,UACzB,MAAM,SAAS,MAAM,OAAO,EAAE;AAqBzB,MAAM,eAAe,MAC1B,qBAAqB,WAAW,CAAC,CAAC;AAI7B,MAAM,eAAe;AAgBrB,MAAM,iBAAiB,CAC5B,KACA,UACa,EAAE,MAAM,QAAQ,SAAS,IAAI,SAAS,SAAS,IAAI,SAAS,MAAM,KAAK,KAAK;AAsBpF,MAAM,oBAAoB,OAAO,QAA4C;AAClF,QAAM,OAAO,MAAM,QAAgB,cAAc,EAAE,IAAI,CAAC;AACxD,SAAO,EAAE,KAAK;AAChB;AAiBO,MAAM,qBAAqB,OAAO,SAAkD;AACzF,QAAM,QAAQ,MAAM,QAAkB,eAAe,EAAE,KAAK,CAAC;AAC7D,SAAO,EAAE,MAAM;AACjB;AAWA,MAAM,kBAAkB,OACtB,QACA,QAAiC,CAAC,MACnB;AACf,QAAM,MAAO,MAAM,gBAAgB,YAAY,QAAQ,CAAC,KAAK,CAAC;AAC9D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,yBAAyB;AAC/D,QAAI,OAAQ,KAAK,QAA+B;AAChD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AASO,MAAM,eAAe,YAAmC;AAC7D,QAAMA,SAAQ,MAAM,gBAA8B,MAAM;AACxD,SAAO,aAAa,EAAE,IAAIA,OAAM,MAAMA,OAAM,KAAK,CAAC;AACpD;AAaO,MAAM,2BAA2B,YAEnC;AACH,MAAI;AACF,UAAM,OAAO,MAAM,gBAAoC,kBAAkB;AACzE,WAAO,EAAE,IAAI,MAAM,QAAQ,KAAK,OAAO;AAAA,EACzC,SAAS,GAAG;AACV,WAAO,EAAE,IAAI,OAAO,MAAO,EAAiB,QAAQ,UAAU;AAAA,EAChE;AACF;AAOO,MAAM,iBAAiB,OAAO,WAA0C;AAC7E,QAAMA,SAAQ,MAAM,gBAA8B,UAAU,EAAE,OAAO,CAAC;AACtE,SAAO,aAAa,EAAE,IAAIA,OAAM,MAAMA,OAAM,KAAK,CAAC;AACpD;AAOO,MAAM,mBAAmB,MAC9B,gBAA0B,MAAM;AAK3B,MAAM,cAAc,CACzB,OAA0B,CAAC,MACD,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;AA4DO,MAAM,gBAAgB,MAA4B,QAAqB,WAAW,CAAC,CAAC;AAGpF,MAAM,kBAAkB,CAAC,YAC9B,QAAkB,WAAW,EAAE,QAAQ,CAAC;AAOnC,MAAM,gBAAgB,OAAO,SAAiB,OAAe,SAA8B;AAChG,QAAM,QAAQ,UAAU,EAAE,SAAS,OAAO,KAAK,CAAC;AAClD;AAGO,MAAM,qBAAqB,CAAC,YACjC,QAAkB,kBAAkB,EAAE,QAAQ,CAAC;AAI1C,MAAM,eAAe,OAAO,SAAiB,QAA+B;AACjF,QAAM,QAAQ,gBAAgB,EAAE,SAAS,IAAI,CAAC;AAChD;AAGO,MAAM,gBAAgB,MAAyB,QAAkB,eAAe,CAAC,CAAC;AAKlF,MAAM,eAAe,OAAO,YAAmC;AACpE,QAAM,QAAQ,gBAAgB,EAAE,QAAQ,CAAC;AAC3C;AAIO,MAAM,gBAAgB,OAAO,YAAmC;AACrE,QAAM,QAAQ,iBAAiB,EAAE,QAAQ,CAAC;AAC5C;AAMA,MAAM,iBAAiB,kBAA4B;AAAA,EACjD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAAuB;AAC5E,CAAC;AAIM,MAAM,aAAa,MAAgB,eAAe,IAAI;AAItD,MAAM,kBAAkB,CAAC,aAC9B,eAAe,SAAS,QAAQ;AAI3B,MAAM,aAAa,MAAgB,eAAe,IAAI;AAItD,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;AAgBxC,MAAM,aAAa,MAA8B,QAAuB,UAAU,CAAC,CAAC;AAIpF,MAAM,cAAc,OAAO,QAAgB,YAAmC;AACnF,QAAM,QAAQ,eAAe,EAAE,QAAQ,QAAQ,CAAC;AAClD;","names":["mount"]}
|
package/dist/vcs.cjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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 vcs_exports = {};
|
|
20
|
+
__export(vcs_exports, {
|
|
21
|
+
getVcsState: () => getVcsState,
|
|
22
|
+
onVcsStateChange: () => onVcsStateChange,
|
|
23
|
+
refreshDiff: () => refreshDiff,
|
|
24
|
+
refreshPRs: () => refreshPRs,
|
|
25
|
+
resetWorkingTree: () => resetWorkingTree,
|
|
26
|
+
useVcsState: () => useVcsState
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(vcs_exports);
|
|
29
|
+
var import_pushChannel = require("./pushChannel");
|
|
30
|
+
var import_sandboxUtils = require("./sandboxUtils");
|
|
31
|
+
const EMPTY = { changes: [], branch: null, prs: [], diffLoading: false };
|
|
32
|
+
const isChangeArray = (v) => Array.isArray(v) && v.every(
|
|
33
|
+
(c) => !!c && typeof c.path === "string" && typeof c.status === "string"
|
|
34
|
+
);
|
|
35
|
+
const channel = (0, import_pushChannel.createPushChannel)({
|
|
36
|
+
pushType: "vcs-state",
|
|
37
|
+
requestType: "request-vcs-state",
|
|
38
|
+
initial: EMPTY,
|
|
39
|
+
parse: (msg) => {
|
|
40
|
+
if (!isChangeArray(msg.changes)) return void 0;
|
|
41
|
+
const branch = msg.branch && typeof msg.branch === "object" ? msg.branch : null;
|
|
42
|
+
const prs = Array.isArray(msg.prs) ? msg.prs : [];
|
|
43
|
+
return {
|
|
44
|
+
changes: msg.changes,
|
|
45
|
+
branch,
|
|
46
|
+
prs,
|
|
47
|
+
diffLoading: msg.diffLoading === true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const getVcsState = () => channel.get();
|
|
52
|
+
const onVcsStateChange = (listener) => channel.onChange(listener);
|
|
53
|
+
const useVcsState = () => channel.use();
|
|
54
|
+
const vcsRequest = async (method, arg = {}) => {
|
|
55
|
+
const res = await (0, import_sandboxUtils.protocolRequest)("vcs", method, [arg]);
|
|
56
|
+
if (!res || res.ok !== true) {
|
|
57
|
+
const err = new Error(res?.message ?? `vcs ${method} failed`);
|
|
58
|
+
err.code = res?.code ?? "unknown";
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const refreshDiff = () => vcsRequest("refreshDiff");
|
|
63
|
+
const refreshPRs = () => vcsRequest("refreshPRs");
|
|
64
|
+
const resetWorkingTree = () => vcsRequest("reset", { confirm: true });
|
|
65
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
66
|
+
0 && (module.exports = {
|
|
67
|
+
getVcsState,
|
|
68
|
+
onVcsStateChange,
|
|
69
|
+
refreshDiff,
|
|
70
|
+
refreshPRs,
|
|
71
|
+
resetWorkingTree,
|
|
72
|
+
useVcsState
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=vcs.cjs.map
|
package/dist/vcs.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vcs.ts"],"sourcesContent":["// The `vcs:read` / `vcs:reset` source-control surface — app-facing wrappers\n// (UI_AS_APPS_SPEC §5.3 Recipe A read channel + §8.4 Recipe B actions;\n// migrate-sidebars-to-apps Phase 05; roadmap R3-52).\n//\n// A first-party contribute panel (the `panel.contribute` app, Phase 06) reads the\n// live source-control state the native `SourceControlPanel` shows — the working-tree\n// diff summary, the branch lineage, and the open-PR list — and can trigger a\n// refresh/reset. The host derives all of it from authenticated GitHub calls +\n// the COW layers; NONE of the underlying `DiffResult` / `FileSystem` / OAuth token\n// ever crosses the boundary (§8.10) — the wire carries only the plain-JSON\n// {@link VcsState} the host projects.\n//\n// Read side: a Recipe-A push channel, the same get/onChange/use trio as\n// `editorContext` / `diagnostics`. Inert until the host wires the channel\n// (site-main `channelBridge`); an app without `vcs:read` simply sees the empty\n// initial. Action side: `protocol-vcs` requests gated host-side — `refreshDiff` /\n// `refreshPRs` by `vcs:read`, `resetWorkingTree` by first-party-only `vcs:reset`.\nimport { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\n\n/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the\n * host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT\n * carried — the native panel showed path + status, and a per-file content diff is\n * a deferred follow-up (plan step 6). */\nexport interface VcsChange {\n path: string;\n status: 'created' | 'modified' | 'deleted';\n}\n\n/** The branch the working tree sits on and the upstream it diverged from\n * (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created\n * branch. `upstreamPushable` is `null` while push access is still being probed. */\nexport interface VcsBranch {\n name: string;\n parentRepo: string;\n parentRef: string;\n parentCommitSha: string;\n upstreamPushable: boolean | null;\n}\n\n/** One pull request open from the current branch (host `BranchPR`). */\nexport interface VcsPR {\n number: number;\n url: string;\n title: string;\n state: 'open' | 'closed' | 'merged';\n draft: boolean;\n}\n\n/** The whole source-control snapshot the host projects to a `vcs:read` frame.\n * Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */\nexport interface VcsState {\n changes: VcsChange[];\n branch: VcsBranch | null;\n prs: VcsPR[];\n /** True while the host is recomputing the diff — the panel shows a spinner\n * without a separate request (plan gotcha). */\n diffLoading: boolean;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `vcs:read`): an empty, branch-less snapshot. */\nconst EMPTY: VcsState = { changes: [], branch: null, prs: [], diffLoading: false };\n\nconst isChangeArray = (v: unknown): v is VcsChange[] =>\n Array.isArray(v) &&\n v.every(\n (c) =>\n !!c &&\n typeof (c as VcsChange).path === 'string' &&\n typeof (c as VcsChange).status === 'string',\n );\n\nconst channel = createPushChannel<VcsState>({\n pushType: 'vcs-state',\n requestType: 'request-vcs-state',\n initial: EMPTY,\n parse: (msg) => {\n // Require a well-formed `changes` array; tolerate an absent branch/prs. A\n // malformed push is ignored (returns undefined) so the last good state stands.\n if (!isChangeArray(msg.changes)) return undefined;\n const branch =\n msg.branch && typeof msg.branch === 'object' ? (msg.branch as VcsBranch) : null;\n const prs = Array.isArray(msg.prs) ? (msg.prs as VcsPR[]) : [];\n return {\n changes: msg.changes,\n branch,\n prs,\n diffLoading: msg.diffLoading === true,\n };\n },\n});\n\n/** One-off read of the current source-control state. Returns the empty snapshot\n * until the host answers (or if the app lacks `vcs:read`). Use\n * {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */\nexport const getVcsState = (): VcsState => channel.get();\n\n/** Subscribe to source-control changes. Invoked immediately with the current\n * value, then on every host push (diff refresh, PR poll, branch change). Returns\n * an unsubscribe. */\nexport const onVcsStateChange = (listener: (state: VcsState) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook: the current source-control state, re-rendering on every change. */\nexport const useVcsState = (): VcsState => channel.use();\n\n// ---------------------------------------------------------------------------\n// Actions (Recipe B, §8.4). The app NAMES an intent and the HOST performs it —\n// the COW/journal stays in the kernel (§2/§4). `refreshDiff`/`refreshPRs` only\n// cause a host-side recompute + a fresh push (gated `vcs:read`, no new authority);\n// `resetWorkingTree` DISCARDS the user's unsaved work, gated by the first-party-only\n// `vcs:reset` — a fork can never hold it. The arm-then-confirm UX stays in the app;\n// the authority is gated host-side (§8.9), and the host requires `confirm:true` as\n// belt-and-braces (T22).\n// ---------------------------------------------------------------------------\n\n/** An error from a `vcs` action, carrying a machine-readable `.code`. */\nexport interface VcsActionError extends Error {\n code:\n | 'forbidden' // the frame lacks the required capability (`vcs:read` / `vcs:reset`)\n | 'invalid-params' // reset called without `confirm: true`\n | 'no-target' // there is no host contribute session (not in edit mode)\n | 'unknown';\n}\n\ntype VcsResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst vcsRequest = async (method: string, arg: Record<string, unknown> = {}): Promise<void> => {\n const res = (await protocolRequest('vcs', method, [arg])) as VcsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `vcs ${method} failed`) as VcsActionError;\n err.code = (res?.code as VcsActionError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.\n * Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshDiff = (): Promise<void> => vcsRequest('refreshDiff');\n\n/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated\n * `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshPRs = (): Promise<void> => vcsRequest('refreshPRs');\n\n/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —\n * irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the\n * gate. Requires `confirm: true` (host belt-and-braces). Rejects with a\n * {@link VcsActionError} (`.code`). */\nexport const resetWorkingTree = (): Promise<void> => vcsRequest('reset', { confirm: true });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,yBAAkC;AAClC,0BAAgC;AA4ChC,MAAM,QAAkB,EAAE,SAAS,CAAC,GAAG,QAAQ,MAAM,KAAK,CAAC,GAAG,aAAa,MAAM;AAEjF,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KACf,EAAE;AAAA,EACA,CAAC,MACC,CAAC,CAAC,KACF,OAAQ,EAAgB,SAAS,YACjC,OAAQ,EAAgB,WAAW;AACvC;AAEF,MAAM,cAAU,sCAA4B;AAAA,EAC1C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,cAAc,IAAI,OAAO,EAAG,QAAO;AACxC,UAAM,SACJ,IAAI,UAAU,OAAO,IAAI,WAAW,WAAY,IAAI,SAAuB;AAC7E,UAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,IAAK,IAAI,MAAkB,CAAC;AAC7D,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA,aAAa,IAAI,gBAAgB;AAAA,IACnC;AAAA,EACF;AACF,CAAC;AAKM,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAKhD,MAAM,mBAAmB,CAAC,aAC/B,QAAQ,SAAS,QAAQ;AAGpB,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAyBvD,MAAM,aAAa,OAAO,QAAgB,MAA+B,CAAC,MAAqB;AAC7F,QAAM,MAAO,UAAM,qCAAgB,OAAO,QAAQ,CAAC,GAAG,CAAC;AACvD,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,OAAO,MAAM,SAAS;AAC5D,QAAI,OAAQ,KAAK,QAAmC;AACpD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,cAAc,MAAqB,WAAW,aAAa;AAIjE,MAAM,aAAa,MAAqB,WAAW,YAAY;AAM/D,MAAM,mBAAmB,MAAqB,WAAW,SAAS,EAAE,SAAS,KAAK,CAAC;","names":[]}
|
package/dist/vcs.d.cts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the
|
|
2
|
+
* host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT
|
|
3
|
+
* carried — the native panel showed path + status, and a per-file content diff is
|
|
4
|
+
* a deferred follow-up (plan step 6). */
|
|
5
|
+
interface VcsChange {
|
|
6
|
+
path: string;
|
|
7
|
+
status: 'created' | 'modified' | 'deleted';
|
|
8
|
+
}
|
|
9
|
+
/** The branch the working tree sits on and the upstream it diverged from
|
|
10
|
+
* (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created
|
|
11
|
+
* branch. `upstreamPushable` is `null` while push access is still being probed. */
|
|
12
|
+
interface VcsBranch {
|
|
13
|
+
name: string;
|
|
14
|
+
parentRepo: string;
|
|
15
|
+
parentRef: string;
|
|
16
|
+
parentCommitSha: string;
|
|
17
|
+
upstreamPushable: boolean | null;
|
|
18
|
+
}
|
|
19
|
+
/** One pull request open from the current branch (host `BranchPR`). */
|
|
20
|
+
interface VcsPR {
|
|
21
|
+
number: number;
|
|
22
|
+
url: string;
|
|
23
|
+
title: string;
|
|
24
|
+
state: 'open' | 'closed' | 'merged';
|
|
25
|
+
draft: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** The whole source-control snapshot the host projects to a `vcs:read` frame.
|
|
28
|
+
* Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */
|
|
29
|
+
interface VcsState {
|
|
30
|
+
changes: VcsChange[];
|
|
31
|
+
branch: VcsBranch | null;
|
|
32
|
+
prs: VcsPR[];
|
|
33
|
+
/** True while the host is recomputing the diff — the panel shows a spinner
|
|
34
|
+
* without a separate request (plan gotcha). */
|
|
35
|
+
diffLoading: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** One-off read of the current source-control state. Returns the empty snapshot
|
|
38
|
+
* until the host answers (or if the app lacks `vcs:read`). Use
|
|
39
|
+
* {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */
|
|
40
|
+
declare const getVcsState: () => VcsState;
|
|
41
|
+
/** Subscribe to source-control changes. Invoked immediately with the current
|
|
42
|
+
* value, then on every host push (diff refresh, PR poll, branch change). Returns
|
|
43
|
+
* an unsubscribe. */
|
|
44
|
+
declare const onVcsStateChange: (listener: (state: VcsState) => void) => (() => void);
|
|
45
|
+
/** React hook: the current source-control state, re-rendering on every change. */
|
|
46
|
+
declare const useVcsState: () => VcsState;
|
|
47
|
+
/** An error from a `vcs` action, carrying a machine-readable `.code`. */
|
|
48
|
+
interface VcsActionError extends Error {
|
|
49
|
+
code: 'forbidden' | 'invalid-params' | 'no-target' | 'unknown';
|
|
50
|
+
}
|
|
51
|
+
/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.
|
|
52
|
+
* Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */
|
|
53
|
+
declare const refreshDiff: () => Promise<void>;
|
|
54
|
+
/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated
|
|
55
|
+
* `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */
|
|
56
|
+
declare const refreshPRs: () => Promise<void>;
|
|
57
|
+
/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —
|
|
58
|
+
* irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the
|
|
59
|
+
* gate. Requires `confirm: true` (host belt-and-braces). Rejects with a
|
|
60
|
+
* {@link VcsActionError} (`.code`). */
|
|
61
|
+
declare const resetWorkingTree: () => Promise<void>;
|
|
62
|
+
|
|
63
|
+
export { type VcsActionError, type VcsBranch, type VcsChange, type VcsPR, type VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState };
|
package/dist/vcs.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the
|
|
2
|
+
* host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT
|
|
3
|
+
* carried — the native panel showed path + status, and a per-file content diff is
|
|
4
|
+
* a deferred follow-up (plan step 6). */
|
|
5
|
+
interface VcsChange {
|
|
6
|
+
path: string;
|
|
7
|
+
status: 'created' | 'modified' | 'deleted';
|
|
8
|
+
}
|
|
9
|
+
/** The branch the working tree sits on and the upstream it diverged from
|
|
10
|
+
* (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created
|
|
11
|
+
* branch. `upstreamPushable` is `null` while push access is still being probed. */
|
|
12
|
+
interface VcsBranch {
|
|
13
|
+
name: string;
|
|
14
|
+
parentRepo: string;
|
|
15
|
+
parentRef: string;
|
|
16
|
+
parentCommitSha: string;
|
|
17
|
+
upstreamPushable: boolean | null;
|
|
18
|
+
}
|
|
19
|
+
/** One pull request open from the current branch (host `BranchPR`). */
|
|
20
|
+
interface VcsPR {
|
|
21
|
+
number: number;
|
|
22
|
+
url: string;
|
|
23
|
+
title: string;
|
|
24
|
+
state: 'open' | 'closed' | 'merged';
|
|
25
|
+
draft: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** The whole source-control snapshot the host projects to a `vcs:read` frame.
|
|
28
|
+
* Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */
|
|
29
|
+
interface VcsState {
|
|
30
|
+
changes: VcsChange[];
|
|
31
|
+
branch: VcsBranch | null;
|
|
32
|
+
prs: VcsPR[];
|
|
33
|
+
/** True while the host is recomputing the diff — the panel shows a spinner
|
|
34
|
+
* without a separate request (plan gotcha). */
|
|
35
|
+
diffLoading: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** One-off read of the current source-control state. Returns the empty snapshot
|
|
38
|
+
* until the host answers (or if the app lacks `vcs:read`). Use
|
|
39
|
+
* {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */
|
|
40
|
+
declare const getVcsState: () => VcsState;
|
|
41
|
+
/** Subscribe to source-control changes. Invoked immediately with the current
|
|
42
|
+
* value, then on every host push (diff refresh, PR poll, branch change). Returns
|
|
43
|
+
* an unsubscribe. */
|
|
44
|
+
declare const onVcsStateChange: (listener: (state: VcsState) => void) => (() => void);
|
|
45
|
+
/** React hook: the current source-control state, re-rendering on every change. */
|
|
46
|
+
declare const useVcsState: () => VcsState;
|
|
47
|
+
/** An error from a `vcs` action, carrying a machine-readable `.code`. */
|
|
48
|
+
interface VcsActionError extends Error {
|
|
49
|
+
code: 'forbidden' | 'invalid-params' | 'no-target' | 'unknown';
|
|
50
|
+
}
|
|
51
|
+
/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.
|
|
52
|
+
* Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */
|
|
53
|
+
declare const refreshDiff: () => Promise<void>;
|
|
54
|
+
/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated
|
|
55
|
+
* `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */
|
|
56
|
+
declare const refreshPRs: () => Promise<void>;
|
|
57
|
+
/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —
|
|
58
|
+
* irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the
|
|
59
|
+
* gate. Requires `confirm: true` (host belt-and-braces). Rejects with a
|
|
60
|
+
* {@link VcsActionError} (`.code`). */
|
|
61
|
+
declare const resetWorkingTree: () => Promise<void>;
|
|
62
|
+
|
|
63
|
+
export { type VcsActionError, type VcsBranch, type VcsChange, type VcsPR, type VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState };
|
package/dist/vcs.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createPushChannel } from "./pushChannel";
|
|
2
|
+
import { protocolRequest } from "./sandboxUtils";
|
|
3
|
+
const EMPTY = { changes: [], branch: null, prs: [], diffLoading: false };
|
|
4
|
+
const isChangeArray = (v) => Array.isArray(v) && v.every(
|
|
5
|
+
(c) => !!c && typeof c.path === "string" && typeof c.status === "string"
|
|
6
|
+
);
|
|
7
|
+
const channel = createPushChannel({
|
|
8
|
+
pushType: "vcs-state",
|
|
9
|
+
requestType: "request-vcs-state",
|
|
10
|
+
initial: EMPTY,
|
|
11
|
+
parse: (msg) => {
|
|
12
|
+
if (!isChangeArray(msg.changes)) return void 0;
|
|
13
|
+
const branch = msg.branch && typeof msg.branch === "object" ? msg.branch : null;
|
|
14
|
+
const prs = Array.isArray(msg.prs) ? msg.prs : [];
|
|
15
|
+
return {
|
|
16
|
+
changes: msg.changes,
|
|
17
|
+
branch,
|
|
18
|
+
prs,
|
|
19
|
+
diffLoading: msg.diffLoading === true
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const getVcsState = () => channel.get();
|
|
24
|
+
const onVcsStateChange = (listener) => channel.onChange(listener);
|
|
25
|
+
const useVcsState = () => channel.use();
|
|
26
|
+
const vcsRequest = async (method, arg = {}) => {
|
|
27
|
+
const res = await protocolRequest("vcs", method, [arg]);
|
|
28
|
+
if (!res || res.ok !== true) {
|
|
29
|
+
const err = new Error(res?.message ?? `vcs ${method} failed`);
|
|
30
|
+
err.code = res?.code ?? "unknown";
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const refreshDiff = () => vcsRequest("refreshDiff");
|
|
35
|
+
const refreshPRs = () => vcsRequest("refreshPRs");
|
|
36
|
+
const resetWorkingTree = () => vcsRequest("reset", { confirm: true });
|
|
37
|
+
export {
|
|
38
|
+
getVcsState,
|
|
39
|
+
onVcsStateChange,
|
|
40
|
+
refreshDiff,
|
|
41
|
+
refreshPRs,
|
|
42
|
+
resetWorkingTree,
|
|
43
|
+
useVcsState
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=vcs.js.map
|
package/dist/vcs.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vcs.ts"],"sourcesContent":["// The `vcs:read` / `vcs:reset` source-control surface — app-facing wrappers\n// (UI_AS_APPS_SPEC §5.3 Recipe A read channel + §8.4 Recipe B actions;\n// migrate-sidebars-to-apps Phase 05; roadmap R3-52).\n//\n// A first-party contribute panel (the `panel.contribute` app, Phase 06) reads the\n// live source-control state the native `SourceControlPanel` shows — the working-tree\n// diff summary, the branch lineage, and the open-PR list — and can trigger a\n// refresh/reset. The host derives all of it from authenticated GitHub calls +\n// the COW layers; NONE of the underlying `DiffResult` / `FileSystem` / OAuth token\n// ever crosses the boundary (§8.10) — the wire carries only the plain-JSON\n// {@link VcsState} the host projects.\n//\n// Read side: a Recipe-A push channel, the same get/onChange/use trio as\n// `editorContext` / `diagnostics`. Inert until the host wires the channel\n// (site-main `channelBridge`); an app without `vcs:read` simply sees the empty\n// initial. Action side: `protocol-vcs` requests gated host-side — `refreshDiff` /\n// `refreshPRs` by `vcs:read`, `resetWorkingTree` by first-party-only `vcs:reset`.\nimport { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\n\n/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the\n * host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT\n * carried — the native panel showed path + status, and a per-file content diff is\n * a deferred follow-up (plan step 6). */\nexport interface VcsChange {\n path: string;\n status: 'created' | 'modified' | 'deleted';\n}\n\n/** The branch the working tree sits on and the upstream it diverged from\n * (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created\n * branch. `upstreamPushable` is `null` while push access is still being probed. */\nexport interface VcsBranch {\n name: string;\n parentRepo: string;\n parentRef: string;\n parentCommitSha: string;\n upstreamPushable: boolean | null;\n}\n\n/** One pull request open from the current branch (host `BranchPR`). */\nexport interface VcsPR {\n number: number;\n url: string;\n title: string;\n state: 'open' | 'closed' | 'merged';\n draft: boolean;\n}\n\n/** The whole source-control snapshot the host projects to a `vcs:read` frame.\n * Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */\nexport interface VcsState {\n changes: VcsChange[];\n branch: VcsBranch | null;\n prs: VcsPR[];\n /** True while the host is recomputing the diff — the panel shows a spinner\n * without a separate request (plan gotcha). */\n diffLoading: boolean;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `vcs:read`): an empty, branch-less snapshot. */\nconst EMPTY: VcsState = { changes: [], branch: null, prs: [], diffLoading: false };\n\nconst isChangeArray = (v: unknown): v is VcsChange[] =>\n Array.isArray(v) &&\n v.every(\n (c) =>\n !!c &&\n typeof (c as VcsChange).path === 'string' &&\n typeof (c as VcsChange).status === 'string',\n );\n\nconst channel = createPushChannel<VcsState>({\n pushType: 'vcs-state',\n requestType: 'request-vcs-state',\n initial: EMPTY,\n parse: (msg) => {\n // Require a well-formed `changes` array; tolerate an absent branch/prs. A\n // malformed push is ignored (returns undefined) so the last good state stands.\n if (!isChangeArray(msg.changes)) return undefined;\n const branch =\n msg.branch && typeof msg.branch === 'object' ? (msg.branch as VcsBranch) : null;\n const prs = Array.isArray(msg.prs) ? (msg.prs as VcsPR[]) : [];\n return {\n changes: msg.changes,\n branch,\n prs,\n diffLoading: msg.diffLoading === true,\n };\n },\n});\n\n/** One-off read of the current source-control state. Returns the empty snapshot\n * until the host answers (or if the app lacks `vcs:read`). Use\n * {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */\nexport const getVcsState = (): VcsState => channel.get();\n\n/** Subscribe to source-control changes. Invoked immediately with the current\n * value, then on every host push (diff refresh, PR poll, branch change). Returns\n * an unsubscribe. */\nexport const onVcsStateChange = (listener: (state: VcsState) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook: the current source-control state, re-rendering on every change. */\nexport const useVcsState = (): VcsState => channel.use();\n\n// ---------------------------------------------------------------------------\n// Actions (Recipe B, §8.4). The app NAMES an intent and the HOST performs it —\n// the COW/journal stays in the kernel (§2/§4). `refreshDiff`/`refreshPRs` only\n// cause a host-side recompute + a fresh push (gated `vcs:read`, no new authority);\n// `resetWorkingTree` DISCARDS the user's unsaved work, gated by the first-party-only\n// `vcs:reset` — a fork can never hold it. The arm-then-confirm UX stays in the app;\n// the authority is gated host-side (§8.9), and the host requires `confirm:true` as\n// belt-and-braces (T22).\n// ---------------------------------------------------------------------------\n\n/** An error from a `vcs` action, carrying a machine-readable `.code`. */\nexport interface VcsActionError extends Error {\n code:\n | 'forbidden' // the frame lacks the required capability (`vcs:read` / `vcs:reset`)\n | 'invalid-params' // reset called without `confirm: true`\n | 'no-target' // there is no host contribute session (not in edit mode)\n | 'unknown';\n}\n\ntype VcsResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst vcsRequest = async (method: string, arg: Record<string, unknown> = {}): Promise<void> => {\n const res = (await protocolRequest('vcs', method, [arg])) as VcsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `vcs ${method} failed`) as VcsActionError;\n err.code = (res?.code as VcsActionError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.\n * Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshDiff = (): Promise<void> => vcsRequest('refreshDiff');\n\n/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated\n * `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshPRs = (): Promise<void> => vcsRequest('refreshPRs');\n\n/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —\n * irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the\n * gate. Requires `confirm: true` (host belt-and-braces). Rejects with a\n * {@link VcsActionError} (`.code`). */\nexport const resetWorkingTree = (): Promise<void> => vcsRequest('reset', { confirm: true });\n"],"mappings":"AAiBA,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AA4ChC,MAAM,QAAkB,EAAE,SAAS,CAAC,GAAG,QAAQ,MAAM,KAAK,CAAC,GAAG,aAAa,MAAM;AAEjF,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KACf,EAAE;AAAA,EACA,CAAC,MACC,CAAC,CAAC,KACF,OAAQ,EAAgB,SAAS,YACjC,OAAQ,EAAgB,WAAW;AACvC;AAEF,MAAM,UAAU,kBAA4B;AAAA,EAC1C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,cAAc,IAAI,OAAO,EAAG,QAAO;AACxC,UAAM,SACJ,IAAI,UAAU,OAAO,IAAI,WAAW,WAAY,IAAI,SAAuB;AAC7E,UAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,IAAK,IAAI,MAAkB,CAAC;AAC7D,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA,aAAa,IAAI,gBAAgB;AAAA,IACnC;AAAA,EACF;AACF,CAAC;AAKM,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAKhD,MAAM,mBAAmB,CAAC,aAC/B,QAAQ,SAAS,QAAQ;AAGpB,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAyBvD,MAAM,aAAa,OAAO,QAAgB,MAA+B,CAAC,MAAqB;AAC7F,QAAM,MAAO,MAAM,gBAAgB,OAAO,QAAQ,CAAC,GAAG,CAAC;AACvD,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,OAAO,MAAM,SAAS;AAC5D,QAAI,OAAQ,KAAK,QAAmC;AACpD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,cAAc,MAAqB,WAAW,aAAa;AAIjE,MAAM,aAAa,MAAqB,WAAW,YAAY;AAM/D,MAAM,mBAAmB,MAAqB,WAAW,SAAS,EAAE,SAAS,KAAK,CAAC;","names":[]}
|
package/dist/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
SDK_VERSION: () => SDK_VERSION
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const SDK_VERSION = "0.
|
|
24
|
+
const SDK_VERSION = "0.27.0";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
SDK_VERSION
|
package/dist/version.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.27.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
|
package/dist/version.d.cts
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.27.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@immediately-run/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Runtime SDK for code executing inside an immediately.run sandbox.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:immediately-run/immediately-run-sdk",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"check:circular": "node scripts/check-circular.mjs",
|
|
33
33
|
"api:check": "node scripts/check-api-stability.mjs",
|
|
34
34
|
"api:update": "node scripts/check-api-stability.mjs --update",
|
|
35
|
+
"verify": "npm run check:circular && npm test && npm run build && npm run api:check",
|
|
35
36
|
"docs": "typedoc --json docs/api.json && node scripts/gen-llms.mjs",
|
|
36
37
|
"prepublishOnly": "npm run check:circular && npm run build && npm run api:check"
|
|
37
38
|
},
|