sh3-core 0.20.2 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/BrandSlot.svelte +2 -2
  2. package/dist/actions/ctx-actions.svelte.test.js +2 -2
  3. package/dist/artifact.d.ts +2 -0
  4. package/dist/boot/satellitePayload.d.ts +2 -0
  5. package/dist/boot/satellitePayload.test.js +19 -0
  6. package/dist/build.d.ts +7 -1
  7. package/dist/build.js +22 -3
  8. package/dist/build.test.js +27 -1
  9. package/dist/createShell.js +34 -9
  10. package/dist/documents/browse.d.ts +20 -0
  11. package/dist/documents/browse.js +35 -0
  12. package/dist/documents/browse.test.js +125 -0
  13. package/dist/documents/config.d.ts +0 -4
  14. package/dist/documents/config.js +0 -8
  15. package/dist/documents/http-backend.d.ts +5 -0
  16. package/dist/documents/http-backend.js +25 -0
  17. package/dist/documents/http-backend.test.js +66 -0
  18. package/dist/documents/index.d.ts +1 -1
  19. package/dist/documents/index.js +1 -1
  20. package/dist/documents/types.d.ts +11 -0
  21. package/dist/host-entry.d.ts +1 -1
  22. package/dist/host-entry.js +1 -1
  23. package/dist/host.d.ts +1 -1
  24. package/dist/host.js +1 -1
  25. package/dist/layout/slotHostPool.svelte.js +2 -2
  26. package/dist/overlays/FloatFrame.svelte +1 -0
  27. package/dist/projects/session-state.svelte.d.ts +3 -0
  28. package/dist/projects/session-state.svelte.js +25 -0
  29. package/dist/projects/session-state.test.js +43 -2
  30. package/dist/projects-shard/ProjectsSection.svelte +14 -18
  31. package/dist/runtime/runVerb-shell.test.js +2 -2
  32. package/dist/runtime/runVerb.test.js +2 -2
  33. package/dist/sh3core-shard/appActions.js +5 -2
  34. package/dist/shards/activate-browse.test.js +2 -2
  35. package/dist/shards/activate-contributions.test.js +2 -2
  36. package/dist/shards/activate-error-isolation.test.js +3 -3
  37. package/dist/shards/activate-on-key-revoked.test.js +2 -2
  38. package/dist/shards/activate-runtime.test.js +2 -2
  39. package/dist/shards/activate.svelte.js +4 -4
  40. package/dist/shards/ctx-fetch.test.js +4 -4
  41. package/dist/shell-shard/verbs/xfer.js +13 -27
  42. package/dist/shell-shard/verbs/xfer.test.js +36 -25
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +1 -1
@@ -5,5 +5,5 @@ export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
5
5
  export { HttpDocumentBackend } from './http-backend';
6
6
  export { createDocumentHandle } from './handle';
7
7
  export { documentChanges } from './notifications';
8
- export { getActiveScopeId, getTenantId, getDocumentBackend, __setActiveScope, __setTenantId, __setDocumentBackend, __setScopeResolver, } from './config';
8
+ export { getActiveScopeId, getDocumentBackend, __setActiveScope, __setDocumentBackend, __setScopeResolver, } from './config';
9
9
  export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY, } from './sync-types';
@@ -192,6 +192,17 @@ export interface DocumentBackend {
192
192
  * Optional; only supported by HttpDocumentBackend in v1.
193
193
  */
194
194
  readBranch?(tenantId: string, shardId: string, path: string, origin: string): Promise<string | null>;
195
+ /**
196
+ * Server-side cross-scope transfer. When implemented, `transferBetweenScopes`
197
+ * delegates to this rather than doing client-side read+write, bypassing the
198
+ * per-shard app allowlist check. `srcPath` and `dstPath` are in the form
199
+ * `shardId/filePath`. Returns whether the destination document already existed.
200
+ */
201
+ xfer?(srcTenant: string, srcPath: string, dstTenant: string, dstPath: string, opts?: {
202
+ move?: boolean;
203
+ }): Promise<{
204
+ existed: boolean;
205
+ }>;
195
206
  }
