sh3-core 0.20.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BrandSlot.svelte +2 -2
- package/dist/actions/ctx-actions.svelte.test.js +2 -2
- package/dist/api.d.ts +2 -2
- package/dist/api.js +1 -1
- package/dist/app/store/StoreView.svelte +26 -35
- package/dist/app/store/storeShard.svelte.js +35 -49
- package/dist/app/store/verbs.js +24 -55
- 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 +34 -9
- 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/env/client.d.ts +6 -10
- package/dist/env/client.js +11 -21
- package/dist/env/index.d.ts +2 -1
- package/dist/env/index.js +1 -1
- 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/registry/archive.d.ts +12 -0
- package/dist/registry/archive.js +80 -0
- package/dist/registry/archive.test.d.ts +1 -0
- package/dist/registry/archive.test.js +84 -0
- package/dist/registry/client.d.ts +9 -29
- package/dist/registry/client.js +14 -60
- package/dist/registry/client.test.js +31 -21
- package/dist/registry/index.d.ts +2 -2
- package/dist/registry/index.js +1 -1
- package/dist/registry/installer.d.ts +4 -4
- package/dist/registry/installer.js +74 -45
- package/dist/registry/schema.js +4 -27
- package/dist/registry/schema.test.d.ts +1 -0
- package/dist/registry/schema.test.js +41 -0
- package/dist/registry/types.d.ts +16 -41
- 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 +3 -2
package/dist/createShell.js
CHANGED
|
@@ -22,11 +22,33 @@ import { registerLoadedBundle } from './registry/register';
|
|
|
22
22
|
import { attachGlobalListeners } from './actions/listeners';
|
|
23
23
|
import { detectSatelliteMode } from './boot/satelliteMode';
|
|
24
24
|
import { MemoryBackend } from './state/backends';
|
|
25
|
-
import { sessionState } from './projects/session-state.svelte';
|
|
25
|
+
import { sessionState, readPendingScope, PENDING_SCOPE_KEY } from './projects/session-state.svelte';
|
|
26
26
|
import SatelliteShell from './satellite/SatelliteShell.svelte';
|
|
27
27
|
export async function createShell(config) {
|
|
28
28
|
var _a, _b;
|
|
29
29
|
const sUrl = (_a = config === null || config === void 0 ? void 0 : config.serverUrl) !== null && _a !== void 0 ? _a : '';
|
|
30
|
+
// 0. Restore pending project scope from a reload-triggered scope switch.
|
|
31
|
+
// Written before the previous page reload by switchProjectScope(); must
|
|
32
|
+
// be set before bootstrap() so shards activate with the correct scope.
|
|
33
|
+
const pendingProjectId = readPendingScope();
|
|
34
|
+
if (pendingProjectId !== null) {
|
|
35
|
+
sessionState.activeProjectId = pendingProjectId;
|
|
36
|
+
}
|
|
37
|
+
// Read ?project= injected by Tauri's --project CLI arg (or any deep-link URL).
|
|
38
|
+
// Guard against overriding an in-session scope switch that wrote to sessionStorage.
|
|
39
|
+
if (typeof window !== 'undefined' && pendingProjectId === null) {
|
|
40
|
+
const urlProject = new URLSearchParams(window.location.search).get('project');
|
|
41
|
+
if (urlProject) {
|
|
42
|
+
// Mirror switchProjectScope's sessionStorage write so subsequent reloads
|
|
43
|
+
// within this tab re-enter the same project (readPendingScope will pick it up).
|
|
44
|
+
sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ projectId: urlProject }));
|
|
45
|
+
sessionState.activeProjectId = urlProject;
|
|
46
|
+
// Strip the param so it doesn't linger in bookmarks or history.
|
|
47
|
+
const cleanUrl = new URL(window.location.href);
|
|
48
|
+
cleanUrl.searchParams.delete('project');
|
|
49
|
+
history.replaceState(null, '', cleanUrl.toString());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
30
52
|
// 1. Platform detection
|
|
31
53
|
const platform = await resolvePlatform();
|
|
32
54
|
if (platform.backends) {
|
|
@@ -60,6 +82,11 @@ export async function createShell(config) {
|
|
|
60
82
|
// but pop-out is currently a Tauri-only POC so we don't fetch it.
|
|
61
83
|
if (platform.localOwner)
|
|
62
84
|
__setActiveScope('local');
|
|
85
|
+
// Inherit the host's active project scope so shard activation lands in the
|
|
86
|
+
// correct tenant (satellite opens in the same project context as the host).
|
|
87
|
+
if (satellite.payload.projectId) {
|
|
88
|
+
sessionState.activeProjectId = satellite.payload.projectId;
|
|
89
|
+
}
|
|
63
90
|
__setScopeResolver(() => sessionState.activeProjectId);
|
|
64
91
|
__setShardScopeResolver(() => sessionState.activeProjectId ? 'project' : 'tenant');
|
|
65
92
|
if (config === null || config === void 0 ? void 0 : config.shards)
|
|
@@ -138,19 +165,17 @@ export async function createShell(config) {
|
|
|
138
165
|
for (const app of config.apps)
|
|
139
166
|
registerApp(app);
|
|
140
167
|
}
|
|
141
|
-
// 7.
|
|
168
|
+
// 7. Wire scope resolvers before bootstrap so shards activate in the correct scope.
|
|
169
|
+
__setScopeResolver(() => sessionState.activeProjectId);
|
|
170
|
+
__setShardScopeResolver(() => sessionState.activeProjectId ? 'project' : 'tenant');
|
|
171
|
+
// 8. Bootstrap
|
|
142
172
|
const bootstrapConfig = {};
|
|
143
173
|
if (config === null || config === void 0 ? void 0 : config.excludeShards)
|
|
144
174
|
bootstrapConfig.excludeShards = config.excludeShards;
|
|
145
175
|
await bootstrap(bootstrapConfig);
|
|
146
|
-
//
|
|
147
|
-
// When the user enters a project, getActiveScopeId() returns the project
|
|
148
|
-
// id so all document operations use the project's virtual tenant.
|
|
149
|
-
__setScopeResolver(() => sessionState.activeProjectId);
|
|
150
|
-
__setShardScopeResolver(() => sessionState.activeProjectId ? 'project' : 'tenant');
|
|
151
|
-
// 8. Attach document-level keyboard / focus listeners
|
|
176
|
+
// 9. Attach document-level keyboard / focus listeners
|
|
152
177
|
attachGlobalListeners();
|
|
153
|
-
//
|
|
178
|
+
// 10. Mount the sh3
|
|
154
179
|
mount(Sh3, { target });
|
|
155
180
|
}
|
|
156
181
|
/**
|
|
@@ -108,6 +108,26 @@ export interface BrowseCapability {
|
|
|
108
108
|
targetShardId?: string;
|
|
109
109
|
delete?: boolean;
|
|
110
110
|
}): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* List all documents in an arbitrary tenant. Write-gated — cross-tenant
|
|
113
|
+
* enumeration is a privileged operation used by xfer -R for cross-scope recursion.
|
|
114
|
+
*
|
|
115
|
+
* Absent (undefined) when `documents:write` is not declared.
|
|
116
|
+
*/
|
|
117
|
+
listDocumentsIn?(tenantId: string): Promise<Array<DocumentMeta & {
|
|
118
|
+
shardId: string;
|
|
119
|
+
}>>;
|
|
120
|
+
/**
|
|
121
|
+
* Copy or move a document between any two tenants. Neither tenant needs to
|
|
122
|
+
* be the active one. Emits documentChanges for both source (delete if
|
|
123
|
+
* opts.delete is true) and destination (create/update). Throws when src
|
|
124
|
+
* and dst are identical.
|
|
125
|
+
*
|
|
126
|
+
* Absent (undefined) when `documents:write` is not declared.
|
|
127
|
+
*/
|
|
128
|
+
transferBetweenScopes?(srcTenant: string, srcShardId: string, srcPath: string, dstTenant: string, dstShardId: string, dstPath: string, opts?: {
|
|
129
|
+
delete?: boolean;
|
|
130
|
+
}): Promise<void>;
|
|
111
131
|
}
|
|
112
132
|
export interface BrowseCapabilityOptions {
|
|
113
133
|
/** When true, the returned capability exposes `readFrom`. */
|
package/dist/documents/browse.js
CHANGED
|
@@ -99,6 +99,41 @@ export function createBrowseCapability(getTenantId, backend, options = { canRead
|
|
|
99
99
|
documentChanges.emit({ type: 'delete', path, tenantId, shardId });
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
|
+
capability.listDocumentsIn = (tenantId) => backend.listAllDocuments(tenantId);
|
|
103
|
+
capability.transferBetweenScopes = async (srcTenant, srcShard, srcPath, dstTenant, dstShard, dstPath, opts) => {
|
|
104
|
+
if (srcTenant === dstTenant && srcShard === dstShard && srcPath === dstPath) {
|
|
105
|
+
throw new Error('transferBetweenScopes: source and destination are identical');
|
|
106
|
+
}
|
|
107
|
+
if (backend.xfer) {
|
|
108
|
+
const { existed } = await backend.xfer(srcTenant, `${srcShard}/${srcPath}`, dstTenant, `${dstShard}/${dstPath}`, { move: opts === null || opts === void 0 ? void 0 : opts.delete });
|
|
109
|
+
documentChanges.emit({
|
|
110
|
+
type: existed ? 'update' : 'create',
|
|
111
|
+
path: dstPath,
|
|
112
|
+
tenantId: dstTenant,
|
|
113
|
+
shardId: dstShard,
|
|
114
|
+
});
|
|
115
|
+
if (opts === null || opts === void 0 ? void 0 : opts.delete) {
|
|
116
|
+
documentChanges.emit({ type: 'delete', path: srcPath, tenantId: srcTenant, shardId: srcShard });
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const content = await backend.read(srcTenant, srcShard, srcPath);
|
|
121
|
+
if (content === null) {
|
|
122
|
+
throw new Error(`Document not found at ${srcShard}/${srcPath} in scope ${srcTenant}`);
|
|
123
|
+
}
|
|
124
|
+
const existed = await backend.exists(dstTenant, dstShard, dstPath);
|
|
125
|
+
await backend.write(dstTenant, dstShard, dstPath, content);
|
|
126
|
+
documentChanges.emit({
|
|
127
|
+
type: existed ? 'update' : 'create',
|
|
128
|
+
path: dstPath,
|
|
129
|
+
tenantId: dstTenant,
|
|
130
|
+
shardId: dstShard,
|
|
131
|
+
});
|
|
132
|
+
if (opts === null || opts === void 0 ? void 0 : opts.delete) {
|
|
133
|
+
await backend.delete(srcTenant, srcShard, srcPath);
|
|
134
|
+
documentChanges.emit({ type: 'delete', path: srcPath, tenantId: srcTenant, shardId: srcShard });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
102
137
|
}
|
|
103
138
|
return capability;
|
|
104
139
|
}
|
|
@@ -304,4 +304,129 @@ describe('BrowseCapability', () => {
|
|
|
304
304
|
expect(await be.read('t2', 's', 'secret.txt')).toBe('hidden');
|
|
305
305
|
});
|
|
306
306
|
});
|
|
307
|
+
describe('listDocumentsIn (documents:write gate)', () => {
|
|
308
|
+
it('is absent when canWrite is false', () => {
|
|
309
|
+
const be = new MemoryDocumentBackend();
|
|
310
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: false });
|
|
311
|
+
expect(browse.listDocumentsIn).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
it('lists documents from an arbitrary tenant, not the active one', async () => {
|
|
314
|
+
const be = new MemoryDocumentBackend();
|
|
315
|
+
await be.write('t2', 'notes', 'a.md', 'hello');
|
|
316
|
+
await be.write('t2', 'notes', 'b.md', 'world');
|
|
317
|
+
await be.write('t1', 'notes', 'c.md', 'active');
|
|
318
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
319
|
+
const docs = await browse.listDocumentsIn('t2');
|
|
320
|
+
expect(docs.map((d) => d.path).sort()).toEqual(['a.md', 'b.md']);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe('transferBetweenScopes (documents:write gate)', () => {
|
|
324
|
+
it('is absent when canWrite is false', () => {
|
|
325
|
+
const be = new MemoryDocumentBackend();
|
|
326
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: false });
|
|
327
|
+
expect(browse.transferBetweenScopes).toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
it('copies a document from one tenant to another and emits create', async () => {
|
|
330
|
+
const be = new MemoryDocumentBackend();
|
|
331
|
+
await be.write('t1', 'notes', 'draft.md', 'content');
|
|
332
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
333
|
+
const events = [];
|
|
334
|
+
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
335
|
+
await browse.transferBetweenScopes('t1', 'notes', 'draft.md', 't2', 'notes', 'draft.md');
|
|
336
|
+
expect(await be.read('t2', 'notes', 'draft.md')).toBe('content');
|
|
337
|
+
expect(await be.read('t1', 'notes', 'draft.md')).toBe('content'); // source intact
|
|
338
|
+
expect(events).toEqual([
|
|
339
|
+
{ type: 'create', path: 'draft.md', tenantId: 't2', shardId: 'notes' },
|
|
340
|
+
]);
|
|
341
|
+
unsub();
|
|
342
|
+
});
|
|
343
|
+
it('deletes source and emits delete when opts.delete is true', async () => {
|
|
344
|
+
const be = new MemoryDocumentBackend();
|
|
345
|
+
await be.write('t1', 'notes', 'draft.md', 'content');
|
|
346
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
347
|
+
const events = [];
|
|
348
|
+
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
349
|
+
await browse.transferBetweenScopes('t1', 'notes', 'draft.md', 't2', 'notes', 'draft.md', { delete: true });
|
|
350
|
+
expect(await be.read('t2', 'notes', 'draft.md')).toBe('content');
|
|
351
|
+
expect(await be.read('t1', 'notes', 'draft.md')).toBeNull();
|
|
352
|
+
expect(events).toEqual([
|
|
353
|
+
{ type: 'create', path: 'draft.md', tenantId: 't2', shardId: 'notes' },
|
|
354
|
+
{ type: 'delete', path: 'draft.md', tenantId: 't1', shardId: 'notes' },
|
|
355
|
+
]);
|
|
356
|
+
unsub();
|
|
357
|
+
});
|
|
358
|
+
it('emits update (not create) when destination already exists', async () => {
|
|
359
|
+
const be = new MemoryDocumentBackend();
|
|
360
|
+
await be.write('t1', 'notes', 'draft.md', 'v1');
|
|
361
|
+
await be.write('t2', 'notes', 'draft.md', 'old');
|
|
362
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
363
|
+
const events = [];
|
|
364
|
+
const unsub = documentChanges.subscribe((c) => events.push(c));
|
|
365
|
+
await browse.transferBetweenScopes('t1', 'notes', 'draft.md', 't2', 'notes', 'draft.md');
|
|
366
|
+
expect(events[0].type).toBe('update');
|
|
367
|
+
unsub();
|
|
368
|
+
});
|
|
369
|
+
it('throws when source and destination are identical', async () => {
|
|
370
|
+
const be = new MemoryDocumentBackend();
|
|
371
|
+
await be.write('t1', 'notes', 'draft.md', 'x');
|
|
372
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
373
|
+
await expect(browse.transferBetweenScopes('t1', 'notes', 'draft.md', 't1', 'notes', 'draft.md')).rejects.toThrow('identical');
|
|
374
|
+
});
|
|
375
|
+
it('throws when source document does not exist', async () => {
|
|
376
|
+
const be = new MemoryDocumentBackend();
|
|
377
|
+
const browse = createBrowseCapability(() => 't1', be, { canRead: false, canWrite: true });
|
|
378
|
+
await expect(browse.transferBetweenScopes('t1', 'notes', 'missing.md', 't2', 'notes', 'missing.md')).rejects.toThrow('not found');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
describe('transferBetweenScopes', () => {
|
|
383
|
+
it('delegates to backend.xfer when the method is present', async () => {
|
|
384
|
+
const xfer = vi.fn(async () => ({ existed: false }));
|
|
385
|
+
const be = new MemoryDocumentBackend();
|
|
386
|
+
be.xfer = xfer;
|
|
387
|
+
const browse = createBrowseCapability(() => 'alice', be, { canRead: true, canWrite: true });
|
|
388
|
+
await browse.transferBetweenScopes('alice', 'notes', 'draft.md', 'proj-1', 'notes', 'draft.md', { delete: true });
|
|
389
|
+
expect(xfer).toHaveBeenCalledWith('alice', 'notes/draft.md', 'proj-1', 'notes/draft.md', { move: true });
|
|
390
|
+
});
|
|
391
|
+
it('emits create on dst and delete on src when move=true and existed=false', async () => {
|
|
392
|
+
const changes = [];
|
|
393
|
+
const unsub = documentChanges.subscribe((c) => changes.push(c));
|
|
394
|
+
const xfer = vi.fn(async () => ({ existed: false }));
|
|
395
|
+
const be = new MemoryDocumentBackend();
|
|
396
|
+
be.xfer = xfer;
|
|
397
|
+
const browse = createBrowseCapability(() => 'alice', be, { canRead: true, canWrite: true });
|
|
398
|
+
await browse.transferBetweenScopes('alice', 'notes', 'draft.md', 'proj-1', 'notes', 'draft.md', { delete: true });
|
|
399
|
+
unsub();
|
|
400
|
+
expect(changes).toContainEqual(expect.objectContaining({ type: 'create', path: 'draft.md', tenantId: 'proj-1', shardId: 'notes' }));
|
|
401
|
+
expect(changes).toContainEqual(expect.objectContaining({ type: 'delete', path: 'draft.md', tenantId: 'alice', shardId: 'notes' }));
|
|
402
|
+
});
|
|
403
|
+
it('emits update on dst when existed=true', async () => {
|
|
404
|
+
const changes = [];
|
|
405
|
+
const unsub = documentChanges.subscribe((c) => changes.push(c));
|
|
406
|
+
const xfer = vi.fn(async () => ({ existed: true }));
|
|
407
|
+
const be = new MemoryDocumentBackend();
|
|
408
|
+
be.xfer = xfer;
|
|
409
|
+
const browse = createBrowseCapability(() => 'alice', be, { canRead: true, canWrite: true });
|
|
410
|
+
await browse.transferBetweenScopes('alice', 'notes', 'draft.md', 'proj-1', 'notes', 'draft.md', { delete: false });
|
|
411
|
+
unsub();
|
|
412
|
+
expect(changes).toContainEqual(expect.objectContaining({ type: 'update', path: 'draft.md', tenantId: 'proj-1', shardId: 'notes' }));
|
|
413
|
+
});
|
|
414
|
+
it('falls back to read+write when backend.xfer is absent', async () => {
|
|
415
|
+
const be = new MemoryDocumentBackend();
|
|
416
|
+
await be.write('alice', 'notes', 'draft.md', 'hello');
|
|
417
|
+
const browse = createBrowseCapability(() => 'alice', be, { canRead: true, canWrite: true });
|
|
418
|
+
await browse.transferBetweenScopes('alice', 'notes', 'draft.md', 'proj-1', 'notes', 'draft.md', { delete: false });
|
|
419
|
+
const copied = await be.read('proj-1', 'notes', 'draft.md');
|
|
420
|
+
expect(copied).toBe('hello');
|
|
421
|
+
const original = await be.read('alice', 'notes', 'draft.md');
|
|
422
|
+
expect(original).toBe('hello');
|
|
423
|
+
});
|
|
424
|
+
it('falls back to read+write+delete when backend.xfer is absent and delete=true', async () => {
|
|
425
|
+
const be = new MemoryDocumentBackend();
|
|
426
|
+
await be.write('alice', 'notes', 'draft.md', 'hello');
|
|
427
|
+
const browse = createBrowseCapability(() => 'alice', be, { canRead: true, canWrite: true });
|
|
428
|
+
await browse.transferBetweenScopes('alice', 'notes', 'draft.md', 'proj-1', 'notes', 'draft.md', { delete: true });
|
|
429
|
+
expect(await be.read('alice', 'notes', 'draft.md')).toBeNull();
|
|
430
|
+
expect(await be.read('proj-1', 'notes', 'draft.md')).toBe('hello');
|
|
431
|
+
});
|
|
307
432
|
});
|
|
@@ -6,12 +6,8 @@ export declare function __setScopeResolver(resolver: (() => string | null) | nul
|
|
|
6
6
|
export declare function getActiveScopeId(): string;
|
|
7
7
|
/** The user's base (personal) tenant id — never overridden by the project resolver. */
|
|
8
8
|
export declare function getPersonalScopeId(): string;
|
|
9
|
-
/** @deprecated use getActiveScopeId — kept until callers migrate. */
|
|
10
|
-
export declare function getTenantId(): string;
|
|
11
9
|
export declare function getDocumentBackend(): DocumentBackend;
|
|
12
10
|
/** Host-only. Set the active scope id before bootstrap(). */
|
|
13
11
|
export declare function __setActiveScope(id: string): void;
|
|
14
|
-
/** @deprecated use __setActiveScope — kept until callers migrate. */
|
|
15
|
-
export declare function __setTenantId(id: string): void;
|
|
16
12
|
/** Host-only. Swap the document backend before bootstrap(). */
|
|
17
13
|
export declare function __setDocumentBackend(b: DocumentBackend): void;
|
package/dist/documents/config.js
CHANGED
|
@@ -31,10 +31,6 @@ export function getActiveScopeId() {
|
|
|
31
31
|
export function getPersonalScopeId() {
|
|
32
32
|
return scopeId;
|
|
33
33
|
}
|
|
34
|
-
/** @deprecated use getActiveScopeId — kept until callers migrate. */
|
|
35
|
-
export function getTenantId() {
|
|
36
|
-
return getActiveScopeId();
|
|
37
|
-
}
|
|
38
34
|
export function getDocumentBackend() {
|
|
39
35
|
return backend;
|
|
40
36
|
}
|
|
@@ -42,10 +38,6 @@ export function getDocumentBackend() {
|
|
|
42
38
|
export function __setActiveScope(id) {
|
|
43
39
|
scopeId = id;
|
|
44
40
|
}
|
|
45
|
-
/** @deprecated use __setActiveScope — kept until callers migrate. */
|
|
46
|
-
export function __setTenantId(id) {
|
|
47
|
-
__setActiveScope(id);
|
|
48
|
-
}
|
|
49
41
|
/** Host-only. Swap the document backend before bootstrap(). */
|
|
50
42
|
export function __setDocumentBackend(b) {
|
|
51
43
|
backend = b;
|
|
@@ -31,6 +31,11 @@ export declare class HttpDocumentBackend implements DocumentBackend {
|
|
|
31
31
|
origin: string;
|
|
32
32
|
} | string): Promise<void>;
|
|
33
33
|
readBranch(tenantId: string, shardId: string, path: string, origin: string): Promise<string | null>;
|
|
34
|
+
xfer(srcTenant: string, srcPath: string, dstTenant: string, dstPath: string, opts?: {
|
|
35
|
+
move?: boolean;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
existed: boolean;
|
|
38
|
+
}>;
|
|
34
39
|
rename(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
|
|
35
40
|
mkdir(tenantId: string, shardId: string, path: string): Promise<void>;
|
|
36
41
|
rmdir(tenantId: string, shardId: string, path: string, opts: {
|
|
@@ -128,6 +128,31 @@ export class HttpDocumentBackend {
|
|
|
128
128
|
throw new Error(`readBranch failed: ${res.status}`);
|
|
129
129
|
return res.text();
|
|
130
130
|
}
|
|
131
|
+
async xfer(srcTenant, srcPath, dstTenant, dstPath, opts) {
|
|
132
|
+
var _a;
|
|
133
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/xfer`;
|
|
134
|
+
const res = await apiFetch(url, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' }),
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
src: { scope: srcTenant, path: srcPath },
|
|
139
|
+
dst: { scope: dstTenant, path: dstPath },
|
|
140
|
+
move: (_a = opts === null || opts === void 0 ? void 0 : opts.move) !== null && _a !== void 0 ? _a : false,
|
|
141
|
+
}),
|
|
142
|
+
credentials: 'include',
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
let detail = `HTTP ${res.status}`;
|
|
146
|
+
try {
|
|
147
|
+
const b = await res.json();
|
|
148
|
+
if (b.error)
|
|
149
|
+
detail = b.error;
|
|
150
|
+
}
|
|
151
|
+
catch ( /* not JSON */_b) { /* not JSON */ }
|
|
152
|
+
throw new Error(`xfer failed: ${detail}`);
|
|
153
|
+
}
|
|
154
|
+
return res.json();
|
|
155
|
+
}
|
|
131
156
|
async rename(tenantId, shardId, oldPath, newPath) {
|
|
132
157
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${oldPath}/rename`;
|
|
133
158
|
const headers = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' });
|
|
@@ -129,3 +129,69 @@ describe('HttpDocumentBackend folder ops', () => {
|
|
|
129
129
|
expect(result).toEqual(['c']);
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
|
+
describe('HttpDocumentBackend.xfer', () => {
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
globalThis.fetch = originalFetch;
|
|
135
|
+
});
|
|
136
|
+
it('POSTs to /api/xfer with src, dst, and move in the body', async () => {
|
|
137
|
+
var _a, _b;
|
|
138
|
+
const calls = [];
|
|
139
|
+
globalThis.fetch = (async (url, init) => {
|
|
140
|
+
calls.push({ url: String(url), init });
|
|
141
|
+
return new Response(JSON.stringify({ ok: true, existed: false }), { status: 200 });
|
|
142
|
+
});
|
|
143
|
+
const be = new HttpDocumentBackend('http://server', 'apikey-1');
|
|
144
|
+
const result = await be.xfer('alice', 'notes/draft.md', 'proj-1', 'notes/draft.md', { move: true });
|
|
145
|
+
expect(calls).toHaveLength(1);
|
|
146
|
+
expect(calls[0].url).toBe('http://server/api/xfer');
|
|
147
|
+
expect((_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.method).toBe('POST');
|
|
148
|
+
const body = JSON.parse((_b = calls[0].init) === null || _b === void 0 ? void 0 : _b.body);
|
|
149
|
+
expect(body).toEqual({
|
|
150
|
+
src: { scope: 'alice', path: 'notes/draft.md' },
|
|
151
|
+
dst: { scope: 'proj-1', path: 'notes/draft.md' },
|
|
152
|
+
move: true,
|
|
153
|
+
});
|
|
154
|
+
expect(result).toEqual({ ok: true, existed: false });
|
|
155
|
+
});
|
|
156
|
+
it('sends move=false when opts.move is false', async () => {
|
|
157
|
+
var _a;
|
|
158
|
+
const calls = [];
|
|
159
|
+
globalThis.fetch = (async (url, init) => {
|
|
160
|
+
calls.push({ url: String(url), init });
|
|
161
|
+
return new Response(JSON.stringify({ ok: true, existed: true }), { status: 200 });
|
|
162
|
+
});
|
|
163
|
+
const be = new HttpDocumentBackend('http://server');
|
|
164
|
+
await be.xfer('alice', 'notes/a.md', 'proj-1', 'notes/a.md', { move: false });
|
|
165
|
+
const body = JSON.parse((_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.body);
|
|
166
|
+
expect(body.move).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('sends move=false when opts is omitted', async () => {
|
|
169
|
+
var _a;
|
|
170
|
+
const calls = [];
|
|
171
|
+
globalThis.fetch = (async (url, init) => {
|
|
172
|
+
calls.push({ url: String(url), init });
|
|
173
|
+
return new Response(JSON.stringify({ ok: true, existed: false }), { status: 200 });
|
|
174
|
+
});
|
|
175
|
+
const be = new HttpDocumentBackend('http://server');
|
|
176
|
+
await be.xfer('alice', 'notes/a.md', 'proj-1', 'notes/a.md');
|
|
177
|
+
const body = JSON.parse((_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.body);
|
|
178
|
+
expect(body.move).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
it('includes Authorization header when apiKey is set', async () => {
|
|
181
|
+
var _a;
|
|
182
|
+
const calls = [];
|
|
183
|
+
globalThis.fetch = (async (url, init) => {
|
|
184
|
+
calls.push({ url: String(url), init });
|
|
185
|
+
return new Response(JSON.stringify({ ok: true, existed: false }), { status: 200 });
|
|
186
|
+
});
|
|
187
|
+
const be = new HttpDocumentBackend('http://server', 'my-key');
|
|
188
|
+
await be.xfer('alice', 'notes/a.md', 'proj-1', 'notes/a.md');
|
|
189
|
+
const headers = (_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.headers;
|
|
190
|
+
expect(headers['Authorization']).toBe('Bearer my-key');
|
|
191
|
+
});
|
|
192
|
+
it('throws with server error message on non-ok response', async () => {
|
|
193
|
+
globalThis.fetch = (async () => new Response(JSON.stringify({ error: 'Source document not found' }), { status: 404 }));
|
|
194
|
+
const be = new HttpDocumentBackend('http://server');
|
|
195
|
+
await expect(be.xfer('alice', 'notes/missing.md', 'proj-1', 'notes/missing.md')).rejects.toThrow('Source document not found');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -3,6 +3,6 @@ export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
|
|
|
3
3
|
export { HttpDocumentBackend } from './http-backend';
|
|
4
4
|
export { createDocumentHandle } from './handle';
|
|
5
5
|
export { documentChanges } from './notifications';
|
|
6
|
-
export { getActiveScopeId,
|
|
6
|
+
export { getActiveScopeId, getDocumentBackend, __setActiveScope, __setDocumentBackend, __setScopeResolver, } from './config';
|
|
7
7
|
export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranch, } from './sync-types';
|
|
8
8
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY, } from './sync-types';
|
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/env/client.d.ts
CHANGED
|
@@ -27,18 +27,14 @@ export interface ServerInstallResult {
|
|
|
27
27
|
}>;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* The
|
|
32
|
-
* (and the server bundle, if present and serverIntegrity was declared).
|
|
30
|
+
* Request the server to install a package autonomously.
|
|
31
|
+
* The server fetches and validates the archive from the registry itself.
|
|
33
32
|
*
|
|
34
|
-
* @param
|
|
35
|
-
* @param
|
|
36
|
-
* @param
|
|
37
|
-
* provided, the server writes it to `server.js` and hot-mounts the
|
|
38
|
-
* shard's routes. If the mount fails, the entire install is rolled
|
|
39
|
-
* back server-side.
|
|
33
|
+
* @param registryUrl - URL of the registry.json that lists the package.
|
|
34
|
+
* @param packageId - The package id to install.
|
|
35
|
+
* @param version - The exact version string to install.
|
|
40
36
|
*/
|
|
41
|
-
export declare function
|
|
37
|
+
export declare function requestServerInstall(registryUrl: string, packageId: string, version: string): Promise<ServerInstallResult>;
|
|
42
38
|
/**
|
|
43
39
|
* Uninstall a package from the server.
|
|
44
40
|
*/
|
package/dist/env/client.js
CHANGED
|
@@ -45,35 +45,25 @@ export async function putEnvState(shardId, state) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
* The
|
|
50
|
-
* (and the server bundle, if present and serverIntegrity was declared).
|
|
48
|
+
* Request the server to install a package autonomously.
|
|
49
|
+
* The server fetches and validates the archive from the registry itself.
|
|
51
50
|
*
|
|
52
|
-
* @param
|
|
53
|
-
* @param
|
|
54
|
-
* @param
|
|
55
|
-
* provided, the server writes it to `server.js` and hot-mounts the
|
|
56
|
-
* shard's routes. If the mount fails, the entire install is rolled
|
|
57
|
-
* back server-side.
|
|
51
|
+
* @param registryUrl - URL of the registry.json that lists the package.
|
|
52
|
+
* @param packageId - The package id to install.
|
|
53
|
+
* @param version - The exact version string to install.
|
|
58
54
|
*/
|
|
59
|
-
export async function
|
|
55
|
+
export async function requestServerInstall(registryUrl, packageId, version) {
|
|
60
56
|
var _a;
|
|
61
57
|
if (!isAdmin())
|
|
62
58
|
throw new Error('Cannot install: not elevated to admin');
|
|
63
59
|
const auth = getAuthHeader();
|
|
64
|
-
const
|
|
65
|
-
form.append('manifest', new Blob([JSON.stringify(manifest)], { type: 'application/json' }), 'manifest.json');
|
|
66
|
-
form.append('client', new Blob([clientBundle], { type: 'application/javascript' }), 'client.js');
|
|
67
|
-
if (serverBundle !== undefined) {
|
|
68
|
-
form.append('server', new Blob([serverBundle], { type: 'application/javascript' }), 'server.js');
|
|
69
|
-
}
|
|
70
|
-
const headers = {};
|
|
60
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
71
61
|
if (auth)
|
|
72
62
|
headers['Authorization'] = auth;
|
|
73
63
|
const res = await apiFetch(`${getEnvServerUrl()}/api/packages/install`, {
|
|
74
64
|
method: 'POST',
|
|
75
65
|
headers,
|
|
76
|
-
body:
|
|
66
|
+
body: JSON.stringify({ registryUrl, packageId, version }),
|
|
77
67
|
credentials: 'omit',
|
|
78
68
|
});
|
|
79
69
|
if (!res.ok) {
|
|
@@ -84,9 +74,9 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
|
|
|
84
74
|
catch ( /* non-JSON */_b) { /* non-JSON */ }
|
|
85
75
|
return {
|
|
86
76
|
ok: false,
|
|
87
|
-
error: typeof body
|
|
88
|
-
code: typeof body
|
|
89
|
-
missing: Array.isArray(body
|
|
77
|
+
error: typeof body['error'] === 'string' ? body['error'] : `HTTP ${res.status}`,
|
|
78
|
+
code: typeof body['code'] === 'string' ? body['code'] : undefined,
|
|
79
|
+
missing: Array.isArray(body['missing']) ? body['missing'] : undefined,
|
|
90
80
|
};
|
|
91
81
|
}
|
|
92
82
|
const body = await res.json();
|
package/dist/env/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export type { EnvState } from './types';
|
|
2
|
-
export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState,
|
|
2
|
+
export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState, requestServerInstall, serverUninstallPackage, fetchServerPackages, } from './client';
|
|
3
|
+
export type { ServerInstallResult } from './client';
|
package/dist/env/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState,
|
|
1
|
+
export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState, requestServerInstall, serverUninstallPackage, fetchServerPackages, } from './client';
|
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
|
};
|