sh3-core 0.8.0 → 0.8.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/Shell.svelte +19 -0
- package/dist/api.d.ts +5 -1
- package/dist/api.js +6 -1
- package/dist/app/admin/ApiKeysView.svelte +16 -27
- package/dist/app/admin/SystemView.svelte +149 -11
- package/dist/documents/backends.d.ts +8 -0
- package/dist/documents/backends.js +87 -0
- package/dist/documents/backends.test.d.ts +1 -0
- package/dist/documents/backends.test.js +33 -0
- package/dist/documents/browse.d.ts +12 -0
- package/dist/documents/browse.js +19 -0
- package/dist/documents/browse.test.d.ts +1 -0
- package/dist/documents/browse.test.js +41 -0
- package/dist/documents/http-backend.d.ts +4 -0
- package/dist/documents/http-backend.js +14 -0
- package/dist/documents/sync/index.d.ts +1 -2
- package/dist/documents/sync/index.js +0 -2
- package/dist/documents/sync/observer.d.ts +3 -0
- package/dist/documents/sync/observer.js +45 -0
- package/dist/documents/sync/registry.d.ts +3 -0
- package/dist/documents/sync/registry.js +8 -1
- package/dist/documents/sync/registry.test.js +11 -0
- package/dist/documents/types.d.ts +18 -0
- package/dist/documents/types.js +6 -1
- package/dist/keys/ConsentDialog.svelte +176 -0
- package/dist/keys/ConsentDialog.svelte.d.ts +3 -0
- package/dist/keys/client.d.ts +13 -0
- package/dist/keys/client.js +65 -0
- package/dist/keys/client.test.d.ts +1 -0
- package/dist/keys/client.test.js +44 -0
- package/dist/keys/consent.svelte.d.ts +16 -0
- package/dist/keys/consent.svelte.js +29 -0
- package/dist/keys/consent.test.d.ts +1 -0
- package/dist/keys/consent.test.js +53 -0
- package/dist/keys/revocation-bus.svelte.d.ts +35 -0
- package/dist/keys/revocation-bus.svelte.js +92 -0
- package/dist/keys/revocation-bus.test.d.ts +1 -0
- package/dist/keys/revocation-bus.test.js +95 -0
- package/dist/keys/types.d.ts +32 -0
- package/dist/keys/types.js +13 -0
- package/dist/layout/inspection.d.ts +17 -0
- package/dist/layout/inspection.js +53 -0
- package/dist/server-shard/types.d.ts +21 -2
- package/dist/server-sync.d.ts +6 -0
- package/dist/server-sync.js +634 -0
- package/dist/server-sync.js.map +7 -0
- package/dist/sh3core-shard/ShellHome.svelte +140 -63
- package/dist/sh3core-shard/sh3coreShard.svelte.js +12 -1
- package/dist/shards/activate-browse.test.d.ts +1 -0
- package/dist/shards/activate-browse.test.js +36 -0
- package/dist/shards/activate-on-key-revoked.test.d.ts +1 -0
- package/dist/shards/activate-on-key-revoked.test.js +60 -0
- package/dist/shards/activate-sync-registry.test.d.ts +1 -0
- package/dist/shards/activate-sync-registry.test.js +42 -0
- package/dist/shards/activate-tenantid.test.d.ts +1 -0
- package/dist/shards/activate-tenantid.test.js +21 -0
- package/dist/shards/activate.svelte.d.ts +12 -0
- package/dist/shards/activate.svelte.js +55 -3
- package/dist/shards/types.d.ts +42 -0
- package/dist/shards/types.js +1 -1
- package/dist/shell/views/KeysAndPeers.svelte +110 -0
- package/dist/shell/views/KeysAndPeers.svelte.d.ts +3 -0
- package/dist/shell-shard/Terminal.svelte +0 -11
- package/dist/shell-shard/manifest.js +1 -1
- package/dist/shell-shard/shellShard.svelte.js +52 -4
- package/dist/shell-shard/toolbar/Toolbar.svelte +11 -32
- package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +0 -2
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +29 -62
- package/dist/shell-shard/verbs/index.js +3 -1
- package/dist/shell-shard/verbs/views.d.ts +2 -0
- package/dist/shell-shard/verbs/views.js +103 -2
- package/dist/testing.d.ts +3 -0
- package/dist/testing.js +77 -0
- package/dist/testing.js.map +7 -0
- package/dist/verbs/types.d.ts +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notifies active shards when one of their minted keys is revoked —
|
|
3
|
+
* regardless of whether the revocation was initiated by the shell UI,
|
|
4
|
+
* the shard itself, or another tab.
|
|
5
|
+
*
|
|
6
|
+
* The bus is populated by a server-sent events stream on /api/keys/events
|
|
7
|
+
* (wired by the shell runtime at boot) and/or by local revoke() calls.
|
|
8
|
+
*/
|
|
9
|
+
const handlersByShard = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Recently-emitted (shardId → Set<keyId>) with per-entry TTL timers.
|
|
12
|
+
* Prevents double-firing when a local emit() is followed by the same
|
|
13
|
+
* event arriving via SSE (or vice-versa) within the dedup window.
|
|
14
|
+
*/
|
|
15
|
+
const recentByShard = new Map();
|
|
16
|
+
const DEDUP_TTL_MS = 5000;
|
|
17
|
+
function markRecent(shardId, keyId) {
|
|
18
|
+
let set = recentByShard.get(shardId);
|
|
19
|
+
if (!set) {
|
|
20
|
+
set = new Set();
|
|
21
|
+
recentByShard.set(shardId, set);
|
|
22
|
+
}
|
|
23
|
+
set.add(keyId);
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
set.delete(keyId);
|
|
26
|
+
if (set.size === 0)
|
|
27
|
+
recentByShard.delete(shardId);
|
|
28
|
+
}, DEDUP_TTL_MS);
|
|
29
|
+
}
|
|
30
|
+
function isRecent(shardId, keyId) {
|
|
31
|
+
var _a, _b;
|
|
32
|
+
return (_b = (_a = recentByShard.get(shardId)) === null || _a === void 0 ? void 0 : _a.has(keyId)) !== null && _b !== void 0 ? _b : false;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to revocation events for a specific shard, or for all shards
|
|
36
|
+
* by passing `'*'` as the shardId.
|
|
37
|
+
* Returns an unsubscribe function.
|
|
38
|
+
*/
|
|
39
|
+
export function subscribe(shardId, handler) {
|
|
40
|
+
var _a;
|
|
41
|
+
const set = (_a = handlersByShard.get(shardId)) !== null && _a !== void 0 ? _a : new Set();
|
|
42
|
+
set.add(handler);
|
|
43
|
+
handlersByShard.set(shardId, set);
|
|
44
|
+
return () => { set.delete(handler); };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Emit a revocation event for the given shardId.
|
|
48
|
+
* No-op if shardId is null or has no subscribers.
|
|
49
|
+
* Deduplicates events: if the same (shardId, keyId) pair was already emitted
|
|
50
|
+
* within the last 5 s it is silently dropped (covers local-then-SSE and
|
|
51
|
+
* SSE-then-local orderings).
|
|
52
|
+
* Internal use only — called by client.ts and the SSE stream bootstrap.
|
|
53
|
+
*/
|
|
54
|
+
export function emit(shardId, keyId) {
|
|
55
|
+
if (!shardId)
|
|
56
|
+
return;
|
|
57
|
+
if (isRecent(shardId, keyId))
|
|
58
|
+
return;
|
|
59
|
+
markRecent(shardId, keyId);
|
|
60
|
+
const set = handlersByShard.get(shardId);
|
|
61
|
+
if (set)
|
|
62
|
+
for (const h of set)
|
|
63
|
+
h(keyId);
|
|
64
|
+
const star = handlersByShard.get('*');
|
|
65
|
+
if (star)
|
|
66
|
+
for (const h of star)
|
|
67
|
+
h(keyId);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Open a server-sent events stream at /api/keys/events and forward
|
|
71
|
+
* revocation events into the local bus.
|
|
72
|
+
*
|
|
73
|
+
* Should be called once at shell boot. Returns a cleanup function that
|
|
74
|
+
* closes the EventSource.
|
|
75
|
+
*
|
|
76
|
+
* No-op in environments without EventSource (e.g. Node test runner).
|
|
77
|
+
*/
|
|
78
|
+
export function startServerSideStream() {
|
|
79
|
+
if (typeof EventSource === 'undefined')
|
|
80
|
+
return () => { };
|
|
81
|
+
const es = new EventSource('/api/keys/events', { withCredentials: true });
|
|
82
|
+
es.onmessage = (msg) => {
|
|
83
|
+
try {
|
|
84
|
+
const ev = JSON.parse(msg.data);
|
|
85
|
+
emit(ev.shardId, ev.id);
|
|
86
|
+
}
|
|
87
|
+
catch (_a) {
|
|
88
|
+
// Ignore malformed messages.
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return () => es.close();
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { subscribe, emit } from './revocation-bus.svelte';
|
|
3
|
+
// Use fake timers so dedup TTL tests don't wait 5 s.
|
|
4
|
+
// Individual tests that need it call vi.useFakeTimers(); others rely on real timers.
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
describe('revocation-bus', () => {
|
|
9
|
+
it('fires the handler for the correct shardId', () => {
|
|
10
|
+
const received = [];
|
|
11
|
+
const off = subscribe('my-shard', (id) => received.push(id));
|
|
12
|
+
try {
|
|
13
|
+
emit('my-shard', 'key-1');
|
|
14
|
+
expect(received).toEqual(['key-1']);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
off();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
it('does not fire for a different shardId', () => {
|
|
21
|
+
const received = [];
|
|
22
|
+
const off = subscribe('shard-a', (id) => received.push(id));
|
|
23
|
+
try {
|
|
24
|
+
emit('shard-b', 'key-1');
|
|
25
|
+
expect(received).toHaveLength(0);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
off();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('does not fire after unsubscribe', () => {
|
|
32
|
+
const received = [];
|
|
33
|
+
const off = subscribe('my-shard', (id) => received.push(id));
|
|
34
|
+
off();
|
|
35
|
+
emit('my-shard', 'key-1');
|
|
36
|
+
expect(received).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
it('is a no-op when shardId is null', () => {
|
|
39
|
+
// Should not throw.
|
|
40
|
+
expect(() => emit(null, 'key-1')).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
it('supports multiple subscribers for the same shard', () => {
|
|
43
|
+
const a = [];
|
|
44
|
+
const b = [];
|
|
45
|
+
const offA = subscribe('shared', (id) => a.push(id));
|
|
46
|
+
const offB = subscribe('shared', (id) => b.push(id));
|
|
47
|
+
try {
|
|
48
|
+
emit('shared', 'k');
|
|
49
|
+
expect(a).toEqual(['k']);
|
|
50
|
+
expect(b).toEqual(['k']);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
offA();
|
|
54
|
+
offB();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it('deduplicates back-to-back emits for the same (shard, key) pair', () => {
|
|
58
|
+
vi.useFakeTimers();
|
|
59
|
+
try {
|
|
60
|
+
const received = [];
|
|
61
|
+
const off = subscribe('dedup-shard', (id) => received.push(id));
|
|
62
|
+
try {
|
|
63
|
+
emit('dedup-shard', 'key-dup');
|
|
64
|
+
emit('dedup-shard', 'key-dup'); // duplicate within TTL window
|
|
65
|
+
expect(received).toHaveLength(1);
|
|
66
|
+
expect(received[0]).toBe('key-dup');
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
off();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
vi.useRealTimers();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
it('re-fires after the dedup TTL has elapsed', () => {
|
|
77
|
+
vi.useFakeTimers();
|
|
78
|
+
try {
|
|
79
|
+
const received = [];
|
|
80
|
+
const off = subscribe('dedup-ttl-shard', (id) => received.push(id));
|
|
81
|
+
try {
|
|
82
|
+
emit('dedup-ttl-shard', 'key-x');
|
|
83
|
+
vi.advanceTimersByTime(6000); // past the 5 s TTL
|
|
84
|
+
emit('dedup-ttl-shard', 'key-x');
|
|
85
|
+
expect(received).toHaveLength(2);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
off();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
vi.useRealTimers();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const PERMISSION_KEYS_MINT = "keys:mint";
|
|
2
|
+
export interface ApiKeyPublic {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
tenantId: string | null;
|
|
6
|
+
ownerUserId: string | null;
|
|
7
|
+
mintedByShardId: string | null;
|
|
8
|
+
scopes: string[];
|
|
9
|
+
connectorId?: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
expiresAt?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MintOpts {
|
|
14
|
+
label: string;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
connectorId?: string;
|
|
17
|
+
expiresIn?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ShardContextKeys {
|
|
20
|
+
mint(opts: MintOpts): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
key: string;
|
|
23
|
+
}>;
|
|
24
|
+
list(): Promise<ApiKeyPublic[]>;
|
|
25
|
+
revoke(id: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare class ScopeEscalationError extends Error {
|
|
28
|
+
constructor(scope: string);
|
|
29
|
+
}
|
|
30
|
+
export declare class ConsentDeniedError extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const PERMISSION_KEYS_MINT = 'keys:mint';
|
|
2
|
+
export class ScopeEscalationError extends Error {
|
|
3
|
+
constructor(scope) {
|
|
4
|
+
super(`Shard may not mint a key with scope '${scope}' — not declared in manifest.`);
|
|
5
|
+
this.name = 'ScopeEscalationError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class ConsentDeniedError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super('User denied key creation.');
|
|
11
|
+
this.name = 'ConsentDeniedError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -50,6 +50,23 @@ export declare function expandChild(splitPath: number[], childIndex: number): bo
|
|
|
50
50
|
* the sole authority on tree mutations.
|
|
51
51
|
*/
|
|
52
52
|
export declare function closeTab(slotId: string): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Pop a docked tab out into a new float. Locates the tab by `slotId` in
|
|
55
|
+
* the currently-rendered docked tree, removes it (preserving viewId +
|
|
56
|
+
* meta), and opens a float with the same view. Returns the new floatId
|
|
57
|
+
* on success, or null if the slot wasn't found in the docked tree.
|
|
58
|
+
*
|
|
59
|
+
* Guarded canClose() is NOT consulted — popout is not a close. The slot
|
|
60
|
+
* is recreated in the float with a fresh slotId, so the view is remounted.
|
|
61
|
+
*/
|
|
62
|
+
export declare function popoutView(slotId: string): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Dock a float back into the currently-rendered layout. The float's
|
|
65
|
+
* active tab is appended to the first tabs group (same policy as
|
|
66
|
+
* `dockIntoActiveLayout`). Returns true on success. The float is closed
|
|
67
|
+
* after its content is transferred.
|
|
68
|
+
*/
|
|
69
|
+
export declare function dockFloat(floatId: string): boolean;
|
|
53
70
|
/**
|
|
54
71
|
* Dock a view into the currently-rendered layout without caring which
|
|
55
72
|
* root it is. Used by the Ctrl+` shell hotkey and other "just put it
|
|
@@ -194,6 +194,59 @@ async function closeFloatTab(tree, slotId) {
|
|
|
194
194
|
}
|
|
195
195
|
return false;
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Pop a docked tab out into a new float. Locates the tab by `slotId` in
|
|
199
|
+
* the currently-rendered docked tree, removes it (preserving viewId +
|
|
200
|
+
* meta), and opens a float with the same view. Returns the new floatId
|
|
201
|
+
* on success, or null if the slot wasn't found in the docked tree.
|
|
202
|
+
*
|
|
203
|
+
* Guarded canClose() is NOT consulted — popout is not a close. The slot
|
|
204
|
+
* is recreated in the float with a fresh slotId, so the view is remounted.
|
|
205
|
+
*/
|
|
206
|
+
export function popoutView(slotId) {
|
|
207
|
+
const tree = activeLayout();
|
|
208
|
+
const located = findTabBySlotId(tree.docked, slotId);
|
|
209
|
+
if (!located)
|
|
210
|
+
return null;
|
|
211
|
+
const entry = located.entry;
|
|
212
|
+
const viewId = entry.viewId;
|
|
213
|
+
if (!viewId)
|
|
214
|
+
return null;
|
|
215
|
+
const title = entry.label;
|
|
216
|
+
const meta = entry.meta;
|
|
217
|
+
removeTabBySlotId(tree.docked, slotId);
|
|
218
|
+
cleanupTree(tree.docked);
|
|
219
|
+
return floatManager.open(viewId, { title, meta });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Dock a float back into the currently-rendered layout. The float's
|
|
223
|
+
* active tab is appended to the first tabs group (same policy as
|
|
224
|
+
* `dockIntoActiveLayout`). Returns true on success. The float is closed
|
|
225
|
+
* after its content is transferred.
|
|
226
|
+
*/
|
|
227
|
+
export function dockFloat(floatId) {
|
|
228
|
+
var _a, _b;
|
|
229
|
+
const tree = activeLayout();
|
|
230
|
+
const floatEntry = tree.floats.find((f) => f.id === floatId);
|
|
231
|
+
if (!floatEntry)
|
|
232
|
+
return false;
|
|
233
|
+
const content = floatEntry.content;
|
|
234
|
+
const tabs = content.type === 'tabs' ? content : null;
|
|
235
|
+
if (!tabs || tabs.tabs.length === 0) {
|
|
236
|
+
floatManager.close(floatId);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
const entry = (_b = tabs.tabs[(_a = tabs.activeTab) !== null && _a !== void 0 ? _a : 0]) !== null && _b !== void 0 ? _b : tabs.tabs[0];
|
|
240
|
+
const ok = dockIntoActiveLayout({
|
|
241
|
+
slotId: entry.slotId,
|
|
242
|
+
viewId: entry.viewId,
|
|
243
|
+
label: entry.label,
|
|
244
|
+
meta: entry.meta,
|
|
245
|
+
});
|
|
246
|
+
if (ok)
|
|
247
|
+
floatManager.close(floatId);
|
|
248
|
+
return ok;
|
|
249
|
+
}
|
|
197
250
|
function findFirstTabsNode(node) {
|
|
198
251
|
if (node.type === 'tabs')
|
|
199
252
|
return node;
|
|
@@ -10,6 +10,8 @@
|
|
|
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 { SyncHandle } from '../documents/sync/types';
|
|
14
|
+
import type { SyncRegistry } from '../documents/sync/registry';
|
|
13
15
|
/**
|
|
14
16
|
* Context provided by sh3-server when mounting a server shard's routes.
|
|
15
17
|
*/
|
|
@@ -22,14 +24,31 @@ export interface ServerShardContext {
|
|
|
22
24
|
* Path: `<dataDir>/shards/<shard-id>/`
|
|
23
25
|
*/
|
|
24
26
|
dataDir: string;
|
|
27
|
+
/** Permission strings declared in the paired client manifest, empty if no manifest. */
|
|
28
|
+
permissions: string[];
|
|
25
29
|
/**
|
|
26
30
|
* Hono middleware that rejects non-admin callers.
|
|
27
|
-
*
|
|
28
|
-
* should not use this.
|
|
31
|
+
* Backwards-compatible alias for scopeRequired('admin:*'). Prefer the generic form.
|
|
29
32
|
*
|
|
30
33
|
* Usage: `router.post('/publish', ctx.adminOnly, handler)`
|
|
31
34
|
*/
|
|
32
35
|
adminOnly: MiddlewareHandler;
|
|
36
|
+
/** 403 unless caller has the named scope (or admin:*). */
|
|
37
|
+
scopeRequired: (scope: string) => MiddlewareHandler;
|
|
38
|
+
/** 401 unless caller has a non-null tenantId. */
|
|
39
|
+
tenantRequired: MiddlewareHandler;
|
|
40
|
+
/**
|
|
41
|
+
* WebSocket upgrade registration — unchanged from 0.8.1.
|
|
42
|
+
*/
|
|
43
|
+
wsRegister?: (onConnect: (ws: any, c: any) => void) => any;
|
|
44
|
+
/**
|
|
45
|
+
* Obtain a tenant-scoped SyncHandle from a server-side route handler.
|
|
46
|
+
* The handle enforces GrantRecord checks exactly like the client-side
|
|
47
|
+
* one. Throws if the shard's manifest does not declare `documents:sync`.
|
|
48
|
+
*/
|
|
49
|
+
sync: (tenantId: string, connectorId: string) => Promise<SyncHandle>;
|
|
50
|
+
/** Tenant-scoped SyncRegistry accessor — grant enumeration/listing. */
|
|
51
|
+
syncRegistry: (tenantId: string) => SyncRegistry;
|
|
33
52
|
}
|
|
34
53
|
/**
|
|
35
54
|
* The interface a server shard bundle must default-export.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { getSyncBundle } from './documents/sync/singleton';
|
|
2
|
+
export { createSyncHandle } from './documents/sync/handle';
|
|
3
|
+
export { createSyncRegistry } from './documents/sync/registry';
|
|
4
|
+
export type { SyncHandle } from './documents/sync/types';
|
|
5
|
+
export type { SyncRegistry } from './documents/sync/registry';
|
|
6
|
+
export type { DocumentBackend, DocumentMeta } from './documents/types';
|