196
207
  /**
197
208
  * Shard-facing document handle returned by `ctx.documents()`. Binds
@@ -1,6 +1,6 @@
1
1
  export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
2
2
  export type { BootstrapConfig } from './host';
3
- export { __setActiveScope, __setTenantId, __setDocumentBackend } from './host';
3
+ export { __setActiveScope, __setDocumentBackend } from './host';
4
4
  export type { Backend } from './state/types';
5
5
  export type { DocumentBackend } from './documents/types';
6
6
  export { HttpDocumentBackend } from './documents/http-backend';
@@ -6,7 +6,7 @@
6
6
  * should touch this path. Shards and apps must not import from here.
7
7
  */
8
8
  export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
9
- export { __setActiveScope, __setTenantId, __setDocumentBackend } from './host';
9
+ export { __setActiveScope, __setDocumentBackend } from './host';
10
10
  export { HttpDocumentBackend } from './documents/http-backend';
11
11
  export { IndexedDBDocumentBackend, MemoryDocumentBackend } from './documents/backends';
12
12
  export { __setEnvServerUrl } from './env/index';
package/dist/host.d.ts CHANGED
@@ -4,7 +4,7 @@ import { __setBackend } from './state/zones.svelte';
4
4
  import { setLocalOwner } from './auth/index';
5
5
  export { __setBackend };
6
6
  export { setLocalOwner };
7
- export { __setActiveScope, __setTenantId, __setDocumentBackend } from './documents/config';
7
+ export { __setActiveScope, __setDocumentBackend } from './documents/config';
8
8
  export declare function registerShard(shard: Parameters<typeof registerShardInternal>[0]): void;
9
9
  export { registerApp };
10
10
  export interface BootstrapConfig {
package/dist/host.js CHANGED
@@ -38,7 +38,7 @@ import { installWebEmitter } from './navigation/platform-web';
38
38
  import { returnToHome } from './apps/lifecycle';
39
39
  export { __setBackend };
40
40
  export { setLocalOwner };
41
- export { __setActiveScope, __setTenantId, __setDocumentBackend } from './documents/config';
41
+ export { __setActiveScope, __setDocumentBackend } from './documents/config';
42
42
  import { getActiveScopeId } from './documents/config';
43
43
  export function registerShard(shard) {
44
44
  registerShardInternal(shard);
@@ -75,7 +75,7 @@ function onViewRegistered(viewId, factory) {
75
75
  if (entry.handle !== undefined)
76
76
  return; // already mounted by a race
77
77
  entry.handle = factory.mount(entry.host, ctx);
78
- if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:')) {
78
+ if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:') || slotId.startsWith('standalone:')) {
79
79
  closableState[slotId] = true;
80
80
  }
81
81
  if ((_b = entry.handle) === null || _b === void 0 ? void 0 : _b.onResize) {
@@ -179,7 +179,7 @@ function createHost(slotId, viewId, label, meta) {
179
179
  },
180
180
  };
181
181
  entry.handle = factory === null || factory === void 0 ? void 0 : factory.mount(host, ctx);
182
- if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:')) {
182
+ if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:') || slotId.startsWith('standalone:')) {
183
183
  closableState[slotId] = true;
184
184
  }
185
185
  // The pool owns the ResizeObserver so its lifetime matches the
@@ -262,6 +262,7 @@
262
262
  title: entry.title,
263
263
  size: { w: entry.size.w, h: entry.size.h },
264
264
  activateShards: walkShardsForContent(entry.content),
265
+ projectId: sh3.getActiveScope().isProject ? sh3.getActiveScope().id : undefined,
265
266
  });
266
267
  floatManager.close(entry.id);
