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.
- package/dist/BrandSlot.svelte +2 -2
- package/dist/actions/ctx-actions.svelte.test.js +2 -2
- package/dist/artifact.d.ts +2 -0
- package/dist/boot/satellitePayload.d.ts +2 -0
- package/dist/boot/satellitePayload.test.js +19 -0
- package/dist/build.d.ts +7 -1
- package/dist/build.js +22 -3
- package/dist/build.test.js +27 -1
- package/dist/createShell.js +34 -9
- package/dist/documents/browse.d.ts +20 -0
- package/dist/documents/browse.js +35 -0
- package/dist/documents/browse.test.js +125 -0
- package/dist/documents/config.d.ts +0 -4
- package/dist/documents/config.js +0 -8
- package/dist/documents/http-backend.d.ts +5 -0
- package/dist/documents/http-backend.js +25 -0
- package/dist/documents/http-backend.test.js +66 -0
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/index.js +1 -1
- package/dist/documents/types.d.ts +11 -0
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/layout/slotHostPool.svelte.js +2 -2
- package/dist/overlays/FloatFrame.svelte +1 -0
- package/dist/projects/session-state.svelte.d.ts +3 -0
- package/dist/projects/session-state.svelte.js +25 -0
- package/dist/projects/session-state.test.js +43 -2
- package/dist/projects-shard/ProjectsSection.svelte +14 -18
- package/dist/runtime/runVerb-shell.test.js +2 -2
- package/dist/runtime/runVerb.test.js +2 -2
- package/dist/sh3core-shard/appActions.js +5 -2
- package/dist/shards/activate-browse.test.js +2 -2
- package/dist/shards/activate-contributions.test.js +2 -2
- package/dist/shards/activate-error-isolation.test.js +3 -3
- package/dist/shards/activate-on-key-revoked.test.js +2 -2
- package/dist/shards/activate-runtime.test.js +2 -2
- package/dist/shards/activate.svelte.js +4 -4
- package/dist/shards/ctx-fetch.test.js +4 -4
- package/dist/shell-shard/verbs/xfer.js +13 -27
- package/dist/shell-shard/verbs/xfer.test.js +36 -25
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/documents/index.js
CHANGED
|
@@ -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,
|
|
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
|
package/dist/host-entry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
|
|
2
2
|
export type { BootstrapConfig } from './host';
|
|
3
|
-
export { __setActiveScope,
|
|
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';
|
package/dist/host-entry.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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';
|