sh3-core 0.19.6 → 0.20.2
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/app/admin/AuthSettingsView.svelte +3 -9
- package/dist/app/admin/MountsView.svelte +276 -0
- package/dist/app/admin/MountsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +6 -6
- package/dist/app/admin/UsersView.svelte +103 -7
- package/dist/app/admin/adminApp.js +1 -0
- package/dist/app/admin/adminShard.svelte.js +10 -0
- package/dist/apps/lifecycle.js +1 -0
- package/dist/apps/types.d.ts +7 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +1 -0
- package/dist/assets/icons.svg +5 -0
- package/dist/auth/admin-users.svelte.js +2 -1
- package/dist/auth/auth.svelte.d.ts +4 -5
- package/dist/auth/auth.svelte.js +5 -6
- package/dist/auth/types.d.ts +0 -2
- package/dist/chrome/CompactChrome.svelte +25 -6
- package/dist/chrome/FloatsSheet.svelte +7 -32
- package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
- package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
- package/dist/chrome/MenuSheet.svelte +154 -148
- package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
- package/dist/chrome/MenuSheet.svelte.test.js +24 -12
- package/dist/createShell.js +32 -21
- package/dist/createShell.remoteAuth.test.js +9 -3
- package/dist/documents/backends.d.ts +12 -0
- package/dist/documents/backends.js +230 -3
- package/dist/documents/backends.test.js +147 -1
- package/dist/documents/browse.d.ts +18 -1
- package/dist/documents/browse.js +40 -7
- package/dist/documents/browse.test.js +35 -35
- package/dist/documents/config.d.ts +6 -0
- package/dist/documents/config.js +18 -1
- package/dist/documents/handle.js +65 -17
- package/dist/documents/handle.test.js +88 -1
- package/dist/documents/http-backend.d.ts +6 -0
- package/dist/documents/http-backend.js +71 -2
- package/dist/documents/http-backend.test.js +51 -1
- package/dist/documents/index.d.ts +2 -2
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.d.ts +4 -2
- package/dist/documents/picker-api.test.d.ts +1 -1
- package/dist/documents/picker-api.test.js +89 -59
- package/dist/documents/picker-primitive.d.ts +4 -0
- package/dist/documents/picker-primitive.js +27 -29
- package/dist/documents/types.d.ts +93 -19
- package/dist/documents/types.js +6 -0
- package/dist/layout/presets.test.js +4 -4
- package/dist/layout/types.d.ts +1 -1
- package/dist/layouts-shard/LayoutsSection.svelte +3 -16
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
- package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte +27 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
- package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
- package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
- package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
- package/dist/primitives/widgets/PickerList.svelte +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +419 -35
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
- package/dist/projects-shard/DeleteProjectDialog.svelte +32 -1
- package/dist/projects-shard/ProjectManage.svelte +197 -28
- package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
- package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
- package/dist/projects-shard/ProjectsSection.svelte +3 -16
- package/dist/projects-shard/projectsApi.js +2 -1
- package/dist/registry/permission-descriptions.js +4 -0
- package/dist/server-shard/types.d.ts +21 -0
- package/dist/sh3Api/headless.js +10 -0
- package/dist/sh3core-shard/HomeSection.svelte +107 -0
- package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
- package/dist/sh3core-shard/Sh3Home.svelte +9 -23
- package/dist/shards/activate.svelte.d.ts +4 -0
- package/dist/shards/activate.svelte.js +11 -3
- package/dist/shards/types.d.ts +7 -0
- package/dist/shell-shard/Terminal.svelte +4 -1
- package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
- package/dist/shell-shard/dispatch.d.ts +2 -0
- package/dist/shell-shard/dispatch.js +2 -0
- package/dist/shell-shard/manifest.js +7 -1
- package/dist/shell-shard/shellShard.svelte.js +1 -1
- package/dist/shell-shard/tenant-fs-client.js +2 -1
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +35 -0
- package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cat.test.js +49 -0
- package/dist/shell-shard/verbs/index.js +12 -0
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +48 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +64 -0
- package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
- package/dist/shell-shard/verbs/mkdir.js +30 -0
- package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mkdir.test.js +48 -0
- package/dist/shell-shard/verbs/mv.d.ts +2 -0
- package/dist/shell-shard/verbs/mv.js +33 -0
- package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mv.test.js +55 -0
- package/dist/shell-shard/verbs/rm.d.ts +2 -0
- package/dist/shell-shard/verbs/rm.js +28 -0
- package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
- package/dist/shell-shard/verbs/rm.test.js +47 -0
- package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
- package/dist/shell-shard/verbs/scope-parse.js +33 -0
- package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
- package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
- package/dist/shell-shard/verbs/xfer.d.ts +2 -0
- package/dist/shell-shard/verbs/xfer.js +101 -0
- package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
- package/dist/shell-shard/verbs/xfer.test.js +96 -0
- package/dist/transport/apiFetch.js +12 -5
- package/dist/verbs/types.d.ts +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -91,6 +91,23 @@ export interface BrowseCapability {
|
|
|
91
91
|
* `typeof ctx.browse.deleteFrom === 'function'`.
|
|
92
92
|
*/
|
|
93
93
|
deleteFrom?(shardId: string, path: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Copy or move a document from the active tenant to another scope.
|
|
96
|
+
* Available only when the caller declares both `documents:browse` and
|
|
97
|
+
* `documents:write`. Used for transferring documents between a user's
|
|
98
|
+
* personal scope and a project scope (or vice versa).
|
|
99
|
+
*
|
|
100
|
+
* When `opts.delete` is true, the source document is removed after
|
|
101
|
+
* copying (effectively a move/transfer). Default: false (copy only).
|
|
102
|
+
*
|
|
103
|
+
* Absent (undefined) on the capability object when `documents:write`
|
|
104
|
+
* is not declared; feature-detect with
|
|
105
|
+
* `typeof ctx.browse.transferToScope === 'function'`.
|
|
106
|
+
*/
|
|
107
|
+
transferToScope?(shardId: string, path: string, targetScope: string, opts?: {
|
|
108
|
+
targetShardId?: string;
|
|
109
|
+
delete?: boolean;
|
|
110
|
+
}): Promise<void>;
|
|
94
111
|
}
|
|
95
112
|
export interface BrowseCapabilityOptions {
|
|
96
113
|
/** When true, the returned capability exposes `readFrom`. */
|
|
@@ -98,4 +115,4 @@ export interface BrowseCapabilityOptions {
|
|
|
98
115
|
/** When true, the returned capability exposes `writeTo`. */
|
|
99
116
|
canWrite: boolean;
|
|
100
117
|
}
|
|
101
|
-
export declare function createBrowseCapability(
|
|
118
|
+
export declare function createBrowseCapability(getTenantId: () => string, backend: DocumentBackend, options?: BrowseCapabilityOptions): BrowseCapability;
|
package/dist/documents/browse.js
CHANGED
|
@@ -4,33 +4,39 @@
|
|
|
4
4
|
* Exposed on ShardContext as `ctx.browse` when the shard declares the
|
|
5
5
|
* 'documents:browse' permission. Read-only: writes still flow through
|
|
6
6
|
* the owning shard's own ctx.documents() handle.
|
|
7
|
+
*
|
|
8
|
+
* Tenant id is resolved lazily via a callback so that the browse
|
|
9
|
+
* capability automatically follows the active scope (personal tenant
|
|
10
|
+
* vs. active project). The callback is wired by the framework at
|
|
11
|
+
* shard activation time.
|
|
7
12
|
*/
|
|
8
13
|
import { documentChanges } from './notifications';
|
|
9
|
-
export function createBrowseCapability(
|
|
14
|
+
export function createBrowseCapability(getTenantId, backend, options = { canRead: false, canWrite: false }) {
|
|
10
15
|
const capability = {
|
|
11
|
-
listDocuments: () => backend.listAllDocuments(
|
|
12
|
-
listShards: () => backend.listAllShards(
|
|
16
|
+
listDocuments: () => backend.listAllDocuments(getTenantId()),
|
|
17
|
+
listShards: () => backend.listAllShards(getTenantId()),
|
|
13
18
|
watchDocuments: (callback) => documentChanges.subscribe((change) => {
|
|
14
|
-
if (change.tenantId !==
|
|
19
|
+
if (change.tenantId !== getTenantId())
|
|
15
20
|
return;
|
|
16
21
|
callback(change);
|
|
17
22
|
}),
|
|
18
23
|
};
|
|
19
24
|
if (options.canRead) {
|
|
20
|
-
capability.readFrom = (shardId, path) => backend.read(
|
|
25
|
+
capability.readFrom = (shardId, path) => backend.read(getTenantId(), shardId, path);
|
|
21
26
|
capability.statusFrom = async (shardId, path) => {
|
|
22
27
|
if (!backend.readMeta)
|
|
23
28
|
return null;
|
|
24
|
-
return backend.readMeta(
|
|
29
|
+
return backend.readMeta(getTenantId(), shardId, path);
|
|
25
30
|
};
|
|
26
31
|
capability.readBranchFrom = async (shardId, path, origin) => {
|
|
27
32
|
if (!backend.readBranch)
|
|
28
33
|
return null;
|
|
29
|
-
return backend.readBranch(
|
|
34
|
+
return backend.readBranch(getTenantId(), shardId, path, origin);
|
|
30
35
|
};
|
|
31
36
|
}
|
|
32
37
|
if (options.canWrite) {
|
|
33
38
|
capability.writeTo = async (shardId, path, content) => {
|
|
39
|
+
const tenantId = getTenantId();
|
|
34
40
|
const existed = await backend.exists(tenantId, shardId, path);
|
|
35
41
|
await backend.write(tenantId, shardId, path, content);
|
|
36
42
|
documentChanges.emit({
|
|
@@ -43,6 +49,7 @@ export function createBrowseCapability(tenantId, backend, options = { canRead: f
|
|
|
43
49
|
capability.resolveConflictFrom = async (shardId, path, choice) => {
|
|
44
50
|
if (!backend.resolve)
|
|
45
51
|
throw new Error('Backend does not support resolveConflict');
|
|
52
|
+
const tenantId = getTenantId();
|
|
46
53
|
await backend.resolve(tenantId, shardId, path, choice);
|
|
47
54
|
documentChanges.emit({ type: 'update', path, tenantId, shardId });
|
|
48
55
|
};
|
|
@@ -50,6 +57,7 @@ export function createBrowseCapability(tenantId, backend, options = { canRead: f
|
|
|
50
57
|
if ((opts === null || opts === void 0 ? void 0 : opts.newShardId) !== undefined) {
|
|
51
58
|
throw new Error('Cross-shard move is not yet supported (ADR-018 amendment 2026-04-27)');
|
|
52
59
|
}
|
|
60
|
+
const tenantId = getTenantId();
|
|
53
61
|
await backend.rename(tenantId, shardId, oldPath, newPath);
|
|
54
62
|
documentChanges.emit({
|
|
55
63
|
type: 'rename',
|
|
@@ -60,12 +68,37 @@ export function createBrowseCapability(tenantId, backend, options = { canRead: f
|
|
|
60
68
|
});
|
|
61
69
|
};
|
|
62
70
|
capability.deleteFrom = async (shardId, path) => {
|
|
71
|
+
const tenantId = getTenantId();
|
|
63
72
|
const existed = await backend.exists(tenantId, shardId, path);
|
|
64
73
|
await backend.delete(tenantId, shardId, path);
|
|
65
74
|
if (existed) {
|
|
66
75
|
documentChanges.emit({ type: 'delete', path, tenantId, shardId });
|
|
67
76
|
}
|
|
68
77
|
};
|
|
78
|
+
capability.transferToScope = async (shardId, path, targetScope, opts) => {
|
|
79
|
+
var _a;
|
|
80
|
+
const tenantId = getTenantId();
|
|
81
|
+
if (targetScope === tenantId) {
|
|
82
|
+
throw new Error('targetScope must differ from the active tenant');
|
|
83
|
+
}
|
|
84
|
+
const targetShard = (_a = opts === null || opts === void 0 ? void 0 : opts.targetShardId) !== null && _a !== void 0 ? _a : shardId;
|
|
85
|
+
const content = await backend.read(tenantId, shardId, path);
|
|
86
|
+
if (content === null) {
|
|
87
|
+
throw new Error(`Document not found at ${shardId}/${path} in scope ${tenantId}`);
|
|
88
|
+
}
|
|
89
|
+
const existed = await backend.exists(targetScope, targetShard, path);
|
|
90
|
+
await backend.write(targetScope, targetShard, path, content);
|
|
91
|
+
documentChanges.emit({
|
|
92
|
+
type: existed ? 'update' : 'create',
|
|
93
|
+
path,
|
|
94
|
+
tenantId: targetScope,
|
|
95
|
+
shardId: targetShard,
|
|
96
|
+
});
|
|
97
|
+
if (opts === null || opts === void 0 ? void 0 : opts.delete) {
|
|
98
|
+
await backend.delete(tenantId, shardId, path);
|
|
99
|
+
documentChanges.emit({ type: 'delete', path, tenantId, shardId });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
69
102
|
}
|
|
70
103
|
return capability;
|
|
71
104
|
}
|
|
@@ -7,7 +7,7 @@ describe('BrowseCapability', () => {
|
|
|
7
7
|
const be = new MemoryDocumentBackend();
|
|
8
8
|
await be.write('t1', 'a', 'x.txt', '1');
|
|
9
9
|
await be.write('t1', 'b', 'y.txt', '22');
|
|
10
|
-
const browse = createBrowseCapability('t1', be);
|
|
10
|
+
const browse = createBrowseCapability(() => 't1', be);
|
|
11
11
|
const docs = await browse.listDocuments();
|
|
12
12
|
expect(docs.map((d) => d.shardId).sort()).toEqual(['a', 'b']);
|
|
13
13
|
});
|
|
@@ -15,12 +15,12 @@ describe('BrowseCapability', () => {
|
|
|
15
15
|
const be = new MemoryDocumentBackend();
|
|
16
16
|
await be.write('t1', 'a', 'x.txt', '1');
|
|
17
17
|
await be.write('t1', 'b', 'y.txt', '2');
|
|
18
|
-
const browse = createBrowseCapability('t1', be);
|
|
18
|
+
const browse = createBrowseCapability(() => 't1', be);
|
|
19
19
|
expect((await browse.listShards()).sort()).toEqual(['a', 'b']);
|
|
20
20
|
});
|
|
21
21
|
it('watchDocuments fires with shardId on tenant-wide emits; filters other tenants', () => {
|
|
22
22
|
const be = new MemoryDocumentBackend();
|
|
23
|
-
const browse = createBrowseCapability('t1', be);
|
|
23
|
+
const browse = createBrowseCapability(() => 't1', be);
|
|
24
24
|
const cb = vi.fn();
|
|
25
25
|
const unsub = browse.watchDocuments(cb);
|
|
26
26
|
documentChanges.emit({ type: 'create', path: 'f.txt', tenantId: 't1', shardId: 's1' });
|
|
@@ -31,7 +31,7 @@ describe('BrowseCapability', () => {
|
|
|
31
31
|
});
|
|
32
32
|
it('watchDocuments unsubscribe stops callbacks', () => {
|
|
33
33
|
const be = new MemoryDocumentBackend();
|
|
34
|
-
const browse = createBrowseCapability('t1', be);
|
|
34
|
+
const browse = createBrowseCapability(() => 't1', be);
|
|
35
35
|
const cb = vi.fn();
|
|
36
36
|
const unsub = browse.watchDocuments(cb);
|
|
37
37
|
unsub();
|
|
@@ -41,46 +41,46 @@ describe('BrowseCapability', () => {
|
|
|
41
41
|
describe('readFrom (documents:read gate)', () => {
|
|
42
42
|
it('is absent when canRead is false', () => {
|
|
43
43
|
const be = new MemoryDocumentBackend();
|
|
44
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: false });
|
|
44
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: false });
|
|
45
45
|
expect(browse.readFrom).toBeUndefined();
|
|
46
46
|
});
|
|
47
47
|
it('is present when canRead is true', () => {
|
|
48
48
|
const be = new MemoryDocumentBackend();
|
|
49
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
49
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
50
50
|
expect(typeof browse.readFrom).toBe('function');
|
|
51
51
|
});
|
|
52
52
|
it('reads content from a foreign shard namespace', async () => {
|
|
53
53
|
const be = new MemoryDocumentBackend();
|
|
54
54
|
await be.write('t1', 'other', 'notes/ideas.md', 'hello');
|
|
55
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
55
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
56
56
|
expect(await browse.readFrom('other', 'notes/ideas.md')).toBe('hello');
|
|
57
57
|
});
|
|
58
58
|
it('returns null when the foreign document does not exist', async () => {
|
|
59
59
|
const be = new MemoryDocumentBackend();
|
|
60
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
60
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
61
61
|
expect(await browse.readFrom('other', 'nope.txt')).toBeNull();
|
|
62
62
|
});
|
|
63
63
|
it('never crosses tenants: a t1 capability cannot read t2 docs', async () => {
|
|
64
64
|
const be = new MemoryDocumentBackend();
|
|
65
65
|
await be.write('t2', 's', 'secret.txt', 'hidden');
|
|
66
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
66
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
67
67
|
expect(await browse.readFrom('s', 'secret.txt')).toBeNull();
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
describe('writeTo (documents:write gate)', () => {
|
|
71
71
|
it('is absent when canWrite is false', () => {
|
|
72
72
|
const be = new MemoryDocumentBackend();
|
|
73
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: false });
|
|
73
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: false });
|
|
74
74
|
expect(browse.writeTo).toBeUndefined();
|
|
75
75
|
});
|
|
76
76
|
it('is present when canWrite is true', () => {
|
|
77
77
|
const be = new MemoryDocumentBackend();
|
|
78
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
78
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
79
79
|
expect(typeof browse.writeTo).toBe('function');
|
|
80
80
|
});
|
|
81
81
|
it('writes into the target shard namespace and emits a create change', async () => {
|
|
82
82
|
const be = new MemoryDocumentBackend();
|
|
83
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
83
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
84
84
|
const events = [];
|
|
85
85
|
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
86
86
|
await browse.writeTo('target-shard', 'notes/ideas.md', 'hello');
|
|
@@ -93,7 +93,7 @@ describe('BrowseCapability', () => {
|
|
|
93
93
|
it('emits update (not create) when overwriting an existing document', async () => {
|
|
94
94
|
const be = new MemoryDocumentBackend();
|
|
95
95
|
await be.write('t1', 'target-shard', 'a.txt', 'v1');
|
|
96
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
96
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
97
97
|
const events = [];
|
|
98
98
|
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
99
99
|
await browse.writeTo('target-shard', 'a.txt', 'v2');
|
|
@@ -105,7 +105,7 @@ describe('BrowseCapability', () => {
|
|
|
105
105
|
});
|
|
106
106
|
it('never crosses tenants: cannot be tricked into writing into tenant b', async () => {
|
|
107
107
|
const be = new MemoryDocumentBackend();
|
|
108
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
108
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
109
109
|
await browse.writeTo('s', 'a.txt', 'x');
|
|
110
110
|
expect(await be.read('t1', 's', 'a.txt')).toBe('x');
|
|
111
111
|
expect(await be.read('t2', 's', 'a.txt')).toBeNull();
|
|
@@ -114,7 +114,7 @@ describe('BrowseCapability', () => {
|
|
|
114
114
|
describe('read + write combined', () => {
|
|
115
115
|
it('round-trips content through readFrom after writeTo', async () => {
|
|
116
116
|
const be = new MemoryDocumentBackend();
|
|
117
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: true });
|
|
117
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: true });
|
|
118
118
|
await browse.writeTo('shard-x', 'doc.txt', 'roundtrip');
|
|
119
119
|
expect(await browse.readFrom('shard-x', 'doc.txt')).toBe('roundtrip');
|
|
120
120
|
});
|
|
@@ -122,13 +122,13 @@ describe('BrowseCapability', () => {
|
|
|
122
122
|
describe('statusFrom / readBranchFrom (documents:read gate)', () => {
|
|
123
123
|
it('absent when canRead is false', () => {
|
|
124
124
|
const be = new MemoryDocumentBackend();
|
|
125
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: false });
|
|
125
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: false });
|
|
126
126
|
expect(browse.statusFrom).toBeUndefined();
|
|
127
127
|
expect(browse.readBranchFrom).toBeUndefined();
|
|
128
128
|
});
|
|
129
129
|
it('present when canRead is true', () => {
|
|
130
130
|
const be = new MemoryDocumentBackend();
|
|
131
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
131
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
132
132
|
expect(typeof browse.statusFrom).toBe('function');
|
|
133
133
|
expect(typeof browse.readBranchFrom).toBe('function');
|
|
134
134
|
});
|
|
@@ -144,13 +144,13 @@ describe('BrowseCapability', () => {
|
|
|
144
144
|
listAllDocuments: vi.fn(async () => []),
|
|
145
145
|
readMeta: vi.fn(async (...args) => { calls.push(args); return null; }),
|
|
146
146
|
};
|
|
147
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
147
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
148
148
|
await browse.statusFrom('other', 'a.txt');
|
|
149
149
|
expect(calls[0]).toEqual(['t1', 'other', 'a.txt']);
|
|
150
150
|
});
|
|
151
151
|
it('statusFrom returns null when backend has no readMeta', async () => {
|
|
152
152
|
const be = new MemoryDocumentBackend();
|
|
153
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
153
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
154
154
|
expect(await browse.statusFrom('other', 'a.txt')).toBeNull();
|
|
155
155
|
});
|
|
156
156
|
it('readBranchFrom delegates to backend.readBranch with tenant baked in', async () => {
|
|
@@ -165,32 +165,32 @@ describe('BrowseCapability', () => {
|
|
|
165
165
|
listAllDocuments: vi.fn(async () => []),
|
|
166
166
|
readBranch: vi.fn(async (...args) => { calls.push(args); return 'x'; }),
|
|
167
167
|
};
|
|
168
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
168
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
169
169
|
const out = await browse.readBranchFrom('other', 'a.txt', 'peer-1');
|
|
170
170
|
expect(out).toBe('x');
|
|
171
171
|
expect(calls[0]).toEqual(['t1', 'other', 'a.txt', 'peer-1']);
|
|
172
172
|
});
|
|
173
173
|
it('readBranchFrom returns null when backend has no readBranch', async () => {
|
|
174
174
|
const be = new MemoryDocumentBackend();
|
|
175
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
175
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
176
176
|
expect(await browse.readBranchFrom('other', 'a.txt', 'peer-1')).toBeNull();
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
179
|
describe('renameFrom (documents:write gate)', () => {
|
|
180
180
|
it('absent when canWrite is false', () => {
|
|
181
181
|
const be = new MemoryDocumentBackend();
|
|
182
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: false });
|
|
182
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: false });
|
|
183
183
|
expect(browse.renameFrom).toBeUndefined();
|
|
184
184
|
});
|
|
185
185
|
it('present when canWrite is true', () => {
|
|
186
186
|
const be = new MemoryDocumentBackend();
|
|
187
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
187
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
188
188
|
expect(typeof browse.renameFrom).toBe('function');
|
|
189
189
|
});
|
|
190
190
|
it('renames in the target shard namespace and emits a rename event', async () => {
|
|
191
191
|
const be = new MemoryDocumentBackend();
|
|
192
192
|
await be.write('t1', 'target-shard', 'old.txt', 'hello');
|
|
193
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
193
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
194
194
|
const events = [];
|
|
195
195
|
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
196
196
|
await browse.renameFrom('target-shard', 'old.txt', 'new.txt');
|
|
@@ -210,7 +210,7 @@ describe('BrowseCapability', () => {
|
|
|
210
210
|
it('never crosses tenants: cannot be tricked into renaming in tenant b', async () => {
|
|
211
211
|
const be = new MemoryDocumentBackend();
|
|
212
212
|
await be.write('t2', 's', 'old.txt', 'hidden');
|
|
213
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
213
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
214
214
|
await expect(browse.renameFrom('s', 'old.txt', 'new.txt'))
|
|
215
215
|
.rejects.toThrow(/not found/);
|
|
216
216
|
expect(await be.read('t2', 's', 'old.txt')).toBe('hidden');
|
|
@@ -218,7 +218,7 @@ describe('BrowseCapability', () => {
|
|
|
218
218
|
it('throws when opts.newShardId is provided (cross-shard move reserved)', async () => {
|
|
219
219
|
const be = new MemoryDocumentBackend();
|
|
220
220
|
await be.write('t1', 's1', 'old.txt', 'hello');
|
|
221
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
221
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
222
222
|
await expect(browse.renameFrom('s1', 'old.txt', 'new.txt', { newShardId: 's2' })).rejects.toThrow(/Cross-shard move is not yet supported/);
|
|
223
223
|
expect(await be.read('t1', 's1', 'old.txt')).toBe('hello');
|
|
224
224
|
});
|
|
@@ -226,12 +226,12 @@ describe('BrowseCapability', () => {
|
|
|
226
226
|
describe('resolveConflictFrom (documents:write gate)', () => {
|
|
227
227
|
it('absent when canWrite is false', () => {
|
|
228
228
|
const be = new MemoryDocumentBackend();
|
|
229
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
229
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
230
230
|
expect(browse.resolveConflictFrom).toBeUndefined();
|
|
231
231
|
});
|
|
232
232
|
it('present when canWrite is true', () => {
|
|
233
233
|
const be = new MemoryDocumentBackend();
|
|
234
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
234
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
235
235
|
expect(typeof browse.resolveConflictFrom).toBe('function');
|
|
236
236
|
});
|
|
237
237
|
it('delegates to backend.resolve with tenant baked in and emits an update', async () => {
|
|
@@ -246,7 +246,7 @@ describe('BrowseCapability', () => {
|
|
|
246
246
|
listAllDocuments: vi.fn(async () => []),
|
|
247
247
|
resolve: vi.fn(async (...args) => { calls.push(args); }),
|
|
248
248
|
};
|
|
249
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
249
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
250
250
|
const events = [];
|
|
251
251
|
const unsub = documentChanges.subscribe((e) => events.push(e));
|
|
252
252
|
await browse.resolveConflictFrom('other', 'a.txt', { origin: 'peer-1' });
|
|
@@ -258,7 +258,7 @@ describe('BrowseCapability', () => {
|
|
|
258
258
|
});
|
|
259
259
|
it('throws if backend does not support resolve', async () => {
|
|
260
260
|
const be = new MemoryDocumentBackend();
|
|
261
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
261
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
262
262
|
await expect(browse.resolveConflictFrom('other', 'a.txt', { origin: 'peer-1' }))
|
|
263
263
|
.rejects.toThrow(/does not support resolveConflict/);
|
|
264
264
|
});
|
|
@@ -266,18 +266,18 @@ describe('BrowseCapability', () => {
|
|
|
266
266
|
describe('deleteFrom (documents:write gate)', () => {
|
|
267
267
|
it('absent when canWrite is false', () => {
|
|
268
268
|
const be = new MemoryDocumentBackend();
|
|
269
|
-
const browse = createBrowseCapability('t1', be, { canRead: true, canWrite: false });
|
|
269
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: true, canWrite: false });
|
|
270
270
|
expect(browse.deleteFrom).toBeUndefined();
|
|
271
271
|
});
|
|
272
272
|
it('present when canWrite is true', () => {
|
|
273
273
|
const be = new MemoryDocumentBackend();
|
|
274
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
274
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
275
275
|
expect(typeof browse.deleteFrom).toBe('function');
|
|
276
276
|
});
|
|
277
277
|
it('deletes from the target shard namespace and emits a delete event', async () => {
|
|
278
278
|
const be = new MemoryDocumentBackend();
|
|
279
279
|
await be.write('t1', 'target-shard', 'a.txt', 'hello');
|
|
280
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
280
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
281
281
|
const events = [];
|
|
282
282
|
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
283
283
|
await browse.deleteFrom('target-shard', 'a.txt');
|
|
@@ -289,7 +289,7 @@ describe('BrowseCapability', () => {
|
|
|
289
289
|
});
|
|
290
290
|
it('is idempotent on missing paths and emits no event', async () => {
|
|
291
291
|
const be = new MemoryDocumentBackend();
|
|
292
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
292
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
293
293
|
const events = [];
|
|
294
294
|
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
295
295
|
await expect(browse.deleteFrom('target-shard', 'nope.txt')).resolves.toBeUndefined();
|
|
@@ -299,7 +299,7 @@ describe('BrowseCapability', () => {
|
|
|
299
299
|
it('never crosses tenants: a t1 capability cannot delete t2 docs', async () => {
|
|
300
300
|
const be = new MemoryDocumentBackend();
|
|
301
301
|
await be.write('t2', 's', 'secret.txt', 'hidden');
|
|
302
|
-
const browse = createBrowseCapability('t1', be, { canRead: false, canWrite: true });
|
|
302
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
303
303
|
await browse.deleteFrom('s', 'secret.txt');
|
|
304
304
|
expect(await be.read('t2', 's', 'secret.txt')).toBe('hidden');
|
|
305
305
|
});
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { DocumentBackend } from './types';
|
|
2
|
+
/** Host-only. Register a callback that resolves the active project scope.
|
|
3
|
+
* When non-null and returns a string, that string overrides the static
|
|
4
|
+
* scopeId for all document operations. Wired by createShell after bootstrap. */
|
|
5
|
+
export declare function __setScopeResolver(resolver: (() => string | null) | null): void;
|
|
2
6
|
export declare function getActiveScopeId(): string;
|
|
7
|
+
/** The user's base (personal) tenant id — never overridden by the project resolver. */
|
|
8
|
+
export declare function getPersonalScopeId(): string;
|
|
3
9
|
/** @deprecated use getActiveScopeId — kept until callers migrate. */
|
|
4
10
|
export declare function getTenantId(): string;
|
|
5
11
|
export declare function getDocumentBackend(): DocumentBackend;
|
package/dist/documents/config.js
CHANGED
|
@@ -5,18 +5,35 @@
|
|
|
5
5
|
* calls __setActiveScope and __setDocumentBackend before bootstrap() to
|
|
6
6
|
* configure multi-scope routing and swap backends (e.g. Tauri FS).
|
|
7
7
|
*
|
|
8
|
+
* Project scope: when a project is active (sessionState.activeProjectId),
|
|
9
|
+
* the scope resolver returns the project id so all document operations
|
|
10
|
+
* operate on the project's virtual tenant instead of the user's personal
|
|
11
|
+
* scope. The resolver is wired by createShell after bootstrap.
|
|
12
|
+
*
|
|
8
13
|
* Defaults: scopeId='local' (single-user self-hosted), backend=IndexedDB.
|
|
9
14
|
*/
|
|
10
15
|
import { IndexedDBDocumentBackend } from './backends';
|
|
11
16
|
const DEFAULT_SCOPE = 'local';
|
|
12
17
|
let scopeId = DEFAULT_SCOPE;
|
|
13
18
|
let backend = new IndexedDBDocumentBackend();
|
|
19
|
+
let scopeResolver = null;
|
|
20
|
+
/** Host-only. Register a callback that resolves the active project scope.
|
|
21
|
+
* When non-null and returns a string, that string overrides the static
|
|
22
|
+
* scopeId for all document operations. Wired by createShell after bootstrap. */
|
|
23
|
+
export function __setScopeResolver(resolver) {
|
|
24
|
+
scopeResolver = resolver;
|
|
25
|
+
}
|
|
14
26
|
export function getActiveScopeId() {
|
|
27
|
+
var _a;
|
|
28
|
+
return (_a = scopeResolver === null || scopeResolver === void 0 ? void 0 : scopeResolver()) !== null && _a !== void 0 ? _a : scopeId;
|
|
29
|
+
}
|
|
30
|
+
/** The user's base (personal) tenant id — never overridden by the project resolver. */
|
|
31
|
+
export function getPersonalScopeId() {
|
|
15
32
|
return scopeId;
|
|
16
33
|
}
|
|
17
34
|
/** @deprecated use getActiveScopeId — kept until callers migrate. */
|
|
18
35
|
export function getTenantId() {
|
|
19
|
-
return
|
|
36
|
+
return getActiveScopeId();
|
|
20
37
|
}
|
|
21
38
|
export function getDocumentBackend() {
|
|
22
39
|
return backend;
|
package/dist/documents/handle.js
CHANGED
|
@@ -31,36 +31,43 @@ export function createDocumentHandle(tenantId, shardId, backend, options) {
|
|
|
31
31
|
return true;
|
|
32
32
|
return options.extensions.some((ext) => path.endsWith(ext));
|
|
33
33
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
34
|
+
function resolveTenant(opts) {
|
|
35
|
+
var _a;
|
|
36
|
+
return (_a = opts === null || opts === void 0 ? void 0 : opts.scope) !== null && _a !== void 0 ? _a : tenantId;
|
|
37
|
+
}
|
|
38
|
+
function emitChange(type, path, tid) {
|
|
39
|
+
documentChanges.emit({ type, path, tenantId: tid, shardId });
|
|
36
40
|
}
|
|
37
41
|
const handle = {
|
|
38
|
-
async list() {
|
|
39
|
-
const
|
|
42
|
+
async list(opts) {
|
|
43
|
+
const tid = resolveTenant(opts);
|
|
44
|
+
const all = await backend.list(tid, shardId);
|
|
40
45
|
if (!options.extensions || options.extensions.length === 0)
|
|
41
46
|
return all;
|
|
42
47
|
return all.filter((meta) => matchesExtensions(meta.path));
|
|
43
48
|
},
|
|
44
|
-
async read(path) {
|
|
45
|
-
const content = await backend.read(
|
|
49
|
+
async read(path, opts) {
|
|
50
|
+
const content = await backend.read(resolveTenant(opts), shardId, path);
|
|
46
51
|
if (content === null)
|
|
47
52
|
return null;
|
|
48
53
|
// Phase 1: text format only. Binary returns as-is from the backend
|
|
49
54
|
// but the handle types it as string for text-format handles.
|
|
50
55
|
return typeof content === 'string' ? content : new TextDecoder().decode(content);
|
|
51
56
|
},
|
|
52
|
-
async write(path, content) {
|
|
53
|
-
const
|
|
54
|
-
await backend.
|
|
55
|
-
|
|
57
|
+
async write(path, content, opts) {
|
|
58
|
+
const tid = resolveTenant(opts);
|
|
59
|
+
const existed = await backend.exists(tid, shardId, path);
|
|
60
|
+
await backend.write(tid, shardId, path, content);
|
|
61
|
+
emitChange(existed ? 'update' : 'create', path, tid);
|
|
56
62
|
},
|
|
57
|
-
async delete(path) {
|
|
58
|
-
const
|
|
59
|
-
await backend.
|
|
63
|
+
async delete(path, opts) {
|
|
64
|
+
const tid = resolveTenant(opts);
|
|
65
|
+
const existed = await backend.exists(tid, shardId, path);
|
|
66
|
+
await backend.delete(tid, shardId, path);
|
|
60
67
|
if (existed)
|
|
61
|
-
emitChange('delete', path);
|
|
68
|
+
emitChange('delete', path, tid);
|
|
62
69
|
},
|
|
63
|
-
async rename(oldPath, newPath) {
|
|
70
|
+
async rename(oldPath, newPath, opts) {
|
|
64
71
|
if (!matchesExtensions(newPath)) {
|
|
65
72
|
throw new Error(`Cannot rename to ${newPath}: violates handle extensions filter`);
|
|
66
73
|
}
|
|
@@ -69,15 +76,56 @@ export function createDocumentHandle(tenantId, shardId, backend, options) {
|
|
|
69
76
|
throw new Error(`Cannot rename: active autosave on ${oldPath}; flush and dispose first`);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
|
|
79
|
+
const tid = resolveTenant(opts);
|
|
80
|
+
await backend.rename(tid, shardId, oldPath, newPath);
|
|
73
81
|
documentChanges.emit({
|
|
74
82
|
type: 'rename',
|
|
75
83
|
path: newPath,
|
|
76
84
|
oldPath,
|
|
77
|
-
tenantId,
|
|
85
|
+
tenantId: tid,
|
|
86
|
+
shardId,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
async mkdir(path, opts) {
|
|
90
|
+
const tid = resolveTenant(opts);
|
|
91
|
+
await backend.mkdir(tid, shardId, path);
|
|
92
|
+
documentChanges.emit({ type: 'folder-create', path, tenantId: tid, shardId });
|
|
93
|
+
},
|
|
94
|
+
async rmdir(path, opts) {
|
|
95
|
+
var _a;
|
|
96
|
+
const recursive = (_a = opts === null || opts === void 0 ? void 0 : opts.recursive) !== null && _a !== void 0 ? _a : false;
|
|
97
|
+
if (recursive) {
|
|
98
|
+
const folderPrefix = path + '/';
|
|
99
|
+
for (const ctrl of controllers) {
|
|
100
|
+
if (ctrl.path === path || ctrl.path.startsWith(folderPrefix)) {
|
|
101
|
+
throw new Error(`Cannot rmdir ${path}: active autosave on ${ctrl.path}; flush and dispose first`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const tid = resolveTenant(opts);
|
|
106
|
+
await backend.rmdir(tid, shardId, path, { recursive });
|
|
107
|
+
documentChanges.emit({ type: 'folder-delete', path, tenantId: tid, shardId });
|
|
108
|
+
},
|
|
109
|
+
async renameFolder(oldPath, newPath, opts) {
|
|
110
|
+
const folderPrefix = oldPath + '/';
|
|
111
|
+
for (const ctrl of controllers) {
|
|
112
|
+
if (ctrl.path === oldPath || ctrl.path.startsWith(folderPrefix)) {
|
|
113
|
+
throw new Error(`Cannot rename folder ${oldPath}: active autosave on ${ctrl.path}; flush and dispose first`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const tid = resolveTenant(opts);
|
|
117
|
+
await backend.renameFolder(tid, shardId, oldPath, newPath);
|
|
118
|
+
documentChanges.emit({
|
|
119
|
+
type: 'folder-rename',
|
|
120
|
+
path: newPath,
|
|
121
|
+
oldPath,
|
|
122
|
+
tenantId: tid,
|
|
78
123
|
shardId,
|
|
79
124
|
});
|
|
80
125
|
},
|
|
126
|
+
async listFolders(prefix, opts) {
|
|
127
|
+
return backend.listFolders(resolveTenant(opts), shardId, prefix !== null && prefix !== void 0 ? prefix : '');
|
|
128
|
+
},
|
|
81
129
|
async exists(path) {
|
|
82
130
|
return backend.exists(tenantId, shardId, path);
|
|
83
131
|
},
|