267
268
  } catch (err) {
@@ -1,3 +1,6 @@
1
+ export declare const PENDING_SCOPE_KEY = "sh3:pending-scope";
2
+ export declare function readPendingScope(): string | null;
3
+ export declare function switchProjectScope(projectId: string | null): void;
1
4
  export declare const sessionState: {
2
5
  activeProjectId: string | null;
3
6
  };
@@ -12,6 +12,31 @@
12
12
  */
13
13
  import { activeApp, breadcrumbApp } from '../apps/registry.svelte';
14
14
  import { unloadApp } from '../apps/lifecycle';
15
+ export const PENDING_SCOPE_KEY = 'sh3:pending-scope';
16
+ export function readPendingScope() {
17
+ if (typeof sessionStorage === 'undefined')
18
+ return null;
19
+ const raw = sessionStorage.getItem(PENDING_SCOPE_KEY);
20
+ if (!raw)
21
+ return null;
22
+ sessionStorage.removeItem(PENDING_SCOPE_KEY);
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ return typeof parsed.projectId === 'string' ? parsed.projectId : null;
26
+ }
27
+ catch (_a) {
28
+ return null;
29
+ }
30
+ }
31
+ export function switchProjectScope(projectId) {
32
+ if (projectId !== null) {
33
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ projectId }));
34
+ }
35
+ else {
36
+ sessionStorage.removeItem(PENDING_SCOPE_KEY);
37
+ }
38
+ window.location.reload();
39
+ }
15
40
  export const sessionState = $state({
16
41
  activeProjectId: null,
17
42
  });
@@ -1,5 +1,5 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { sessionState, setActiveProjectId } from './session-state.svelte';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { sessionState, setActiveProjectId, switchProjectScope, readPendingScope, PENDING_SCOPE_KEY, } from './session-state.svelte';
3
3
  import { breadcrumbApp, activeApp } from '../apps/registry.svelte';
4
4
  vi.mock('../apps/lifecycle', () => ({
5
5
  unloadApp: vi.fn(),
@@ -53,3 +53,44 @@ describe('sessionState.activeProjectId', () => {
53
53
  expect(lifecycle.unloadApp).not.toHaveBeenCalled();
54
54
  });
55
55
  });
56
+ describe('readPendingScope', () => {
57
+ beforeEach(() => sessionStorage.clear());
58
+ it('returns null when nothing is stored', () => {
59
+ expect(readPendingScope()).toBeNull();
60
+ });
61
+ it('returns the stored projectId and clears the key', () => {
62
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ projectId: 'proj-abc' }));
63
+ expect(readPendingScope()).toBe('proj-abc');
64
+ expect(sessionStorage.getItem(PENDING_SCOPE_KEY)).toBeNull();
65
+ });
66
+ it('returns null and clears a malformed entry', () => {
67
+ sessionStorage.setItem(PENDING_SCOPE_KEY, 'not-json');
68
+ expect(readPendingScope()).toBeNull();
69
+ expect(sessionStorage.getItem(PENDING_SCOPE_KEY)).toBeNull();
70
+ });
71
+ it('returns null if projectId field is not a string', () => {
72
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ other: 42 }));
73
+ expect(readPendingScope()).toBeNull();
74
+ });
75
+ });
76
+ describe('switchProjectScope', () => {
77
+ let reloadSpy;
78
+ beforeEach(() => {
79
+ sessionStorage.clear();
80
+ reloadSpy = vi.fn();
81
+ vi.stubGlobal('location', { reload: reloadSpy });
82
+ });
83
+ afterEach(() => vi.unstubAllGlobals());
84
+ it('writes projectId to sessionStorage and reloads', () => {
85
+ switchProjectScope('proj-abc');
86
+ const raw = sessionStorage.getItem(PENDING_SCOPE_KEY);
87
+ expect(JSON.parse(raw)).toEqual({ projectId: 'proj-abc' });
88
+ expect(reloadSpy).toHaveBeenCalledOnce();
89
+ });
90
+ it('removes the key for null (personal scope) and reloads', () => {
91
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ projectId: 'old' }));
92
+ switchProjectScope(null);
93
+ expect(sessionStorage.getItem(PENDING_SCOPE_KEY)).toBeNull();
94
+ expect(reloadSpy).toHaveBeenCalledOnce();
95
+ });
96
+ });
@@ -1,26 +1,13 @@
1
1
  <script lang="ts">
