sh3-core 0.8.2 → 0.9.1
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/api.d.ts +4 -7
- package/dist/api.js +2 -4
- package/dist/app/store/InstalledView.svelte +55 -1
- package/dist/app/store/PermissionConfirmModal.svelte +232 -0
- package/dist/app/store/PermissionConfirmModal.svelte.d.ts +17 -0
- package/dist/app/store/StoreView.svelte +119 -5
- package/dist/app/store/storeShard.svelte.d.ts +10 -1
- package/dist/app/store/storeShard.svelte.js +51 -7
- package/dist/app/store/storeShard.svelte.test.js +34 -0
- package/dist/apps/types.d.ts +3 -5
- package/dist/documents/backends.d.ts +2 -0
- package/dist/documents/backends.js +6 -0
- package/dist/documents/browse.d.ts +31 -1
- package/dist/documents/browse.js +18 -2
- package/dist/documents/browse.test.js +81 -0
- package/dist/documents/handle.js +13 -5
- package/dist/documents/handle.test.js +55 -0
- package/dist/documents/http-backend.d.ts +11 -4
- package/dist/documents/http-backend.js +37 -11
- package/dist/documents/index.d.ts +2 -1
- package/dist/documents/index.js +1 -1
- package/dist/documents/sync-types.d.ts +45 -0
- package/dist/documents/sync-types.js +11 -0
- package/dist/documents/types.d.ts +69 -2
- package/dist/documents/types.js +32 -2
- package/dist/keys/ConsentDialog.svelte +4 -4
- package/dist/keys/consent.test.js +4 -3
- package/dist/keys/types.d.ts +4 -2
- package/dist/registry/client.js +3 -0
- package/dist/registry/installer.d.ts +4 -1
- package/dist/registry/installer.js +25 -11
- package/dist/registry/permission-descriptions.d.ts +21 -0
- package/dist/registry/permission-descriptions.js +67 -0
- package/dist/registry/permission-descriptions.test.js +86 -0
- package/dist/registry/schema.js +19 -6
- package/dist/registry/types.d.ts +17 -5
- package/dist/server-shard/types.d.ts +55 -8
- package/dist/shards/activate-browse.test.js +87 -3
- package/dist/shards/activate.svelte.js +9 -31
- package/dist/shards/types.d.ts +0 -15
- package/dist/shell/views/KeysAndPeers.svelte +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -10
- package/dist/documents/journal-hook.d.ts +0 -6
- package/dist/documents/journal-hook.js +0 -16
- package/dist/documents/sync/activate-integration.test.js +0 -37
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte +0 -99
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +0 -15
- package/dist/documents/sync/components/SyncGrantPicker.svelte +0 -70
- package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +0 -12
- package/dist/documents/sync/conflicts.d.ts +0 -30
- package/dist/documents/sync/conflicts.js +0 -77
- package/dist/documents/sync/conflicts.test.js +0 -71
- package/dist/documents/sync/engine.d.ts +0 -19
- package/dist/documents/sync/engine.js +0 -188
- package/dist/documents/sync/engine.test.d.ts +0 -1
- package/dist/documents/sync/engine.test.js +0 -169
- package/dist/documents/sync/handle.d.ts +0 -11
- package/dist/documents/sync/handle.js +0 -79
- package/dist/documents/sync/handle.test.js +0 -56
- package/dist/documents/sync/hash.d.ts +0 -1
- package/dist/documents/sync/hash.js +0 -13
- package/dist/documents/sync/hash.test.d.ts +0 -1
- package/dist/documents/sync/hash.test.js +0 -20
- package/dist/documents/sync/index.d.ts +0 -5
- package/dist/documents/sync/index.js +0 -10
- package/dist/documents/sync/journal.d.ts +0 -30
- package/dist/documents/sync/journal.js +0 -179
- package/dist/documents/sync/journal.test.d.ts +0 -1
- package/dist/documents/sync/journal.test.js +0 -87
- package/dist/documents/sync/observer.d.ts +0 -3
- package/dist/documents/sync/observer.js +0 -45
- package/dist/documents/sync/registry.d.ts +0 -13
- package/dist/documents/sync/registry.js +0 -73
- package/dist/documents/sync/registry.test.d.ts +0 -1
- package/dist/documents/sync/registry.test.js +0 -53
- package/dist/documents/sync/serialization.d.ts +0 -5
- package/dist/documents/sync/serialization.js +0 -24
- package/dist/documents/sync/serialization.test.d.ts +0 -1
- package/dist/documents/sync/serialization.test.js +0 -26
- package/dist/documents/sync/singleton.d.ts +0 -11
- package/dist/documents/sync/singleton.js +0 -26
- package/dist/documents/sync/tombstones.d.ts +0 -19
- package/dist/documents/sync/tombstones.js +0 -58
- package/dist/documents/sync/tombstones.test.d.ts +0 -1
- package/dist/documents/sync/tombstones.test.js +0 -37
- package/dist/documents/sync/types.d.ts +0 -116
- package/dist/documents/sync/types.js +0 -27
- package/dist/documents/sync/write-hook.test.d.ts +0 -1
- package/dist/documents/sync/write-hook.test.js +0 -36
- package/dist/server-sync.d.ts +0 -6
- package/dist/server-sync.js +0 -634
- package/dist/server-sync.js.map +0 -7
- package/dist/shards/activate-sync-registry.test.d.ts +0 -1
- package/dist/shards/activate-sync-registry.test.js +0 -42
- package/dist/testing.d.ts +0 -3
- package/dist/testing.js +0 -77
- package/dist/testing.js.map +0 -7
- /package/dist/{documents/sync/activate-integration.test.d.ts → app/store/storeShard.svelte.test.d.ts} +0 -0
- /package/dist/documents/{sync/handle.test.d.ts → handle.test.d.ts} +0 -0
- /package/dist/{documents/sync/conflicts.test.d.ts → registry/permission-descriptions.test.d.ts} +0 -0
|
@@ -10,8 +10,51 @@
|
|
|
10
10
|
* The server bundle is a separate ESM file whose default export conforms
|
|
11
11
|
* to the `ServerShard` interface.
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
14
|
-
import type {
|
|
13
|
+
import type { DocumentMeta } from '../documents/types';
|
|
14
|
+
import type { SyncPolicy, ConflictFile } from '../documents/sync-types';
|
|
15
|
+
/**
|
|
16
|
+
* Per-tenant document API exposed to server shards via
|
|
17
|
+
* `ServerShardContext.documents(tenantId)`. Every method is
|
|
18
|
+
* permission-checked by the host at call time.
|
|
19
|
+
*/
|
|
20
|
+
export interface TenantDocumentAPI {
|
|
21
|
+
read(shardId: string, path: string): Promise<string | null>;
|
|
22
|
+
exists(shardId: string, path: string): Promise<boolean>;
|
|
23
|
+
list(shardId: string): Promise<DocumentMeta[]>;
|
|
24
|
+
listAll(): Promise<Array<DocumentMeta & {
|
|
25
|
+
shardId: string;
|
|
26
|
+
}>>;
|
|
27
|
+
write(shardId: string, path: string, content: string | Uint8Array, metadata?: Record<string, unknown>): Promise<{
|
|
28
|
+
version: number;
|
|
29
|
+
syncState: 'synced' | 'pending';
|
|
30
|
+
}>;
|
|
31
|
+
delete(shardId: string, path: string): Promise<void>;
|
|
32
|
+
applyFromPeer(input: {
|
|
33
|
+
shardId: string;
|
|
34
|
+
path: string;
|
|
35
|
+
content: string | Uint8Array;
|
|
36
|
+
incomingVersion: number;
|
|
37
|
+
expectedLocalVersion: number;
|
|
38
|
+
origin: string;
|
|
39
|
+
deleted?: boolean;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
applied: true;
|
|
43
|
+
version: number;
|
|
44
|
+
} | {
|
|
45
|
+
applied: false;
|
|
46
|
+
reason: 'stale' | 'conflict' | 'conflict-extended';
|
|
47
|
+
}>;
|
|
48
|
+
getTick(): Promise<number>;
|
|
49
|
+
readPolicy(): Promise<SyncPolicy | null>;
|
|
50
|
+
writePolicy(policy: SyncPolicy): Promise<void>;
|
|
51
|
+
listConflicts(): Promise<Array<{
|
|
52
|
+
shardId: string;
|
|
53
|
+
path: string;
|
|
54
|
+
}>>;
|
|
55
|
+
readConflict(shardId: string, path: string): Promise<ConflictFile | null>;
|
|
56
|
+
resolveConflict(shardId: string, path: string, choice: 'local' | string | Uint8Array): Promise<void>;
|
|
57
|
+
}
|
|
15
58
|
/**
|
|
16
59
|
* Context provided by sh3-server when mounting a server shard's routes.
|
|
17
60
|
*/
|
|
@@ -41,14 +84,16 @@ export interface ServerShardContext {
|
|
|
41
84
|
* WebSocket upgrade registration — unchanged from 0.8.1.
|
|
42
85
|
*/
|
|
43
86
|
wsRegister?: (onConnect: (ws: any, c: any) => void) => any;
|
|
87
|
+
/** Tenant ids that currently have doc content on this server. */
|
|
88
|
+
tenants(): string[];
|
|
89
|
+
/** Per-tenant document API, permission-checked per operation. */
|
|
90
|
+
documents(tenant: string): TenantDocumentAPI;
|
|
44
91
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
92
|
+
* Declare the server's role for a tenant. Called by shards with
|
|
93
|
+
* `sync:peer` permission. No-op unless the caller holds it.
|
|
94
|
+
* Absent => 'primary' behavior at the store.
|
|
48
95
|
*/
|
|
49
|
-
|
|
50
|
-
/** Tenant-scoped SyncRegistry accessor — grant enumeration/listing. */
|
|
51
|
-
syncRegistry: (tenantId: string) => SyncRegistry;
|
|
96
|
+
setPeerRole(tenant: string, role: 'primary' | 'replica'): void;
|
|
52
97
|
}
|
|
53
98
|
/**
|
|
54
99
|
* The interface a server shard bundle must default-export.
|
|
@@ -64,6 +109,8 @@ export interface ServerShard {
|
|
|
64
109
|
* May be async if the shard needs to initialise resources before serving.
|
|
65
110
|
*/
|
|
66
111
|
routes: (router: HonoLike, context: ServerShardContext) => void | Promise<void>;
|
|
112
|
+
/** Optional shutdown hook. Called once on server SIGTERM/SIGINT. */
|
|
113
|
+
teardown?(): void | Promise<void>;
|
|
67
114
|
}
|
|
68
115
|
/**
|
|
69
116
|
* Hono MiddlewareHandler type — duplicated here to avoid importing hono
|
|
@@ -2,14 +2,14 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
2
2
|
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
3
|
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
4
|
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
-
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
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
|
__setTenantId('tenant-a');
|
|
11
11
|
});
|
|
12
|
-
it('is undefined when permission is
|
|
12
|
+
it('is undefined when no documents permission is declared', async () => {
|
|
13
13
|
let captured = null;
|
|
14
14
|
registerShard({
|
|
15
15
|
manifest: { id: 'no-browse', label: 'n', version: '0.0.0', views: [] },
|
|
@@ -18,7 +18,7 @@ describe('ctx.browse permission gating', () => {
|
|
|
18
18
|
await activateShard('no-browse');
|
|
19
19
|
expect(captured.browse).toBeUndefined();
|
|
20
20
|
});
|
|
21
|
-
it('
|
|
21
|
+
it('exposes metadata methods when documents:browse is declared', async () => {
|
|
22
22
|
var _a, _b, _c;
|
|
23
23
|
let captured = null;
|
|
24
24
|
registerShard({
|
|
@@ -33,4 +33,88 @@ describe('ctx.browse permission gating', () => {
|
|
|
33
33
|
expect(typeof ((_b = captured.browse) === null || _b === void 0 ? void 0 : _b.watchDocuments)).toBe('function');
|
|
34
34
|
expect(typeof ((_c = captured.browse) === null || _c === void 0 ? void 0 : _c.listShards)).toBe('function');
|
|
35
35
|
});
|
|
36
|
+
it('omits readFrom and writeTo when only documents:browse is declared', async () => {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
let captured = null;
|
|
39
|
+
registerShard({
|
|
40
|
+
manifest: {
|
|
41
|
+
id: 'browse-only', label: 'b', version: '0.0.0', views: [],
|
|
42
|
+
permissions: [PERMISSION_DOCUMENTS_BROWSE],
|
|
43
|
+
},
|
|
44
|
+
activate(ctx) { captured = ctx; },
|
|
45
|
+
});
|
|
46
|
+
await activateShard('browse-only');
|
|
47
|
+
expect((_a = captured.browse) === null || _a === void 0 ? void 0 : _a.readFrom).toBeUndefined();
|
|
48
|
+
expect((_b = captured.browse) === null || _b === void 0 ? void 0 : _b.writeTo).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
it('exposes readFrom when documents:browse + documents:read are declared', async () => {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
let captured = null;
|
|
53
|
+
registerShard({
|
|
54
|
+
manifest: {
|
|
55
|
+
id: 'browse-and-read', label: 'br', version: '0.0.0', views: [],
|
|
56
|
+
permissions: [PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ],
|
|
57
|
+
},
|
|
58
|
+
activate(ctx) { captured = ctx; },
|
|
59
|
+
});
|
|
60
|
+
await activateShard('browse-and-read');
|
|
61
|
+
expect(typeof ((_a = captured.browse) === null || _a === void 0 ? void 0 : _a.readFrom)).toBe('function');
|
|
62
|
+
expect((_b = captured.browse) === null || _b === void 0 ? void 0 : _b.writeTo).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
it('exposes writeTo when documents:browse + documents:write are declared', async () => {
|
|
65
|
+
var _a, _b;
|
|
66
|
+
let captured = null;
|
|
67
|
+
registerShard({
|
|
68
|
+
manifest: {
|
|
69
|
+
id: 'browse-and-write', label: 'bw', version: '0.0.0', views: [],
|
|
70
|
+
permissions: [PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_WRITE],
|
|
71
|
+
},
|
|
72
|
+
activate(ctx) { captured = ctx; },
|
|
73
|
+
});
|
|
74
|
+
await activateShard('browse-and-write');
|
|
75
|
+
expect((_a = captured.browse) === null || _a === void 0 ? void 0 : _a.readFrom).toBeUndefined();
|
|
76
|
+
expect(typeof ((_b = captured.browse) === null || _b === void 0 ? void 0 : _b.writeTo)).toBe('function');
|
|
77
|
+
});
|
|
78
|
+
it('exposes both readFrom and writeTo when all three permissions are declared', async () => {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
let captured = null;
|
|
81
|
+
registerShard({
|
|
82
|
+
manifest: {
|
|
83
|
+
id: 'full-access', label: 'full', version: '0.0.0', views: [],
|
|
84
|
+
permissions: [
|
|
85
|
+
PERMISSION_DOCUMENTS_BROWSE,
|
|
86
|
+
PERMISSION_DOCUMENTS_READ,
|
|
87
|
+
PERMISSION_DOCUMENTS_WRITE,
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
activate(ctx) { captured = ctx; },
|
|
91
|
+
});
|
|
92
|
+
await activateShard('full-access');
|
|
93
|
+
expect(typeof ((_a = captured.browse) === null || _a === void 0 ? void 0 : _a.readFrom)).toBe('function');
|
|
94
|
+
expect(typeof ((_b = captured.browse) === null || _b === void 0 ? void 0 : _b.writeTo)).toBe('function');
|
|
95
|
+
});
|
|
96
|
+
it('yields no ctx.browse when documents:read is declared without documents:browse', async () => {
|
|
97
|
+
let captured = null;
|
|
98
|
+
registerShard({
|
|
99
|
+
manifest: {
|
|
100
|
+
id: 'read-only', label: 'r', version: '0.0.0', views: [],
|
|
101
|
+
permissions: [PERMISSION_DOCUMENTS_READ],
|
|
102
|
+
},
|
|
103
|
+
activate(ctx) { captured = ctx; },
|
|
104
|
+
});
|
|
105
|
+
await activateShard('read-only');
|
|
106
|
+
expect(captured.browse).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
it('yields no ctx.browse when documents:write is declared without documents:browse', async () => {
|
|
109
|
+
let captured = null;
|
|
110
|
+
registerShard({
|
|
111
|
+
manifest: {
|
|
112
|
+
id: 'write-only', label: 'w', version: '0.0.0', views: [],
|
|
113
|
+
permissions: [PERMISSION_DOCUMENTS_WRITE],
|
|
114
|
+
},
|
|
115
|
+
activate(ctx) { captured = ctx; },
|
|
116
|
+
});
|
|
117
|
+
await activateShard('write-only');
|
|
118
|
+
expect(captured.browse).toBeUndefined();
|
|
119
|
+
});
|
|
36
120
|
});
|
|
@@ -23,12 +23,8 @@ import { fetchEnvState, putEnvState } from '../env/client';
|
|
|
23
23
|
import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
24
24
|
import { createZoneManager } from '../state/manage';
|
|
25
25
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
26
|
-
import {
|
|
27
|
-
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
26
|
+
import { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from '../documents/types';
|
|
28
27
|
import { createBrowseCapability } from '../documents/browse';
|
|
29
|
-
import { getSyncBundle } from '../documents/sync/singleton';
|
|
30
|
-
import { createSyncHandle } from '../documents/sync/handle';
|
|
31
|
-
import { createSyncRegistryAccessor } from '../documents/sync/observer';
|
|
32
28
|
import { createShardKeysApi } from '../keys/client';
|
|
33
29
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
34
30
|
import { subscribe } from '../keys/revocation-bus.svelte';
|
|
@@ -74,7 +70,7 @@ export function registerShard(shard) {
|
|
|
74
70
|
* @throws If the shard is not registered, or if a manifest view has no factory after activation.
|
|
75
71
|
*/
|
|
76
72
|
export async function activateShard(id) {
|
|
77
|
-
var _a, _b, _c, _d, _e
|
|
73
|
+
var _a, _b, _c, _d, _e;
|
|
78
74
|
const shard = registeredShards.get(id);
|
|
79
75
|
if (!shard) {
|
|
80
76
|
throw new Error(`Cannot activate shard "${id}": not registered`);
|
|
@@ -141,33 +137,15 @@ export async function activateShard(id) {
|
|
|
141
137
|
? createZoneManager()
|
|
142
138
|
: undefined,
|
|
143
139
|
browse: ((_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_BROWSE))
|
|
144
|
-
? createBrowseCapability(getTenantId(), getDocumentBackend()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: undefined,
|
|
149
|
-
sync: ((_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_SYNC))
|
|
150
|
-
? () => {
|
|
151
|
-
const backend = getDocumentBackend();
|
|
152
|
-
const tenantId = getTenantId();
|
|
153
|
-
const bundlePromise = getSyncBundle(backend, tenantId);
|
|
154
|
-
const handlePromise = bundlePromise.then(({ engine, registry }) => createSyncHandle({ tenantId, connectorId: id, engine, registry }));
|
|
155
|
-
return {
|
|
156
|
-
connectorId: id,
|
|
157
|
-
grantedScopes: async () => (await handlePromise).grantedScopes(),
|
|
158
|
-
getManifest: async (scope) => (await handlePromise).getManifest(scope),
|
|
159
|
-
changesSince: async (scope, cursor) => (await handlePromise).changesSince(scope, cursor),
|
|
160
|
-
ack: async (scope, cursor) => (await handlePromise).ack(scope, cursor),
|
|
161
|
-
apply: async (scope, entry, opts) => (await handlePromise).apply(scope, entry, opts),
|
|
162
|
-
applyBatch: async (scope, manifest, opts) => (await handlePromise).applyBatch(scope, manifest, opts),
|
|
163
|
-
forget: async (scope, path) => (await handlePromise).forget(scope, path),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
140
|
+
? createBrowseCapability(getTenantId(), getDocumentBackend(), {
|
|
141
|
+
canRead: shard.manifest.permissions.includes(PERMISSION_DOCUMENTS_READ),
|
|
142
|
+
canWrite: shard.manifest.permissions.includes(PERMISSION_DOCUMENTS_WRITE),
|
|
143
|
+
})
|
|
166
144
|
: undefined,
|
|
167
|
-
keys: ((
|
|
145
|
+
keys: ((_c = shard.manifest.permissions) === null || _c === void 0 ? void 0 : _c.includes(PERMISSION_KEYS_MINT))
|
|
168
146
|
? createShardKeysApi({
|
|
169
147
|
shardId: id,
|
|
170
|
-
shardPermissions: (
|
|
148
|
+
shardPermissions: (_d = shard.manifest.permissions) !== null && _d !== void 0 ? _d : [],
|
|
171
149
|
})
|
|
172
150
|
: undefined,
|
|
173
151
|
};
|
|
@@ -204,7 +182,7 @@ export async function activateShard(id) {
|
|
|
204
182
|
console.warn(`[sh3] Failed to hydrate env state for shard "${id}":`, err instanceof Error ? err.message : err);
|
|
205
183
|
}
|
|
206
184
|
}
|
|
207
|
-
void ((
|
|
185
|
+
void ((_e = shard.autostart) === null || _e === void 0 ? void 0 : _e.call(shard, ctx));
|
|
208
186
|
}
|
|
209
187
|
/**
|
|
210
188
|
* Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -2,8 +2,6 @@ import type { StateZones } from '../state/zones.svelte';
|
|
|
2
2
|
import type { ZoneSchema, ZoneManager } from '../state/types';
|
|
3
3
|
import type { DocumentHandle, DocumentHandleOptions } from '../documents/types';
|
|
4
4
|
import type { BrowseCapability } from '../documents/browse';
|
|
5
|
-
import type { SyncHandle } from '../documents/sync/types';
|
|
6
|
-
import type { SyncRegistry } from '../documents/sync/registry';
|
|
7
5
|
import type { EnvState } from '../env/types';
|
|
8
6
|
import type { Verb } from '../verbs/types';
|
|
9
7
|
import type { ShardContextKeys } from '../keys/types';
|
|
@@ -214,12 +212,6 @@ export interface ShardContext {
|
|
|
214
212
|
* `if (ctx.zones)` before use.
|
|
215
213
|
*/
|
|
216
214
|
zones?: ZoneManager;
|
|
217
|
-
/**
|
|
218
|
-
* Cross-shard document sync API. Only present when the shard's
|
|
219
|
-
* manifest declares the `'documents:sync'` permission. Check with
|
|
220
|
-
* `if (ctx.sync)` before use.
|
|
221
|
-
*/
|
|
222
|
-
sync?: () => SyncHandle;
|
|
223
215
|
/**
|
|
224
216
|
* Tenant-wide document browse API. Read-only enumeration and change
|
|
225
217
|
* subscription across every shard's documents for the active tenant.
|
|
@@ -228,13 +220,6 @@ export interface ShardContext {
|
|
|
228
220
|
* owning shard's own `ctx.documents()` handle.
|
|
229
221
|
*/
|
|
230
222
|
browse?: BrowseCapability;
|
|
231
|
-
/**
|
|
232
|
-
* Sync registry observer. Read-only list/revoke/conflict enumeration
|
|
233
|
-
* for explorer-class shards. Only present when the shard declares
|
|
234
|
-
* `'documents:browse'`. Granting still happens exclusively via
|
|
235
|
-
* `<SyncGrantPicker />`.
|
|
236
|
-
*/
|
|
237
|
-
syncRegistry?: () => SyncRegistry;
|
|
238
223
|
/**
|
|
239
224
|
* Mint/list/revoke keys minted by this shard. Only available when the
|
|
240
225
|
* manifest declares the `keys:mint` permission.
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
<div class="keys-peers-info">
|
|
71
71
|
<span class="keys-peers-label">{row.label}</span>
|
|
72
72
|
<span class="keys-peers-meta">
|
|
73
|
-
Minted by: {row.mintedByShardId ?? 'system'}{row.
|
|
73
|
+
Minted by: {row.mintedByShardId ?? 'system'}{row.peerRole || row.peerId ? ` \u00B7 Peer: ${row.peerRole ?? '—'}${row.peerId ? ` (${row.peerId})` : ''}` : ''}
|
|
74
74
|
</span>
|
|
75
75
|
<span class="keys-peers-meta">
|
|
76
76
|
Scopes: {row.scopes.join(', ')}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.9.1";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.9.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sh3-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -26,18 +26,10 @@
|
|
|
26
26
|
"./build": {
|
|
27
27
|
"types": "./dist/build.d.ts",
|
|
28
28
|
"default": "./dist/build.js"
|
|
29
|
-
},
|
|
30
|
-
"./server-sync": {
|
|
31
|
-
"types": "./dist/server-sync.d.ts",
|
|
32
|
-
"default": "./dist/server-sync.js"
|
|
33
|
-
},
|
|
34
|
-
"./testing": {
|
|
35
|
-
"types": "./dist/testing.d.ts",
|
|
36
|
-
"default": "./dist/testing.js"
|
|
37
29
|
}
|
|
38
30
|
},
|
|
39
31
|
"scripts": {
|
|
40
|
-
"build": "node --import tsx scripts/sync-version.ts && svelte-package -i src -o dist && node --import tsx scripts/
|
|
32
|
+
"build": "node --import tsx scripts/sync-version.ts && svelte-package -i src -o dist && node --import tsx scripts/generate-api-docs.ts",
|
|
41
33
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
42
34
|
"pack": "npm run build && npm pack",
|
|
43
35
|
"test": "vitest run --passWithNoTests",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { JournalEntry } from './sync/types';
|
|
2
|
-
type Appender = (entry: Omit<JournalEntry, 'seq' | 'ts'>) => Promise<void>;
|
|
3
|
-
export declare function setJournalAppender(fn: Appender): void;
|
|
4
|
-
export declare function clearJournalAppender(): void;
|
|
5
|
-
export declare function notifyJournal(entry: Omit<JournalEntry, 'seq' | 'ts'>): Promise<void>;
|
|
6
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Journal appender hook — lets the sync engine subscribe to regular
|
|
3
|
-
* shard writes/deletes without creating an import cycle between the
|
|
4
|
-
* document handle and the sync subsystem.
|
|
5
|
-
*/
|
|
6
|
-
let appender = null;
|
|
7
|
-
export function setJournalAppender(fn) {
|
|
8
|
-
appender = fn;
|
|
9
|
-
}
|
|
10
|
-
export function clearJournalAppender() {
|
|
11
|
-
appender = null;
|
|
12
|
-
}
|
|
13
|
-
export async function notifyJournal(entry) {
|
|
14
|
-
if (appender)
|
|
15
|
-
await appender(entry);
|
|
16
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { MemoryDocumentBackend } from '../backends';
|
|
3
|
-
import { __setDocumentBackend, __setTenantId } from '../config';
|
|
4
|
-
import { __resetShardRegistryForTest, registerShard, activateShard } from '../../shards/activate.svelte';
|
|
5
|
-
import { __resetSyncBundlesForTest } from './singleton';
|
|
6
|
-
import { PERMISSION_DOCUMENTS_SYNC } from './types';
|
|
7
|
-
describe('ctx.sync() gating', () => {
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
__resetShardRegistryForTest();
|
|
10
|
-
__resetSyncBundlesForTest();
|
|
11
|
-
__setDocumentBackend(new MemoryDocumentBackend());
|
|
12
|
-
__setTenantId('tenant-a');
|
|
13
|
-
});
|
|
14
|
-
it('is undefined without documents:sync permission', async () => {
|
|
15
|
-
let captured;
|
|
16
|
-
const shard = {
|
|
17
|
-
manifest: { id: 's-none', version: '0', views: [] },
|
|
18
|
-
activate: async (ctx) => { captured = ctx; },
|
|
19
|
-
};
|
|
20
|
-
registerShard(shard);
|
|
21
|
-
await activateShard('s-none');
|
|
22
|
-
expect(captured.sync).toBeUndefined();
|
|
23
|
-
});
|
|
24
|
-
it('is a function when documents:sync is declared', async () => {
|
|
25
|
-
let captured;
|
|
26
|
-
const shard = {
|
|
27
|
-
manifest: { id: 's-sync', version: '0', views: [], permissions: [PERMISSION_DOCUMENTS_SYNC] },
|
|
28
|
-
activate: async (ctx) => { captured = ctx; },
|
|
29
|
-
};
|
|
30
|
-
registerShard(shard);
|
|
31
|
-
await activateShard('s-sync');
|
|
32
|
-
expect(typeof captured.sync).toBe('function');
|
|
33
|
-
const h = captured.sync();
|
|
34
|
-
expect(h.connectorId).toBe('s-sync');
|
|
35
|
-
expect(await h.grantedScopes()).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
import SyncGrantPicker from './SyncGrantPicker.svelte';
|
|
4
|
-
import { createSyncRegistry, type SyncRegistry } from '../registry';
|
|
5
|
-
import { getDocumentBackend, getTenantId } from '../../config';
|
|
6
|
-
import type { GrantRecord, SyncScope, ConflictResolution } from '../types';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
/** Optional connector-specific filter; if omitted, shows everything. */
|
|
10
|
-
connectorId?: string;
|
|
11
|
-
/** Shard IDs whose conflict artifacts should be listed. */
|
|
12
|
-
conflictShardIds?: string[];
|
|
13
|
-
/** Pending grant request, if any — embeds the picker when set. */
|
|
14
|
-
pendingRequest?: { connectorId: string; scope: SyncScope };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let { connectorId, conflictShardIds = [], pendingRequest }: Props = $props();
|
|
18
|
-
|
|
19
|
-
let registry: SyncRegistry | null = $state(null);
|
|
20
|
-
let grants: GrantRecord[] = $state([]);
|
|
21
|
-
let conflicts: ConflictResolution[] = $state([]);
|
|
22
|
-
|
|
23
|
-
async function refresh() {
|
|
24
|
-
if (!registry) return;
|
|
25
|
-
grants = await registry.list(connectorId);
|
|
26
|
-
const all: ConflictResolution[] = [];
|
|
27
|
-
for (const shardId of conflictShardIds) {
|
|
28
|
-
all.push(...await registry.listConflicts(shardId));
|
|
29
|
-
}
|
|
30
|
-
conflicts = all;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
onMount(async () => {
|
|
34
|
-
registry = createSyncRegistry(getDocumentBackend(), getTenantId());
|
|
35
|
-
await refresh();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
async function revoke(record: GrantRecord) {
|
|
39
|
-
if (!registry) return;
|
|
40
|
-
await registry.revoke(record.connectorId, record.scope);
|
|
41
|
-
await refresh();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function describeScope(s: SyncScope): string {
|
|
45
|
-
if (s.kind === 'tenant') return 'entire tenant';
|
|
46
|
-
if (s.kind === 'shard') return `shard:${s.shardId}`;
|
|
47
|
-
return `shard:${s.shardId}/${s.prefix}`;
|
|
48
|
-
}
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<section class="document-sync-explorer" part="container">
|
|
52
|
-
<h2 part="title">Document Sync</h2>
|
|
53
|
-
|
|
54
|
-
{#if pendingRequest}
|
|
55
|
-
<SyncGrantPicker
|
|
56
|
-
connectorId={pendingRequest.connectorId}
|
|
57
|
-
scope={pendingRequest.scope}
|
|
58
|
-
onGranted={refresh}
|
|
59
|
-
/>
|
|
60
|
-
{/if}
|
|
61
|
-
|
|
62
|
-
<h3 part="subtitle">Granted scopes</h3>
|
|
63
|
-
{#if grants.length === 0}
|
|
64
|
-
<p part="empty">No scopes granted yet.</p>
|
|
65
|
-
{:else}
|
|
66
|
-
<ul part="grants">
|
|
67
|
-
{#each grants as g}
|
|
68
|
-
<li>
|
|
69
|
-
<span part="grant-connector">{g.connectorId}</span>
|
|
70
|
-
<span part="grant-scope">{describeScope(g.scope)}</span>
|
|
71
|
-
<button type="button" onclick={() => revoke(g)} part="revoke">Revoke</button>
|
|
72
|
-
</li>
|
|
73
|
-
{/each}
|
|
74
|
-
</ul>
|
|
75
|
-
{/if}
|
|
76
|
-
|
|
77
|
-
<h3 part="subtitle">Conflicts</h3>
|
|
78
|
-
{#if conflicts.length === 0}
|
|
79
|
-
<p part="empty">No active conflicts.</p>
|
|
80
|
-
{:else}
|
|
81
|
-
<ul part="conflicts">
|
|
82
|
-
{#each conflicts as c}
|
|
83
|
-
<li>
|
|
84
|
-
<code part="conflict-path">{c.shardId}:{c.path}</code>
|
|
85
|
-
<small part="conflict-artifact">{c.conflictArtifactPath}</small>
|
|
86
|
-
</li>
|
|
87
|
-
{/each}
|
|
88
|
-
</ul>
|
|
89
|
-
{/if}
|
|
90
|
-
</section>
|
|
91
|
-
|
|
92
|
-
<style>
|
|
93
|
-
.document-sync-explorer {
|
|
94
|
-
display: grid;
|
|
95
|
-
gap: 0.75rem;
|
|
96
|
-
}
|
|
97
|
-
ul { list-style: none; padding: 0; margin: 0; }
|
|
98
|
-
li { display: flex; gap: 0.5rem; align-items: center; }
|
|
99
|
-
</style>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { SyncScope } from '../types';
|
|
2
|
-
interface Props {
|
|
3
|
-
/** Optional connector-specific filter; if omitted, shows everything. */
|
|
4
|
-
connectorId?: string;
|
|
5
|
-
/** Shard IDs whose conflict artifacts should be listed. */
|
|
6
|
-
conflictShardIds?: string[];
|
|
7
|
-
/** Pending grant request, if any — embeds the picker when set. */
|
|
8
|
-
pendingRequest?: {
|
|
9
|
-
connectorId: string;
|
|
10
|
-
scope: SyncScope;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
declare const DocumentSyncExplorer: import("svelte").Component<Props, {}, "">;
|
|
14
|
-
type DocumentSyncExplorer = ReturnType<typeof DocumentSyncExplorer>;
|
|
15
|
-
export default DocumentSyncExplorer;
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getDocumentBackend, getTenantId } from '../../config';
|
|
3
|
-
import { __grantInternal } from '../registry';
|
|
4
|
-
import type { SyncScope } from '../types';
|
|
5
|
-
|
|
6
|
-
import type { Snippet } from 'svelte';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
connectorId: string;
|
|
10
|
-
scope: SyncScope;
|
|
11
|
-
onGranted?: () => void;
|
|
12
|
-
onCancel?: () => void;
|
|
13
|
-
rationale?: Snippet;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let { connectorId, scope, onGranted, onCancel, rationale }: Props = $props();
|
|
17
|
-
|
|
18
|
-
let pending = $state(false);
|
|
19
|
-
let error = $state<string | null>(null);
|
|
20
|
-
|
|
21
|
-
function describe(s: SyncScope): string {
|
|
22
|
-
switch (s.kind) {
|
|
23
|
-
case 'tenant': return 'all your documents across every shard';
|
|
24
|
-
case 'shard': return `all documents for shard "${s.shardId}"`;
|
|
25
|
-
case 'path': return `documents under "${s.prefix}" in shard "${s.shardId}"`;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function confirm() {
|
|
30
|
-
pending = true;
|
|
31
|
-
error = null;
|
|
32
|
-
try {
|
|
33
|
-
await __grantInternal(getDocumentBackend(), getTenantId(), connectorId, scope);
|
|
34
|
-
onGranted?.();
|
|
35
|
-
} catch (e) {
|
|
36
|
-
error = e instanceof Error ? e.message : String(e);
|
|
37
|
-
} finally {
|
|
38
|
-
pending = false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<section class="sync-grant-picker" part="container">
|
|
44
|
-
<header part="header">
|
|
45
|
-
<h3 part="title">Grant sync access</h3>
|
|
46
|
-
</header>
|
|
47
|
-
<p part="summary">
|
|
48
|
-
<strong>{connectorId}</strong> is requesting access to {describe(scope)}.
|
|
49
|
-
</p>
|
|
50
|
-
{#if rationale}{@render rationale()}{/if}
|
|
51
|
-
{#if error}
|
|
52
|
-
<p class="error" part="error">{error}</p>
|
|
53
|
-
{/if}
|
|
54
|
-
<footer part="actions">
|
|
55
|
-
<button type="button" disabled={pending} onclick={() => onCancel?.()} part="cancel">Cancel</button>
|
|
56
|
-
<button type="button" disabled={pending} onclick={confirm} part="confirm">Grant</button>
|
|
57
|
-
</footer>
|
|
58
|
-
</section>
|
|
59
|
-
|
|
60
|
-
<style>
|
|
61
|
-
.sync-grant-picker {
|
|
62
|
-
display: grid;
|
|
63
|
-
gap: 0.75rem;
|
|
64
|
-
padding: 1rem;
|
|
65
|
-
border: 1px solid var(--sh3-border, #444);
|
|
66
|
-
border-radius: 6px;
|
|
67
|
-
}
|
|
68
|
-
.error { color: var(--sh3-error, #c00); }
|
|
69
|
-
footer { display: flex; gap: 0.5rem; justify-content: flex-end; }
|
|
70
|
-
</style>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { SyncScope } from '../types';
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
interface Props {
|
|
4
|
-
connectorId: string;
|
|
5
|
-
scope: SyncScope;
|
|
6
|
-
onGranted?: () => void;
|
|
7
|
-
onCancel?: () => void;
|
|
8
|
-
rationale?: Snippet;
|
|
9
|
-
}
|
|
10
|
-
declare const SyncGrantPicker: import("svelte").Component<Props, {}, "">;
|
|
11
|
-
type SyncGrantPicker = ReturnType<typeof SyncGrantPicker>;
|
|
12
|
-
export default SyncGrantPicker;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { DocumentBackend } from '../types';
|
|
2
|
-
import type { ConflictPolicy, ConflictResolution } from './types';
|
|
3
|
-
interface ConflictInput {
|
|
4
|
-
connectorId: string;
|
|
5
|
-
shardId: string;
|
|
6
|
-
path: string;
|
|
7
|
-
localHash: string;
|
|
8
|
-
remoteHash: string;
|
|
9
|
-
remoteContent?: string | ArrayBuffer;
|
|
10
|
-
baseHash?: string;
|
|
11
|
-
}
|
|
12
|
-
export type ConflictAction = {
|
|
13
|
-
action: 'apply-remote';
|
|
14
|
-
asPath?: string;
|
|
15
|
-
} | {
|
|
16
|
-
action: 'skip';
|
|
17
|
-
} | {
|
|
18
|
-
action: 'conflict';
|
|
19
|
-
resolution: ConflictResolution;
|
|
20
|
-
};
|
|
21
|
-
export declare class ConflictManager {
|
|
22
|
-
private backend;
|
|
23
|
-
private tenantId;
|
|
24
|
-
constructor(backend: DocumentBackend, tenantId: string);
|
|
25
|
-
resolve(policy: ConflictPolicy, input: ConflictInput): Promise<ConflictAction>;
|
|
26
|
-
getBaseHash(connectorId: string, shardId: string, path: string): Promise<string | null>;
|
|
27
|
-
setBaseHash(connectorId: string, shardId: string, path: string, hash: string): Promise<void>;
|
|
28
|
-
listConflicts(shardId: string): Promise<ConflictResolution[]>;
|
|
29
|
-
}
|
|
30
|
-
export {};
|