sh3-core 0.8.1 → 0.9.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/Shell.svelte +19 -0
- package/dist/api.d.ts +6 -6
- package/dist/api.js +6 -3
- package/dist/app/admin/ApiKeysView.svelte +16 -27
- 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/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 +40 -2
- package/dist/documents/types.js +3 -2
- 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.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.js +54 -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.js +95 -0
- package/dist/keys/types.d.ts +34 -0
- package/dist/keys/types.js +13 -0
- package/dist/server-shard/types.d.ts +68 -2
- package/dist/sh3core-shard/ShellHome.svelte +140 -63
- package/dist/sh3core-shard/sh3coreShard.svelte.js +12 -1
- package/dist/shards/activate-on-key-revoked.test.js +60 -0
- package/dist/shards/activate.svelte.js +21 -24
- package/dist/shards/types.d.ts +7 -13
- 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/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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- 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.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.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/shards/activate-sync-registry.test.d.ts +0 -1
- package/dist/shards/activate-sync-registry.test.js +0 -42
- /package/dist/documents/{sync/handle.test.d.ts → handle.test.d.ts} +0 -0
- /package/dist/{documents/sync/activate-integration.test.d.ts → keys/client.test.d.ts} +0 -0
- /package/dist/{documents/sync/conflicts.test.d.ts → keys/consent.test.d.ts} +0 -0
- /package/dist/{documents/sync/engine.test.d.ts → keys/revocation-bus.test.d.ts} +0 -0
- /package/dist/{documents/sync/hash.test.d.ts → shards/activate-on-key-revoked.test.d.ts} +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Keys & Peers view — lists every API key minted for the current user's
|
|
4
|
+
* tenant (admin keys excluded) and allows individual revocation.
|
|
5
|
+
*
|
|
6
|
+
* Reachable from settings. No create-key button; keys are only minted
|
|
7
|
+
* in-shard via the consent flow.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ApiKeyPublic } from '../../keys/types';
|
|
11
|
+
import { subscribe as subscribeBus } from '../../keys/revocation-bus.svelte';
|
|
12
|
+
|
|
13
|
+
let rows = $state<ApiKeyPublic[]>([]);
|
|
14
|
+
let loadError = $state<string | null>(null);
|
|
15
|
+
let loading = $state(true);
|
|
16
|
+
let confirmingId = $state<string | null>(null);
|
|
17
|
+
|
|
18
|
+
async function refresh(): Promise<void> {
|
|
19
|
+
loadError = null;
|
|
20
|
+
loading = true;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/api/keys', { credentials: 'include' });
|
|
23
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
24
|
+
rows = await res.json();
|
|
25
|
+
} catch (err) {
|
|
26
|
+
loadError = err instanceof Error ? err.message : String(err);
|
|
27
|
+
} finally {
|
|
28
|
+
loading = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function revoke(id: string): Promise<void> {
|
|
33
|
+
confirmingId = null;
|
|
34
|
+
const res = await fetch(`/api/keys/${encodeURIComponent(id)}`, {
|
|
35
|
+
method: 'DELETE',
|
|
36
|
+
credentials: 'include',
|
|
37
|
+
});
|
|
38
|
+
if (res.ok || res.status === 404) {
|
|
39
|
+
rows = rows.filter((r) => r.id !== id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatDate(iso: string): string {
|
|
44
|
+
return new Date(iso).toLocaleDateString(undefined, {
|
|
45
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$effect(() => { refresh(); });
|
|
50
|
+
|
|
51
|
+
// Refresh whenever any key is revoked (by this or another tab/shard).
|
|
52
|
+
$effect(() => subscribeBus('*', () => { void refresh(); }));
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<div class="keys-peers">
|
|
56
|
+
<div class="keys-peers-header">
|
|
57
|
+
<h2>Keys & Peers</h2>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{#if loading}
|
|
61
|
+
<p class="keys-peers-muted">Loading...</p>
|
|
62
|
+
{:else if loadError}
|
|
63
|
+
<p class="keys-peers-error">Failed to load: {loadError}</p>
|
|
64
|
+
{:else if rows.length === 0}
|
|
65
|
+
<p class="keys-peers-muted">No keys yet. Shards will ask your permission before creating one.</p>
|
|
66
|
+
{:else}
|
|
67
|
+
<ul class="keys-peers-list">
|
|
68
|
+
{#each rows as row (row.id)}
|
|
69
|
+
<li class="keys-peers-item">
|
|
70
|
+
<div class="keys-peers-info">
|
|
71
|
+
<span class="keys-peers-label">{row.label}</span>
|
|
72
|
+
<span class="keys-peers-meta">
|
|
73
|
+
Minted by: {row.mintedByShardId ?? 'system'}{row.peerRole || row.peerId ? ` \u00B7 Peer: ${row.peerRole ?? '—'}${row.peerId ? ` (${row.peerId})` : ''}` : ''}
|
|
74
|
+
</span>
|
|
75
|
+
<span class="keys-peers-meta">
|
|
76
|
+
Scopes: {row.scopes.join(', ')}
|
|
77
|
+
</span>
|
|
78
|
+
<span class="keys-peers-meta">
|
|
79
|
+
Created: {formatDate(row.createdAt)} · Expires: {row.expiresAt ? formatDate(row.expiresAt) : 'never'}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="keys-peers-actions">
|
|
83
|
+
{#if confirmingId === row.id}
|
|
84
|
+
<button type="button" class="keys-peers-btn-danger" onclick={() => revoke(row.id)}>Confirm</button>
|
|
85
|
+
<button type="button" class="keys-peers-btn-secondary" onclick={() => { confirmingId = null; }}>Cancel</button>
|
|
86
|
+
{:else}
|
|
87
|
+
<button type="button" class="keys-peers-btn-danger" onclick={() => { confirmingId = row.id; }}>Revoke</button>
|
|
88
|
+
{/if}
|
|
89
|
+
</div>
|
|
90
|
+
</li>
|
|
91
|
+
{/each}
|
|
92
|
+
</ul>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
.keys-peers { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
|
|
98
|
+
.keys-peers-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
99
|
+
.keys-peers-header h2 { margin: 0; font-size: 18px; }
|
|
100
|
+
.keys-peers-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
101
|
+
.keys-peers-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px 16px; background: var(--shell-bg-elevated, #252540); border: 1px solid var(--shell-border, #3a3a5c); border-radius: var(--shell-radius, 6px); gap: 12px; }
|
|
102
|
+
.keys-peers-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
103
|
+
.keys-peers-label { font-weight: 600; }
|
|
104
|
+
.keys-peers-meta { font-size: 11px; color: var(--shell-fg-subtle); }
|
|
105
|
+
.keys-peers-actions { display: flex; gap: 6px; flex-shrink: 0; align-items: flex-start; padding-top: 2px; }
|
|
106
|
+
.keys-peers-btn-danger { background: transparent; color: var(--shell-error, #d32f2f); border: 1px solid var(--shell-error, #d32f2f); font-size: 12px; padding: 4px 10px; border-radius: var(--shell-radius, 6px); cursor: pointer; }
|
|
107
|
+
.keys-peers-btn-secondary { background: transparent; color: var(--shell-fg-subtle); border: 1px solid var(--shell-border, #3a3a5c); font-size: 12px; padding: 4px 10px; border-radius: var(--shell-radius, 6px); cursor: pointer; }
|
|
108
|
+
.keys-peers-muted { color: var(--shell-fg-muted); font-style: italic; }
|
|
109
|
+
.keys-peers-error { color: var(--shell-error, #d32f2f); font-size: 13px; }
|
|
110
|
+
</style>
|
|
@@ -88,15 +88,6 @@
|
|
|
88
88
|
toolbarRegistry.register({ id: 'focus-lock', order: 20, visible: (ctx) => ctx.mode.id === 'user', component: FocusLockSlot });
|
|
89
89
|
toolbarRegistry.register({ id: 'target-shard', order: 30, visible: (ctx) => ctx.mode.id === 'user', component: TargetShardSlot });
|
|
90
90
|
|
|
91
|
-
let toolbarExpanded = $state((() => {
|
|
92
|
-
try { return localStorage.getItem('sh3.shell.toolbarExpanded') !== '0'; } catch { return true; }
|
|
93
|
-
})());
|
|
94
|
-
|
|
95
|
-
function toggleToolbar() {
|
|
96
|
-
toolbarExpanded = !toolbarExpanded;
|
|
97
|
-
try { localStorage.setItem('sh3.shell.toolbarExpanded', toolbarExpanded ? '1' : '0'); } catch {}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
91
|
/** Walk the layout tree and return the viewId of the active tab in the first
|
|
101
92
|
* TabsNode found (breadth-first). Returns null if the layout contains no
|
|
102
93
|
* tabs node with a populated active tab. */
|
|
@@ -205,8 +196,6 @@
|
|
|
205
196
|
<Toolbar
|
|
206
197
|
registry={toolbarRegistry}
|
|
207
198
|
ctx={{ mode, role }}
|
|
208
|
-
expanded={toolbarExpanded}
|
|
209
|
-
onToggle={toggleToolbar}
|
|
210
199
|
slotProps={{
|
|
211
200
|
mode: { mode, role, registry: modeRegistry, onSelect: setMode },
|
|
212
201
|
'focus-lock': { locked: focusLocked, onToggle: () => (focusLocked = !focusLocked) },
|
|
@@ -4,28 +4,21 @@
|
|
|
4
4
|
interface Props {
|
|
5
5
|
registry: ToolbarSlotRegistry;
|
|
6
6
|
ctx: ShellSlotCtx;
|
|
7
|
-
expanded: boolean;
|
|
8
|
-
onToggle: () => void;
|
|
9
7
|
slotProps?: Record<string, Record<string, unknown>>;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
let { registry, ctx,
|
|
10
|
+
let { registry, ctx, slotProps = {} }: Props = $props();
|
|
13
11
|
|
|
14
12
|
let slots = $derived(registry.list(ctx));
|
|
15
13
|
</script>
|
|
16
14
|
|
|
17
|
-
<div class="toolbar"
|
|
18
|
-
<
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{@const Slot = s.component}
|
|
25
|
-
<Slot {...(slotProps[s.id] ?? {})} />
|
|
26
|
-
{/each}
|
|
27
|
-
</div>
|
|
28
|
-
{/if}
|
|
15
|
+
<div class="toolbar">
|
|
16
|
+
<div class="toolbar-slots">
|
|
17
|
+
{#each slots as s (s.id)}
|
|
18
|
+
{@const Slot = s.component}
|
|
19
|
+
<Slot {...(slotProps[s.id] ?? {})} />
|
|
20
|
+
{/each}
|
|
21
|
+
</div>
|
|
29
22
|
</div>
|
|
30
23
|
|
|
31
24
|
<style>
|
|
@@ -33,30 +26,16 @@
|
|
|
33
26
|
display: flex;
|
|
34
27
|
align-items: center;
|
|
35
28
|
gap: 6px;
|
|
36
|
-
padding:
|
|
37
|
-
background: var(--shell-
|
|
29
|
+
padding: 4px 6px;
|
|
30
|
+
background: var(--shell-bg-elevated, var(--shell-bg, #1a1a1a));
|
|
38
31
|
border-bottom: 1px solid var(--shell-border, #333);
|
|
39
32
|
flex-shrink: 0;
|
|
40
|
-
min-height: 24px;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.toolbar-toggle {
|
|
44
|
-
background: none;
|
|
45
|
-
border: none;
|
|
46
|
-
color: var(--shell-fg-dim, #888);
|
|
47
|
-
cursor: pointer;
|
|
48
|
-
font-size: 0.7em;
|
|
49
|
-
padding: 0 2px;
|
|
50
|
-
line-height: 1;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.toolbar-toggle:hover {
|
|
54
|
-
color: var(--shell-fg, #ddd);
|
|
55
33
|
}
|
|
56
34
|
|
|
57
35
|
.toolbar-slots {
|
|
58
36
|
display: flex;
|
|
59
37
|
align-items: center;
|
|
60
38
|
gap: 8px;
|
|
39
|
+
flex-wrap: wrap;
|
|
61
40
|
}
|
|
62
41
|
</style>
|
|
@@ -2,8 +2,6 @@ import type { ToolbarSlotRegistry, ShellSlotCtx } from './slots';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
registry: ToolbarSlotRegistry;
|
|
4
4
|
ctx: ShellSlotCtx;
|
|
5
|
-
expanded: boolean;
|
|
6
|
-
onToggle: () => void;
|
|
7
5
|
slotProps?: Record<string, Record<string, unknown>>;
|
|
8
6
|
}
|
|
9
7
|
declare const Toolbar: import("svelte").Component<Props, {}, "">;
|
|
@@ -11,92 +11,59 @@
|
|
|
11
11
|
|
|
12
12
|
let { mode, role, registry, onSelect }: Props = $props();
|
|
13
13
|
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
function select(id: string) {
|
|
17
|
-
open = false;
|
|
18
|
-
onSelect(id);
|
|
19
|
-
}
|
|
14
|
+
let modes = $derived(registry.list(role));
|
|
20
15
|
</script>
|
|
21
16
|
|
|
22
17
|
{#if role === 'admin'}
|
|
23
|
-
<div class="mode-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
>
|
|
36
|
-
{m.label}
|
|
37
|
-
</button>
|
|
38
|
-
</li>
|
|
39
|
-
{/each}
|
|
40
|
-
</ul>
|
|
41
|
-
{/if}
|
|
18
|
+
<div class="mode-bar" role="toolbar" aria-label="Shell mode">
|
|
19
|
+
{#each modes as m (m.id)}
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="mode-btn"
|
|
23
|
+
class:active={m.id === mode.id}
|
|
24
|
+
aria-pressed={m.id === mode.id}
|
|
25
|
+
onclick={() => onSelect(m.id)}
|
|
26
|
+
>
|
|
27
|
+
{m.label}
|
|
28
|
+
</button>
|
|
29
|
+
{/each}
|
|
42
30
|
</div>
|
|
43
31
|
{:else}
|
|
44
32
|
<span class="mode-label">{mode.label}</span>
|
|
45
33
|
{/if}
|
|
46
34
|
|
|
47
35
|
<style>
|
|
48
|
-
.mode-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
.mode-bar {
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
gap: 2px;
|
|
39
|
+
padding: 1px;
|
|
40
|
+
border: 1px solid var(--shell-border, #444);
|
|
41
|
+
border-radius: 3px;
|
|
51
42
|
}
|
|
52
43
|
|
|
53
44
|
.mode-btn {
|
|
54
45
|
background: none;
|
|
55
|
-
border:
|
|
56
|
-
color: var(--shell-fg, #
|
|
57
|
-
padding: 2px
|
|
58
|
-
border-radius:
|
|
46
|
+
border: none;
|
|
47
|
+
color: var(--shell-fg-dim, var(--shell-fg-muted, #888));
|
|
48
|
+
padding: 2px 8px;
|
|
49
|
+
border-radius: 2px;
|
|
59
50
|
cursor: pointer;
|
|
60
51
|
font-size: 0.85em;
|
|
52
|
+
line-height: 1.4;
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
.mode-btn:hover {
|
|
64
|
-
background: var(--shell-hover, #
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.mode-menu {
|
|
68
|
-
position: absolute;
|
|
69
|
-
top: 100%;
|
|
70
|
-
left: 0;
|
|
71
|
-
margin: 2px 0 0;
|
|
72
|
-
padding: 0;
|
|
73
|
-
list-style: none;
|
|
74
|
-
background: var(--shell-bg, #111);
|
|
75
|
-
border: 1px solid var(--shell-border, #444);
|
|
76
|
-
border-radius: 3px;
|
|
77
|
-
z-index: 100;
|
|
78
|
-
min-width: 100%;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.mode-option {
|
|
82
|
-
display: block;
|
|
83
|
-
width: 100%;
|
|
84
|
-
background: none;
|
|
85
|
-
border: none;
|
|
56
|
+
background: var(--shell-hover, color-mix(in srgb, var(--shell-fg, #ddd) 10%, transparent));
|
|
86
57
|
color: var(--shell-fg, #ddd);
|
|
87
|
-
padding: 4px 10px;
|
|
88
|
-
text-align: left;
|
|
89
|
-
cursor: pointer;
|
|
90
|
-
font-size: 0.85em;
|
|
91
58
|
}
|
|
92
59
|
|
|
93
|
-
.mode-
|
|
94
|
-
|
|
95
|
-
|
|
60
|
+
.mode-btn.active {
|
|
61
|
+
background: var(--shell-accent, #7c7cf0);
|
|
62
|
+
color: var(--shell-bg, #1a1a2e);
|
|
96
63
|
}
|
|
97
64
|
|
|
98
65
|
.mode-label {
|
|
99
66
|
font-size: 0.85em;
|
|
100
|
-
color: var(--shell-fg-dim, #888);
|
|
67
|
+
color: var(--shell-fg-dim, var(--shell-fg-muted, #888));
|
|
101
68
|
}
|
|
102
69
|
</style>
|
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.0";
|
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.0';
|
package/package.json
CHANGED
|
@@ -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 {};
|