2
- /*
3
- * Projects section for Sh3Home.
4
- *
5
- * Renders the list of projects the current user is a member of as
6
- * selectable cards. Selecting a project sets sessionState.activeProjectId
7
- * (which then filters the apps grid via the appAllowlist) and binds any
8
- * subsequently launched app's documents to the project scope.
9
- */
10
-
11
2
  import { projectsState, openProjectManage } from './projectsShard.svelte';
12
- import { sessionState, setActiveProjectId } from '../projects/session-state.svelte';
3
+ import { sessionState, switchProjectScope } from '../projects/session-state.svelte';
13
4
  import { isAdmin } from '../auth/auth.svelte';
14
5
  import HomeSection from '../sh3core-shard/HomeSection.svelte';
6
+ import Button from '../primitives/Button.svelte';
15
7
 
16
8
  const visible = $derived(projectsState.projects.length > 0);
17
9
  const activeId = $derived(sessionState.activeProjectId);
18
10
  const elevated = $derived(isAdmin());
19
-
20
- function selectProject(id: string) {
21
- setActiveProjectId(activeId === id ? null : id);
22
- }
23
-
24
11
  function editProject(id: string, ev: MouseEvent) {
25
12
  ev.stopPropagation();
26
13
  const project = projectsState.projects.find((p) => p.id === id) ?? null;
@@ -28,7 +15,13 @@
28
15
  }
29
16
  </script>
30
17
 
