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
|
@@ -10,6 +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 { 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
|
+
}
|
|
13
58
|
/**
|
|
14
59
|
* Context provided by sh3-server when mounting a server shard's routes.
|
|
15
60
|
*/
|
|
@@ -22,14 +67,33 @@ export interface ServerShardContext {
|
|
|
22
67
|
* Path: `<dataDir>/shards/<shard-id>/`
|
|
23
68
|
*/
|
|
24
69
|
dataDir: string;
|
|
70
|
+
/** Permission strings declared in the paired client manifest, empty if no manifest. */
|
|
71
|
+
permissions: string[];
|
|
25
72
|
/**
|
|
26
73
|
* Hono middleware that rejects non-admin callers.
|
|
27
|
-
*
|
|
28
|
-
* should not use this.
|
|
74
|
+
* Backwards-compatible alias for scopeRequired('admin:*'). Prefer the generic form.
|
|
29
75
|
*
|
|
30
76
|
* Usage: `router.post('/publish', ctx.adminOnly, handler)`
|
|
31
77
|
*/
|
|
32
78
|
adminOnly: MiddlewareHandler;
|
|
79
|
+
/** 403 unless caller has the named scope (or admin:*). */
|
|
80
|
+
scopeRequired: (scope: string) => MiddlewareHandler;
|
|
81
|
+
/** 401 unless caller has a non-null tenantId. */
|
|
82
|
+
tenantRequired: MiddlewareHandler;
|
|
83
|
+
/**
|
|
84
|
+
* WebSocket upgrade registration — unchanged from 0.8.1.
|
|
85
|
+
*/
|
|
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;
|
|
91
|
+
/**
|
|
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.
|
|
95
|
+
*/
|
|
96
|
+
setPeerRole(tenant: string, role: 'primary' | 'replica'): void;
|
|
33
97
|
}
|
|
34
98
|
/**
|
|
35
99
|
* The interface a server shard bundle must default-export.
|
|
@@ -45,6 +109,8 @@ export interface ServerShard {
|
|
|
45
109
|
* May be async if the shard needs to initialise resources before serving.
|
|
46
110
|
*/
|
|
47
111
|
routes: (router: HonoLike, context: ServerShardContext) => void | Promise<void>;
|
|
112
|
+
/** Optional shutdown hook. Called once on server SIGTERM/SIGINT. */
|
|
113
|
+
teardown?(): void | Promise<void>;
|
|
48
114
|
}
|
|
49
115
|
/**
|
|
50
116
|
* Hono MiddlewareHandler type — duplicated here to avoid importing hono
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/*
|
|
3
|
-
* Shell home — the view shown when no app is active.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Shell home — the view shown when no app is active.
|
|
4
|
+
*
|
|
5
|
+
* Layout: a title header, a filter bar, then one grid per visible
|
|
6
|
+
* section (User apps always, Admin apps when elevated). Each app is
|
|
7
|
+
* rendered as a square card; the whole card is the launch action.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { listRegisteredApps, launchApp, isAdmin, VERSION } from '../api';
|
|
9
11
|
import ShellTitle from './ShellTitle.svelte';
|
|
10
12
|
|
|
13
|
+
let filter = $state('');
|
|
14
|
+
|
|
11
15
|
const apps = $derived(listRegisteredApps());
|
|
12
|
-
const userApps = $derived(apps.filter(m => !m.admin));
|
|
13
|
-
const adminApps = $derived(apps.filter(m => m.admin));
|
|
14
16
|
const elevated = $derived(isAdmin());
|
|
17
|
+
|
|
18
|
+
function matches(m: { id: string; label: string }, q: string): boolean {
|
|
19
|
+
if (!q) return true;
|
|
20
|
+
const needle = q.toLowerCase();
|
|
21
|
+
return m.label.toLowerCase().includes(needle) || m.id.toLowerCase().includes(needle);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const userApps = $derived(apps.filter((m) => !m.admin && matches(m, filter)));
|
|
25
|
+
const adminApps = $derived(apps.filter((m) => m.admin && matches(m, filter)));
|
|
26
|
+
const totalVisible = $derived(userApps.length + (elevated ? adminApps.length : 0));
|
|
15
27
|
</script>
|
|
16
28
|
|
|
17
29
|
<div class="shell-home">
|
|
@@ -26,54 +38,68 @@
|
|
|
26
38
|
</div>
|
|
27
39
|
</header>
|
|
28
40
|
|
|
41
|
+
<div class="shell-home-filter">
|
|
42
|
+
<input
|
|
43
|
+
type="search"
|
|
44
|
+
placeholder="Filter apps…"
|
|
45
|
+
bind:value={filter}
|
|
46
|
+
aria-label="Filter apps by name"
|
|
47
|
+
class="shell-home-filter-input"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
29
51
|
{#if userApps.length > 0}
|
|
30
52
|
<section class="shell-home-section">
|
|
31
53
|
<h2 class="shell-home-section-title">Apps</h2>
|
|
32
|
-
<
|
|
54
|
+
<div class="shell-home-grid">
|
|
33
55
|
{#each userApps as manifest (manifest.id)}
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
class="shell-home-card"
|
|
59
|
+
onclick={() => launchApp(manifest.id)}
|
|
60
|
+
title="Launch {manifest.label}"
|
|
61
|
+
>
|
|
62
|
+
<div class="shell-home-card-label">{manifest.label}</div>
|
|
63
|
+
<div class="shell-home-card-meta">
|
|
64
|
+
<span class="shell-home-card-id">{manifest.id}</span>
|
|
65
|
+
<span class="shell-home-card-version">v{manifest.version}</span>
|
|
38
66
|
</div>
|
|
39
|
-
|
|
40
|
-
type="button"
|
|
41
|
-
class="shell-home-launch"
|
|
42
|
-
onclick={() => launchApp(manifest.id)}
|
|
43
|
-
>
|
|
44
|
-
Launch
|
|
45
|
-
</button>
|
|
46
|
-
</li>
|
|
67
|
+
</button>
|
|
47
68
|
{/each}
|
|
48
|
-
</
|
|
69
|
+
</div>
|
|
49
70
|
</section>
|
|
50
71
|
{/if}
|
|
51
72
|
|
|
52
73
|
{#if elevated && adminApps.length > 0}
|
|
53
74
|
<section class="shell-home-section">
|
|
54
75
|
<h2 class="shell-home-section-title">Admin</h2>
|
|
55
|
-
<
|
|
76
|
+
<div class="shell-home-grid">
|
|
56
77
|
{#each adminApps as manifest (manifest.id)}
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="shell-home-card"
|
|
81
|
+
onclick={() => launchApp(manifest.id)}
|
|
82
|
+
title="Launch {manifest.label}"
|
|
83
|
+
>
|
|
84
|
+
<div class="shell-home-card-label">{manifest.label}</div>
|
|
85
|
+
<div class="shell-home-card-meta">
|
|
86
|
+
<span class="shell-home-card-id">{manifest.id}</span>
|
|
87
|
+
<span class="shell-home-card-version">v{manifest.version}</span>
|
|
61
88
|
</div>
|
|
62
|
-
|
|
63
|
-
type="button"
|
|
64
|
-
class="shell-home-launch"
|
|
65
|
-
onclick={() => launchApp(manifest.id)}
|
|
66
|
-
>
|
|
67
|
-
Launch
|
|
68
|
-
</button>
|
|
69
|
-
</li>
|
|
89
|
+
</button>
|
|
70
90
|
{/each}
|
|
71
|
-
</
|
|
91
|
+
</div>
|
|
72
92
|
</section>
|
|
73
93
|
{/if}
|
|
74
94
|
|
|
75
|
-
{#if
|
|
76
|
-
<p class="shell-home-empty">
|
|
95
|
+
{#if totalVisible === 0}
|
|
96
|
+
<p class="shell-home-empty">
|
|
97
|
+
{#if apps.length === 0}
|
|
98
|
+
No apps registered.
|
|
99
|
+
{:else}
|
|
100
|
+
No apps match “{filter}”.
|
|
101
|
+
{/if}
|
|
102
|
+
</p>
|
|
77
103
|
{/if}
|
|
78
104
|
</div>
|
|
79
105
|
|
|
@@ -93,7 +119,7 @@
|
|
|
93
119
|
}
|
|
94
120
|
.shell-home-header {
|
|
95
121
|
text-align: center;
|
|
96
|
-
margin-bottom:
|
|
122
|
+
margin-bottom: 24px;
|
|
97
123
|
display: flex;
|
|
98
124
|
flex-direction: column;
|
|
99
125
|
align-items: center;
|
|
@@ -136,14 +162,38 @@
|
|
|
136
162
|
position: relative;
|
|
137
163
|
top: -1px;
|
|
138
164
|
}
|
|
165
|
+
.shell-home-filter {
|
|
166
|
+
width: 100%;
|
|
167
|
+
max-width: 720px;
|
|
168
|
+
margin-bottom: 24px;
|
|
169
|
+
}
|
|
170
|
+
.shell-home-filter-input {
|
|
171
|
+
width: 100%;
|
|
172
|
+
padding: 10px 14px;
|
|
173
|
+
font: inherit;
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
color: var(--shell-fg);
|
|
176
|
+
background: var(--shell-bg-elevated);
|
|
177
|
+
border: 1px solid var(--shell-border);
|
|
178
|
+
border-radius: var(--shell-radius-md);
|
|
179
|
+
outline: none;
|
|
180
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
181
|
+
}
|
|
182
|
+
.shell-home-filter-input::placeholder {
|
|
183
|
+
color: var(--shell-fg-muted);
|
|
184
|
+
}
|
|
185
|
+
.shell-home-filter-input:focus {
|
|
186
|
+
border-color: var(--shell-accent);
|
|
187
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 25%, transparent);
|
|
188
|
+
}
|
|
139
189
|
.shell-home-empty {
|
|
140
190
|
color: var(--shell-fg-muted);
|
|
141
191
|
font-style: italic;
|
|
142
192
|
}
|
|
143
193
|
.shell-home-section {
|
|
144
194
|
width: 100%;
|
|
145
|
-
max-width:
|
|
146
|
-
margin-bottom:
|
|
195
|
+
max-width: 720px;
|
|
196
|
+
margin-bottom: 28px;
|
|
147
197
|
}
|
|
148
198
|
.shell-home-section-title {
|
|
149
199
|
font-size: 13px;
|
|
@@ -153,40 +203,67 @@
|
|
|
153
203
|
color: var(--shell-fg-subtle);
|
|
154
204
|
margin: 0 0 12px;
|
|
155
205
|
}
|
|
156
|
-
.shell-home-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
206
|
+
.shell-home-grid {
|
|
207
|
+
display: grid;
|
|
208
|
+
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
|
209
|
+
gap: 10px;
|
|
210
|
+
}
|
|
211
|
+
.shell-home-card {
|
|
212
|
+
aspect-ratio: 1 / 1;
|
|
160
213
|
display: flex;
|
|
161
214
|
flex-direction: column;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
display: grid;
|
|
166
|
-
grid-template-columns: 1fr auto;
|
|
167
|
-
grid-template-rows: auto auto;
|
|
168
|
-
gap: 4px 16px;
|
|
169
|
-
align-items: center;
|
|
170
|
-
padding: 14px 18px;
|
|
215
|
+
justify-content: space-between;
|
|
216
|
+
text-align: left;
|
|
217
|
+
padding: 10px;
|
|
171
218
|
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
172
219
|
border: 1px solid var(--shell-border);
|
|
173
220
|
border-radius: var(--shell-radius-md);
|
|
221
|
+
color: inherit;
|
|
222
|
+
font: inherit;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
225
|
+
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
226
|
+
}
|
|
227
|
+
.shell-home-card:hover {
|
|
228
|
+
border-color: var(--shell-accent);
|
|
229
|
+
transform: translateY(-1px);
|
|
230
|
+
box-shadow:
|
|
231
|
+
0 6px 14px rgba(0, 0, 0, 0.3),
|
|
232
|
+
0 0 0 1px color-mix(in srgb, var(--shell-accent) 35%, transparent),
|
|
233
|
+
0 4px 12px color-mix(in srgb, var(--shell-accent) 18%, transparent);
|
|
174
234
|
}
|
|
175
|
-
.shell-home-
|
|
176
|
-
|
|
177
|
-
|
|
235
|
+
.shell-home-card:focus-visible {
|
|
236
|
+
outline: none;
|
|
237
|
+
border-color: var(--shell-accent);
|
|
238
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 40%, transparent);
|
|
239
|
+
}
|
|
240
|
+
.shell-home-card:active {
|
|
241
|
+
transform: translateY(0);
|
|
242
|
+
}
|
|
243
|
+
.shell-home-card-label {
|
|
178
244
|
font-weight: 600;
|
|
245
|
+
font-size: 12px;
|
|
246
|
+
line-height: 1.2;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
display: -webkit-box;
|
|
249
|
+
-webkit-box-orient: vertical;
|
|
250
|
+
-webkit-line-clamp: 2;
|
|
251
|
+
line-clamp: 2;
|
|
179
252
|
}
|
|
180
|
-
.shell-home-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
253
|
+
.shell-home-card-meta {
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
gap: 1px;
|
|
257
|
+
font-size: 9px;
|
|
184
258
|
color: var(--shell-fg-subtle);
|
|
259
|
+
min-width: 0;
|
|
185
260
|
}
|
|
186
|
-
.shell-home-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
261
|
+
.shell-home-card-id {
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
text-overflow: ellipsis;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
.shell-home-card-version {
|
|
267
|
+
color: var(--shell-fg-muted);
|
|
191
268
|
}
|
|
192
269
|
</style>
|
|
@@ -23,13 +23,17 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { mount, unmount } from 'svelte';
|
|
25
25
|
import ShellHome from './ShellHome.svelte';
|
|
26
|
+
import KeysAndPeers from '../shell/views/KeysAndPeers.svelte';
|
|
26
27
|
import { VERSION } from '../version';
|
|
27
28
|
export const sh3coreShard = {
|
|
28
29
|
manifest: {
|
|
29
30
|
id: '__sh3core__',
|
|
30
31
|
label: 'SH3 Core',
|
|
31
32
|
version: VERSION,
|
|
32
|
-
views: [
|
|
33
|
+
views: [
|
|
34
|
+
{ id: 'sh3core:home', label: 'Home' },
|
|
35
|
+
{ id: 'shell:keys-and-peers', label: 'Keys & Peers' },
|
|
36
|
+
],
|
|
33
37
|
},
|
|
34
38
|
activate(ctx) {
|
|
35
39
|
const factory = {
|
|
@@ -43,7 +47,14 @@ export const sh3coreShard = {
|
|
|
43
47
|
};
|
|
44
48
|
},
|
|
45
49
|
};
|
|
50
|
+
const keysFactory = {
|
|
51
|
+
mount(container, _context) {
|
|
52
|
+
const instance = mount(KeysAndPeers, { target: container });
|
|
53
|
+
return { unmount() { unmount(instance); } };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
46
56
|
ctx.registerView('sh3core:home', factory);
|
|
57
|
+
ctx.registerView('shell:keys-and-peers', keysFactory);
|
|
47
58
|
},
|
|
48
59
|
autostart() {
|
|
49
60
|
// Intentionally empty. Defining this field is what puts the sh3core
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, deactivateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
+
import { emit } from '../keys/revocation-bus.svelte';
|
|
6
|
+
describe('onKeyRevoked hook wiring', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
__resetShardRegistryForTest();
|
|
9
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
10
|
+
__setTenantId('tenant-a');
|
|
11
|
+
});
|
|
12
|
+
it('fires onKeyRevoked when the bus emits for the shard', async () => {
|
|
13
|
+
const received = [];
|
|
14
|
+
registerShard({
|
|
15
|
+
manifest: { id: 'hook-shard', label: 'h', version: '0.0.0', views: [] },
|
|
16
|
+
activate() { },
|
|
17
|
+
onKeyRevoked(id) { received.push(id); },
|
|
18
|
+
});
|
|
19
|
+
await activateShard('hook-shard');
|
|
20
|
+
emit('hook-shard', 'key-abc');
|
|
21
|
+
// Handler is async; give microtasks a chance to flush.
|
|
22
|
+
await Promise.resolve();
|
|
23
|
+
expect(received).toEqual(['key-abc']);
|
|
24
|
+
});
|
|
25
|
+
it('does not fire for a different shardId', async () => {
|
|
26
|
+
const received = [];
|
|
27
|
+
registerShard({
|
|
28
|
+
manifest: { id: 'shard-x', label: 'x', version: '0.0.0', views: [] },
|
|
29
|
+
activate() { },
|
|
30
|
+
onKeyRevoked(id) { received.push(id); },
|
|
31
|
+
});
|
|
32
|
+
await activateShard('shard-x');
|
|
33
|
+
emit('shard-y', 'key-other');
|
|
34
|
+
await Promise.resolve();
|
|
35
|
+
expect(received).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
it('does not fire after deactivation', async () => {
|
|
38
|
+
const received = [];
|
|
39
|
+
registerShard({
|
|
40
|
+
manifest: { id: 'shard-deact', label: 'd', version: '0.0.0', views: [] },
|
|
41
|
+
activate() { },
|
|
42
|
+
onKeyRevoked(id) { received.push(id); },
|
|
43
|
+
});
|
|
44
|
+
await activateShard('shard-deact');
|
|
45
|
+
deactivateShard('shard-deact');
|
|
46
|
+
emit('shard-deact', 'key-gone');
|
|
47
|
+
await Promise.resolve();
|
|
48
|
+
expect(received).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
it('does not subscribe when onKeyRevoked is absent', async () => {
|
|
51
|
+
// Should not throw — just silently skips subscribing.
|
|
52
|
+
registerShard({
|
|
53
|
+
manifest: { id: 'no-hook', label: 'n', version: '0.0.0', views: [] },
|
|
54
|
+
activate() { },
|
|
55
|
+
});
|
|
56
|
+
await expect(activateShard('no-hook')).resolves.toBeUndefined();
|
|
57
|
+
// Emitting for a shard with no listener is a no-op.
|
|
58
|
+
expect(() => emit('no-hook', 'k')).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -23,12 +23,11 @@ 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 { PERMISSION_DOCUMENTS_SYNC } from '../documents/sync/types';
|
|
27
26
|
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
28
27
|
import { createBrowseCapability } from '../documents/browse';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
28
|
+
import { createShardKeysApi } from '../keys/client';
|
|
29
|
+
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
30
|
+
import { subscribe } from '../keys/revocation-bus.svelte';
|
|
32
31
|
/**
|
|
33
32
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
34
33
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
@@ -140,29 +139,27 @@ export async function activateShard(id) {
|
|
|
140
139
|
browse: ((_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_BROWSE))
|
|
141
140
|
? createBrowseCapability(getTenantId(), getDocumentBackend())
|
|
142
141
|
: undefined,
|
|
143
|
-
|
|
144
|
-
?
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const backend = getDocumentBackend();
|
|
149
|
-
const tenantId = getTenantId();
|
|
150
|
-
const bundlePromise = getSyncBundle(backend, tenantId);
|
|
151
|
-
const handlePromise = bundlePromise.then(({ engine, registry }) => createSyncHandle({ tenantId, connectorId: id, engine, registry }));
|
|
152
|
-
return {
|
|
153
|
-
connectorId: id,
|
|
154
|
-
grantedScopes: async () => (await handlePromise).grantedScopes(),
|
|
155
|
-
getManifest: async (scope) => (await handlePromise).getManifest(scope),
|
|
156
|
-
changesSince: async (scope, cursor) => (await handlePromise).changesSince(scope, cursor),
|
|
157
|
-
ack: async (scope, cursor) => (await handlePromise).ack(scope, cursor),
|
|
158
|
-
apply: async (scope, entry, opts) => (await handlePromise).apply(scope, entry, opts),
|
|
159
|
-
applyBatch: async (scope, manifest, opts) => (await handlePromise).applyBatch(scope, manifest, opts),
|
|
160
|
-
forget: async (scope, path) => (await handlePromise).forget(scope, path),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
142
|
+
keys: ((_c = shard.manifest.permissions) === null || _c === void 0 ? void 0 : _c.includes(PERMISSION_KEYS_MINT))
|
|
143
|
+
? createShardKeysApi({
|
|
144
|
+
shardId: id,
|
|
145
|
+
shardPermissions: (_d = shard.manifest.permissions) !== null && _d !== void 0 ? _d : [],
|
|
146
|
+
})
|
|
163
147
|
: undefined,
|
|
164
148
|
};
|
|
165
149
|
entry.ctx = ctx;
|
|
150
|
+
// Wire onKeyRevoked hook: subscribe to the revocation bus for this shard.
|
|
151
|
+
// Only shards that declare the hook incur the subscription overhead.
|
|
152
|
+
if (shard.onKeyRevoked) {
|
|
153
|
+
const off = subscribe(id, async (keyId) => {
|
|
154
|
+
try {
|
|
155
|
+
await shard.onKeyRevoked(keyId);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.error(`[sh3] onKeyRevoked failed in "${id}":`, err);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
entry.cleanupFns.push(async () => off());
|
|
162
|
+
}
|
|
166
163
|
active.set(id, entry);
|
|
167
164
|
activeShards.set(id, shard);
|
|
168
165
|
await shard.activate(ctx);
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ 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';
|
|
7
|
+
import type { ShardContextKeys } from '../keys/types';
|
|
8
|
+
export { PERMISSION_KEYS_MINT, type ShardContextKeys, type ApiKeyPublic, type MintOpts, ScopeEscalationError, ConsentDeniedError } from '../keys/types';
|
|
9
9
|
/**
|
|
10
10
|
* The object returned by `ViewFactory.mount`. The framework calls
|
|
11
11
|
* `unmount()` when the slot goes away, and `onResize(w, h)` whenever the
|
|
@@ -212,12 +212,6 @@ export interface ShardContext {
|
|
|
212
212
|
* `if (ctx.zones)` before use.
|
|
213
213
|
*/
|
|
214
214
|
zones?: ZoneManager;
|
|
215
|
-
/**
|
|
216
|
-
* Cross-shard document sync API. Only present when the shard's
|
|
217
|
-
* manifest declares the `'documents:sync'` permission. Check with
|
|
218
|
-
* `if (ctx.sync)` before use.
|
|
219
|
-
*/
|
|
220
|
-
sync?: () => SyncHandle;
|
|
221
215
|
/**
|
|
222
216
|
* Tenant-wide document browse API. Read-only enumeration and change
|
|
223
217
|
* subscription across every shard's documents for the active tenant.
|
|
@@ -227,12 +221,10 @@ export interface ShardContext {
|
|
|
227
221
|
*/
|
|
228
222
|
browse?: BrowseCapability;
|
|
229
223
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* `'documents:browse'`. Granting still happens exclusively via
|
|
233
|
-
* `<SyncGrantPicker />`.
|
|
224
|
+
* Mint/list/revoke keys minted by this shard. Only available when the
|
|
225
|
+
* manifest declares the `keys:mint` permission.
|
|
234
226
|
*/
|
|
235
|
-
|
|
227
|
+
keys?: ShardContextKeys;
|
|
236
228
|
}
|
|
237
229
|
/**
|
|
238
230
|
* A shard module. Shards are the fundamental unit of contribution in SH3.
|
|
@@ -274,6 +266,8 @@ export interface Shard {
|
|
|
274
266
|
* `ShardContext` that `activate` received.
|
|
275
267
|
*/
|
|
276
268
|
resume?(ctx: ShardContext): void | Promise<void>;
|
|
269
|
+
/** Fires when a key minted by this shard is revoked from any source. */
|
|
270
|
+
onKeyRevoked?(id: string): void | Promise<void>;
|
|
277
271
|
}
|
|
278
272
|
/**
|
|
279
273
|
* Source-level shape of a shard as written by external package authors.
|
package/dist/shards/types.js
CHANGED