31
- {#if visible}
18
+ {#if activeId !== null}
19
+ <HomeSection title="Project" persistKey="project-active">
20
+ <div class="leave-project">
21
+ <Button onclick={() => switchProjectScope(null)}>Leave project</Button>
22
+ </div>
23
+ </HomeSection>
24
+ {:else if visible}
32
25
  <HomeSection title="Projects" persistKey="projects">
33
26
  <div class="projects-grid">
34
27
  {#each projectsState.projects as project (project.id)}
@@ -36,8 +29,7 @@
36
29
  <button
37
30
  type="button"
38
31
  class="project-card"
39
- class:active={activeId === project.id}
40
- onclick={() => selectProject(project.id)}
32
+ onclick={() => switchProjectScope(project.id)}
41
33
  title={project.description ?? `${project.members.length} member${project.members.length === 1 ? '' : 's'}`}
42
34
  >
43
35
  <span class="project-name">{project.name}</span>
@@ -104,4 +96,8 @@
104
96
  }
105
97
  .project-name { font-weight: 600; font-size: 13px; }
106
98
  .project-meta { font-size: 11px; color: var(--sh3-fg-muted); }
99
+ .leave-project {
100
+ display: flex;
101
+ justify-content: flex-start;
102
+ }
107
103
  </style>
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { describe, it, expect, beforeEach } from 'vitest';
13
13
  import { MemoryDocumentBackend } from '../documents/backends';
14
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
14
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
15
15
  import { registerShard, activateShard, __resetShardRegistryForTest, } from '../shards/activate.svelte';
16
16
  import { __resetViewRegistryForTest } from '../shards/registry';
17
17
  import { __resetActionsRegistryForTest } from '../actions/registry';
@@ -25,7 +25,7 @@ describe('shell-shard programmatic verbs (integration)', () => {
25
25
  __resetActionsRegistryForTest();
26
26
  __resetAppRegistryForTest();
27
27
  __setDocumentBackend(new MemoryDocumentBackend());
28
- __setTenantId('tenant-test');
28
+ __setActiveScope('tenant-test');
29
29
  registerShard(shellShard);
30
30
  await activateShard('shell');
31
31
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, __resetShardRegistryForTest, } from '../shards/activate.svelte';
5
5
  import { __resetViewRegistryForTest } from '../shards/registry';
6
6
  import { runVerbProgrammatic } from './runVerb';
@@ -19,7 +19,7 @@ describe('runVerbProgrammatic', () => {
19
19
  __resetShardRegistryForTest();
20
20
  __resetViewRegistryForTest();
21
21
  __setDocumentBackend(new MemoryDocumentBackend());
22
- __setTenantId('tenant-test');
22
+ __setActiveScope('tenant-test');
23
23
  });
24
24
  it('rejects on unknown shard', async () => {
25
25
  await expect(runVerbProgrammatic('missing', 'echo', [])).rejects.toThrow('unknown shard: missing');
@@ -30,6 +30,7 @@ import AppInfoView from './AppInfoView.svelte';
30
30
  import { spawnSatellite } from '../sh3Api/window';
31
31
  import { activeApp, getActiveApp } from '../apps/registry.svelte';
32
32
  import { returnToHome } from '../apps/lifecycle';
33
+ import { sessionState } from '../projects/session-state.svelte';
33
34
  const isTauri = typeof globalThis.__TAURI_INTERNALS__ !== 'undefined';
34
35
  export function computeAppActionDisabled(g) {
35
36
  return !g.admin || g.builtin;
@@ -111,7 +112,7 @@ async function runCheckUpdate(_ctx) {
111
112
  modalManager.open(AppUpdateAvailableModal, props);
112
113
  }
113
114
  function runPopOut(_ctx) {
114
- var _a;
115
+ var _a, _b;
115
116
  const ref = readSelection();
116
117
  if (!ref)
117
118
  return;
@@ -122,10 +123,11 @@ function runPopOut(_ctx) {
122
123
  kind: 'app',
123
124
  appId: ref.appId,
124
125
  activateShards: (_a = manifest.requiredShards) !== null && _a !== void 0 ? _a : [],
126
+ projectId: (_b = sessionState.activeProjectId) !== null && _b !== void 0 ? _b : undefined,
125
127
  });
126
128
  }
127
129
  async function runPopOutCurrent(_ctx) {
128
- var _a;
130
+ var _a, _b;
129
131
  const current = getActiveApp();
130
132
  if (!current)
131
133
  return;
@@ -136,6 +138,7 @@ async function runPopOutCurrent(_ctx) {
136
138
  kind: 'app',
137
139
  appId,
138
140
  activateShards: requiredShards,
141
+ projectId: (_b = sessionState.activeProjectId) !== null && _b !== void 0 ? _b : undefined,
139
142
  });
140
143
  }
141
144
  function runUninstall(_ctx) {
@@ -1,13 +1,13 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
5
5
  import { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from '../documents/types';
6
6
  describe('ctx.browse permission gating', () => {
7
7
  beforeEach(() => {
8
8
  __resetShardRegistryForTest();
9
9
  __setDocumentBackend(new MemoryDocumentBackend());
10
- __setTenantId('tenant-a');
10
+ __setActiveScope('tenant-a');
11
11
  });
12
12
  it('is undefined when no documents permission is declared', async () => {
13
13
  let captured = null;
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, deactivateShard, __resetShardRegistryForTest, } from './activate.svelte';
5
5
  import { __resetContributionsForTest, list, listPoints } from '../contributions';
6
6
  describe('ctx.contributions', () => {
@@ -8,7 +8,7 @@ describe('ctx.contributions', () => {
8
8
  __resetShardRegistryForTest();
9
9
  __resetContributionsForTest();
10
10
  __setDocumentBackend(new MemoryDocumentBackend());
11
- __setTenantId('tenant-a');
11
+ __setActiveScope('tenant-a');
12
12
  });
13
13
  it('is always present on ShardContext (no permission required)', async () => {
14
14
  let captured = null;
@@ -1,12 +1,12 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, registeredShards, activeShards, __resetShardRegistryForTest, erroredShards, } from './activate.svelte';
5
5
  describe('erroredShards map', () => {
6
6
  beforeEach(() => {
7
7
  __resetShardRegistryForTest();
8
8
  __setDocumentBackend(new MemoryDocumentBackend());
9
- __setTenantId('tenant-a');
9
+ __setActiveScope('tenant-a');
10
10
  });
11
11
  it('is empty after reset', () => {
12
12
  expect(erroredShards.size).toBe(0);
@@ -21,7 +21,7 @@ describe('activateShard — unwind on activation failure', () => {
21
21
  beforeEach(() => {
22
22
  __resetShardRegistryForTest();
23
23
  __setDocumentBackend(new MemoryDocumentBackend());
24
- __setTenantId('tenant-a');
24
+ __setActiveScope('tenant-a');
25
25
  });
26
26
  it('unwinds partial state and records the error when activate throws', async () => {
27
27
  const shard = {
@@ -1,13 +1,13 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, deactivateShard, __resetShardRegistryForTest } from './activate.svelte';
5
5
  import { emit } from '../keys/revocation-bus.svelte';
6
6
  describe('onKeyRevoked hook wiring', () => {
7
7
  beforeEach(() => {
8
8
  __resetShardRegistryForTest();
9
9
  __setDocumentBackend(new MemoryDocumentBackend());
10
- __setTenantId('tenant-a');
10
+ __setActiveScope('tenant-a');
11
11
  });
12
12
  it('fires onKeyRevoked when the bus emits for the shard', async () => {
13
13
  const received = [];
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
5
5
  import { __resetViewRegistryForTest } from './registry';
6
6
  function programmaticVerb(name, summary, body) {
@@ -22,7 +22,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
22
22
  __resetShardRegistryForTest();
23
23
  __resetViewRegistryForTest();
24
24
  __setDocumentBackend(new MemoryDocumentBackend());
25
- __setTenantId('tenant-test');
25
+ __setActiveScope('tenant-test');
26
26
  });
27
27
  it('listVerbs returns every verb across active shards with shardId', async () => {
28
28
  registerShard({
@@ -19,7 +19,7 @@
19
19
  import { sh3 } from '../sh3Runtime.svelte';
20
20
  import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb } from './registry';
21
21
  import { makeSh3Api } from '../sh3Api/headless';
22
- import { createDocumentHandle, getTenantId, getDocumentBackend, getActiveScopeId } from '../documents';
22
+ import { createDocumentHandle, getDocumentBackend, getActiveScopeId } from '../documents';
23
23
  import { fetchEnvState, putEnvState } from '../env/client';
24
24
  import { getEnvServerUrl } from '../env/index';
25
25
  import { apiFetch } from '../transport/apiFetch';
@@ -153,7 +153,7 @@ export async function activateShard(id, opts) {
153
153
  };
154
154
  const hasBrowse = (_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_DOCUMENTS_BROWSE);
155
155
  const browseCap = hasBrowse
156
- ? createBrowseCapability(() => getTenantId(), getDocumentBackend(), {
156
+ ? createBrowseCapability(getActiveScopeId, getDocumentBackend(), {
157
157
  canRead: (_c = (_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_READ)) !== null && _c !== void 0 ? _c : false,
158
158
  canWrite: (_e = (_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_WRITE)) !== null && _e !== void 0 ? _e : false,
159
159
  })
@@ -225,7 +225,7 @@ export async function activateShard(id, opts) {
225
225
  return checkIsAdmin();
226
226
  },
227
227
  get tenantId() {
228
- return getTenantId();
228
+ return getActiveScopeId();
229
229
  },
230
230
  getScope: () => { var _a; return (_a = scopeResolver === null || scopeResolver === void 0 ? void 0 : scopeResolver()) !== null && _a !== void 0 ? _a : 'tenant'; },
231
231
  zones: ((_f = shard.manifest.permissions) === null || _f === void 0 ? void 0 : _f.includes(PERMISSION_STATE_MANAGE))
@@ -235,7 +235,7 @@ export async function activateShard(id, opts) {
235
235
  documentPicker: browseCap
236
236
  ? createDocumentPicker(() => browseCap.listDocuments())
237
237
  : createDocumentPicker(async () => {
238
- const docs = await getDocumentBackend().list(getTenantId(), id);
238
+ const docs = await getDocumentBackend().list(getActiveScopeId(), id);
239
239
  return docs.map(d => (Object.assign(Object.assign({}, d), { shardId: id })));
240
240
  }),
241
241
  keys: ((_g = shard.manifest.permissions) === null || _g === void 0 ? void 0 : _g.includes(PERMISSION_KEYS_MINT))
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { __setEnvServerUrl } from '../env/index';
5
5
  import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
6
6
  import { __resetViewRegistryForTest } from './registry';
@@ -11,7 +11,7 @@ describe('ctx.fetch', () => {
11
11
  __resetShardRegistryForTest();
12
12
  __resetViewRegistryForTest();
13
13
  __setDocumentBackend(new MemoryDocumentBackend());
14
- __setTenantId('tenant-test');
14
+ __setActiveScope('tenant-test');
15
15
  __setEnvServerUrl('https://example.com');
16
16
  });
17
17
  afterEach(() => {
@@ -69,7 +69,7 @@ describe('ctx.serverUrl', () => {
69
69
  __resetShardRegistryForTest();
70
70
  __resetViewRegistryForTest();
71
71
  __setDocumentBackend(new MemoryDocumentBackend());
72
- __setTenantId('tenant-test');
72
+ __setActiveScope('tenant-test');
73
73
  __setEnvServerUrl('https://example.com');
74
74
  });
75
75
  afterEach(() => {
@@ -100,7 +100,7 @@ describe('ctx.resolveUrl', () => {
100
100
  __resetShardRegistryForTest();
101
101
  __resetViewRegistryForTest();
102
102
  __setDocumentBackend(new MemoryDocumentBackend());
103
- __setTenantId('tenant-test');
103
+ __setActiveScope('tenant-test');
104
104
  __setEnvServerUrl('https://example.com');
105
105
  });
106
106
  afterEach(() => {
@@ -3,7 +3,8 @@ export const xferVerb = {
3
3
  name: 'xfer',
4
4
  summary: [
5
5
  'Transfer docs across scopes. Usage: xfer [-R] [-C] <src> <dst>',
6
- ' Scopes: @me | @project-<slug> (e.g. @project-acme:notes/draft.md)',
6
+ ' Scopes: @me | @project-<slug> (e.g. @me:notes/draft.md, @project-acme:notes/draft.md)',
7
+ ' Either side may be @me or @project-<slug>; bare paths resolve to the active scope.',
7
8
  ' -R recursive (src is a folder prefix)',
8
9
  ' -C copy only, do not delete source',
9
10
  ].join('\n'),
@@ -14,12 +15,10 @@ export const xferVerb = {
14
15
  ctx.scrollback.push({ kind: 'status', text: 'xfer: document capability not available', level: 'error', ts });
15
16
  return;
16
17
  }
17
- const scope = ctx.sh3.getActiveScope();
18
- if (!scope.isProject) {
19
- ctx.scrollback.push({ kind: 'status', text: 'xfer: only available when a project scope is active', level: 'error', ts });
18
+ if (!ctx.docs.transferBetweenScopes) {
19
+ ctx.scrollback.push({ kind: 'status', text: 'xfer: write permission not granted', level: 'error', ts });
20
20
  return;
21
21
  }
22
- // Parse flags
23
22
  let recursive = false;
24
23
  let copy = false;
25
24
  const positional = [];
@@ -38,16 +37,13 @@ export const xferVerb = {
38
37
  ctx.scrollback.push({ kind: 'status', text: 'usage: xfer [-R] [-C] <src> <dst>', level: 'error', ts });
39
38
  return;
40
39
  }
41
- if (!ctx.docs.transferToScope) {
42
- ctx.scrollback.push({ kind: 'status', text: 'xfer: write permission not granted', level: 'error', ts });
43
- return;
44
- }
45
40
  const srcParsed = parseScopePath(positional[0]);
46
41
  const dstParsed = parseScopePath(positional[1]);
47
42
  if (!srcParsed || !dstParsed) {
48
43
  ctx.scrollback.push({ kind: 'status', text: 'xfer: invalid path', level: 'error', ts });
49
44
  return;
50
45
  }
46
+ const scope = ctx.sh3.getActiveScope();
51
47
  let srcTenant;
52
48
  let dstTenant;
53
49
  try {
@@ -58,33 +54,23 @@ export const xferVerb = {
58
54
  ctx.scrollback.push({ kind: 'status', text: `xfer: ${e.message}`, level: 'error', ts });
59
55
  return;
60
56
  }
61
- // transferToScope always reads from the active tenant; reject if src doesn't match.
62
- if (srcTenant !== scope.id) {
63
- ctx.scrollback.push({
64
- kind: 'status',
65
- text: 'xfer: source must be the active project scope in v1 — switch to the source scope first',
66
- level: 'error',
67
- ts,
68
- });
69
- return;
70
- }
71
- const opts = { delete: !copy, targetShardId: dstParsed.shardId };
57
+ const moveOpts = { delete: !copy };
72
58
  if (!recursive) {
73
59
  if (!srcParsed.path) {
74
60
  ctx.scrollback.push({ kind: 'status', text: 'xfer: path required (use -R for folder recursion)', level: 'error', ts });
75
61
  return;
76
62
  }
77
- await ctx.docs.transferToScope(srcParsed.shardId, srcParsed.path, dstTenant, opts);
63
+ if (srcTenant === dstTenant && srcParsed.shardId === dstParsed.shardId && srcParsed.path === dstParsed.path) {
64
+ ctx.scrollback.push({ kind: 'status', text: 'xfer: source and destination are the same', level: 'error', ts });
65
+ return;
66
+ }
67
+ await ctx.docs.transferBetweenScopes(srcTenant, srcParsed.shardId, srcParsed.path, dstTenant, dstParsed.shardId, dstParsed.path, moveOpts);
78
68
  const verb = copy ? 'copied' : 'moved';
79
69
  ctx.scrollback.push({ kind: 'status', text: `xfer: ${verb} ${positional[0]} → ${positional[1]}`, level: 'info', ts });
80
70
  return;
81
71
  }
82
- // Recursive: list all docs in srcTenant matching the prefix
83
- // transferToScope uses getTenantId() (active scope) — to read from srcTenant
84
- // we rely on the src scope being the active tenant or the capability seeing it.
85
- // For v1 we use listDocuments (active tenant) and filter by shard + prefix.
86
72
  const prefix = srcParsed.path;
87
- const allDocs = await ctx.docs.listDocuments();
73
+ const allDocs = await ctx.docs.listDocumentsIn(srcTenant);
88
74
  const matching = allDocs.filter((d) => d.shardId === srcParsed.shardId && (!prefix || d.path.startsWith(prefix)));
89
75
  if (matching.length === 0) {
90
76
  ctx.scrollback.push({ kind: 'status', text: `xfer: no documents found under ${positional[0]}`, level: 'info', ts });
@@ -92,7 +78,7 @@ export const xferVerb = {
92
78
  }
93
79
  let count = 0;
94
80
  for (const doc of matching) {
95
- await ctx.docs.transferToScope(doc.shardId, doc.path, dstTenant, opts);
81
+ await ctx.docs.transferBetweenScopes(srcTenant, doc.shardId, doc.path, dstTenant, dstParsed.shardId, doc.path, moveOpts);
96
82
  count++;
97
83
  }
98
84
  const verb = copy ? 'copied' : 'moved';