sh3-core 0.9.0 → 0.10.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 +2 -1
- package/dist/api.js +1 -1
- 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.d.ts +1 -0
- package/dist/app/store/storeShard.svelte.test.js +34 -0
- package/dist/contributions/index.d.ts +2 -0
- package/dist/contributions/index.js +8 -0
- package/dist/contributions/registry.d.ts +21 -0
- package/dist/contributions/registry.js +89 -0
- package/dist/contributions/registry.test.d.ts +1 -0
- package/dist/contributions/registry.test.js +109 -0
- package/dist/contributions/types.d.ts +24 -0
- package/dist/contributions/types.js +10 -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/types.d.ts +29 -0
- package/dist/documents/types.js +29 -0
- 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.d.ts +1 -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/shards/activate-browse.test.js +87 -3
- package/dist/shards/activate-contributions.test.d.ts +1 -0
- package/dist/shards/activate-contributions.test.js +110 -0
- package/dist/shards/activate.svelte.js +28 -2
- package/dist/shards/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -17,8 +17,9 @@ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
|
17
17
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
18
18
|
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
|
|
19
19
|
export type { DocumentHandle, DocumentHandleOptions, DocumentFormat, DocumentMeta, DocumentChange, AutosaveController, } from './documents/types';
|
|
20
|
-
export { PERMISSION_DOCUMENTS_BROWSE } from './documents/types';
|
|
20
|
+
export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
|
|
21
21
|
export type { BrowseCapability } from './documents/browse';
|
|
22
|
+
export type { ContributionsApi } from './contributions/types';
|
|
22
23
|
export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranch, } from './documents/sync-types';
|
|
23
24
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
|
|
24
25
|
export { registeredShards, activeShards } from './shards/activate.svelte';
|
package/dist/api.js
CHANGED
|
@@ -29,7 +29,7 @@ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
|
29
29
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
30
30
|
// Layout inspection / mutation for advanced shards (diagnostic, etc.).
|
|
31
31
|
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
|
|
32
|
-
export { PERMISSION_DOCUMENTS_BROWSE } from './documents/types';
|
|
32
|
+
export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
|
|
33
33
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
|
|
34
34
|
// Shard introspection — read-only reactive maps exposing which shards are
|
|
35
35
|
// known to the host and which are currently active. Intended for diagnostic
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { storeContext } from './storeShard.svelte';
|
|
11
11
|
import { uninstallPackage } from '../../registry/installer';
|
|
12
12
|
import { serverUninstallPackage } from '../../env/client';
|
|
13
|
+
import PermissionConfirmModal from './PermissionConfirmModal.svelte';
|
|
14
|
+
import type { InstalledPackage } from '../../registry/types';
|
|
13
15
|
|
|
14
16
|
const ctx = storeContext;
|
|
15
17
|
|
|
@@ -17,6 +19,14 @@
|
|
|
17
19
|
let updatingIds = $state<Set<string>>(new Set());
|
|
18
20
|
let updateError = $state<string | null>(null);
|
|
19
21
|
|
|
22
|
+
let updateModal = $state<null | {
|
|
23
|
+
pkg: InstalledPackage;
|
|
24
|
+
toVersion: string;
|
|
25
|
+
added: string[];
|
|
26
|
+
removed: string[];
|
|
27
|
+
resolve: (ok: boolean) => void;
|
|
28
|
+
}>(null);
|
|
29
|
+
|
|
20
30
|
async function handleUninstall(id: string) {
|
|
21
31
|
if (uninstallingIds.has(id)) return;
|
|
22
32
|
|
|
@@ -41,7 +51,25 @@
|
|
|
41
51
|
updateError = null;
|
|
42
52
|
|
|
43
53
|
try {
|
|
44
|
-
await ctx.updatePackage(id)
|
|
54
|
+
await ctx.updatePackage(id, (added, removed) => {
|
|
55
|
+
return new Promise<boolean>((resolve) => {
|
|
56
|
+
const pkg = ctx.state.ephemeral.installed.find(
|
|
57
|
+
(p: InstalledPackage) => p.id === id,
|
|
58
|
+
);
|
|
59
|
+
const target = ctx.state.ephemeral.updatable[id];
|
|
60
|
+
if (!pkg || !target) {
|
|
61
|
+
resolve(true); // Falls through to the existing behavior.
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
updateModal = {
|
|
65
|
+
pkg,
|
|
66
|
+
toVersion: target.latest.version,
|
|
67
|
+
added,
|
|
68
|
+
removed,
|
|
69
|
+
resolve,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
});
|
|
45
73
|
} catch (err) {
|
|
46
74
|
updateError = err instanceof Error ? err.message : String(err);
|
|
47
75
|
} finally {
|
|
@@ -51,6 +79,20 @@
|
|
|
51
79
|
}
|
|
52
80
|
}
|
|
53
81
|
|
|
82
|
+
function confirmUpdate() {
|
|
83
|
+
const m = updateModal;
|
|
84
|
+
if (!m) return;
|
|
85
|
+
updateModal = null;
|
|
86
|
+
m.resolve(true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cancelUpdate() {
|
|
90
|
+
const m = updateModal;
|
|
91
|
+
if (!m) return;
|
|
92
|
+
updateModal = null;
|
|
93
|
+
m.resolve(false);
|
|
94
|
+
}
|
|
95
|
+
|
|
54
96
|
function handleRefresh() {
|
|
55
97
|
ctx.refreshInstalled();
|
|
56
98
|
}
|
|
@@ -121,6 +163,18 @@
|
|
|
121
163
|
{/each}
|
|
122
164
|
</ul>
|
|
123
165
|
{/if}
|
|
166
|
+
|
|
167
|
+
{#if updateModal}
|
|
168
|
+
<PermissionConfirmModal
|
|
169
|
+
mode="update"
|
|
170
|
+
pkg={{ label: updateModal.pkg.id, version: updateModal.toVersion }}
|
|
171
|
+
fromVersion={updateModal.pkg.version}
|
|
172
|
+
added={updateModal.added}
|
|
173
|
+
removed={updateModal.removed}
|
|
174
|
+
onConfirm={confirmUpdate}
|
|
175
|
+
onCancel={cancelUpdate}
|
|
176
|
+
/>
|
|
177
|
+
{/if}
|
|
124
178
|
</div>
|
|
125
179
|
|
|
126
180
|
<style>
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* PermissionConfirmModal — confirmation modal for install and update flows.
|
|
4
|
+
*
|
|
5
|
+
* Install mode: lists the full declared permission set for the incoming
|
|
6
|
+
* package.
|
|
7
|
+
* Update mode: lists the permissions newly added or removed compared to
|
|
8
|
+
* the currently installed version.
|
|
9
|
+
*
|
|
10
|
+
* Informational only — the buttons are Cancel / Confirm. Accepting grants
|
|
11
|
+
* all declared permissions; denying aborts the operation with no state
|
|
12
|
+
* change.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describePermission } from '../../registry/permission-descriptions';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
mode: 'install' | 'update';
|
|
19
|
+
pkg: { label: string; version: string; author?: string };
|
|
20
|
+
fromVersion?: string;
|
|
21
|
+
permissions?: string[];
|
|
22
|
+
added?: string[];
|
|
23
|
+
removed?: string[];
|
|
24
|
+
onConfirm: () => void;
|
|
25
|
+
onCancel: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
mode,
|
|
30
|
+
pkg,
|
|
31
|
+
fromVersion,
|
|
32
|
+
permissions = [],
|
|
33
|
+
added = [],
|
|
34
|
+
removed = [],
|
|
35
|
+
onConfirm,
|
|
36
|
+
onCancel,
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const confirmLabel = $derived(mode === 'install' ? 'Install' : 'Update');
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
43
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
44
|
+
<div
|
|
45
|
+
class="perm-modal-backdrop"
|
|
46
|
+
onclick={onCancel}
|
|
47
|
+
onkeydown={(e) => { if (e.key === 'Escape') onCancel(); }}
|
|
48
|
+
role="dialog"
|
|
49
|
+
aria-modal="true"
|
|
50
|
+
aria-label="{confirmLabel} {pkg.label}"
|
|
51
|
+
tabindex="-1"
|
|
52
|
+
>
|
|
53
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
54
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
55
|
+
<div class="perm-modal-panel" onclick={(e) => e.stopPropagation()} role="document">
|
|
56
|
+
<header class="perm-modal-header">
|
|
57
|
+
<h3>{confirmLabel} {pkg.label}</h3>
|
|
58
|
+
<div class="perm-modal-subtitle">
|
|
59
|
+
{#if mode === 'update' && fromVersion}
|
|
60
|
+
{fromVersion} → {pkg.version}
|
|
61
|
+
{:else}
|
|
62
|
+
v{pkg.version}
|
|
63
|
+
{/if}
|
|
64
|
+
{#if pkg.author}
|
|
65
|
+
<span class="perm-modal-author">by {pkg.author}</span>
|
|
66
|
+
{/if}
|
|
67
|
+
</div>
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<div class="perm-modal-body">
|
|
71
|
+
{#if mode === 'install'}
|
|
72
|
+
{#if permissions.length === 0}
|
|
73
|
+
<p class="perm-modal-empty">
|
|
74
|
+
This package requires no special permissions.
|
|
75
|
+
</p>
|
|
76
|
+
{:else}
|
|
77
|
+
<p class="perm-modal-intro">This package requests the following permissions:</p>
|
|
78
|
+
<ul class="perm-modal-list">
|
|
79
|
+
{#each permissions as id (id)}
|
|
80
|
+
{@const d = describePermission(id)}
|
|
81
|
+
<li class="perm-modal-item">
|
|
82
|
+
<div class="perm-modal-item-title">{d.title}</div>
|
|
83
|
+
<div class="perm-modal-item-desc">{d.description}</div>
|
|
84
|
+
</li>
|
|
85
|
+
{/each}
|
|
86
|
+
</ul>
|
|
87
|
+
{/if}
|
|
88
|
+
{:else}
|
|
89
|
+
{#if added.length > 0}
|
|
90
|
+
<p class="perm-modal-intro">New permissions in this update:</p>
|
|
91
|
+
<ul class="perm-modal-list perm-modal-added">
|
|
92
|
+
{#each added as id (id)}
|
|
93
|
+
{@const d = describePermission(id)}
|
|
94
|
+
<li class="perm-modal-item">
|
|
95
|
+
<div class="perm-modal-item-title">{d.title}</div>
|
|
96
|
+
<div class="perm-modal-item-desc">{d.description}</div>
|
|
97
|
+
</li>
|
|
98
|
+
{/each}
|
|
99
|
+
</ul>
|
|
100
|
+
{/if}
|
|
101
|
+
{#if removed.length > 0}
|
|
102
|
+
<p class="perm-modal-intro">Permissions no longer requested:</p>
|
|
103
|
+
<ul class="perm-modal-list perm-modal-removed">
|
|
104
|
+
{#each removed as id (id)}
|
|
105
|
+
{@const d = describePermission(id)}
|
|
106
|
+
<li class="perm-modal-item">
|
|
107
|
+
<div class="perm-modal-item-title">{d.title}</div>
|
|
108
|
+
<div class="perm-modal-item-desc">{d.description}</div>
|
|
109
|
+
</li>
|
|
110
|
+
{/each}
|
|
111
|
+
</ul>
|
|
112
|
+
{/if}
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<footer class="perm-modal-footer">
|
|
117
|
+
<button class="perm-modal-cancel" onclick={onCancel}>Cancel</button>
|
|
118
|
+
<button class="perm-modal-confirm" onclick={onConfirm}>{confirmLabel}</button>
|
|
119
|
+
</footer>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<style>
|
|
124
|
+
.perm-modal-backdrop {
|
|
125
|
+
position: fixed;
|
|
126
|
+
inset: 0;
|
|
127
|
+
background: rgba(0, 0, 0, 0.5);
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
z-index: 1000;
|
|
132
|
+
}
|
|
133
|
+
.perm-modal-panel {
|
|
134
|
+
background: var(--shell-bg, #1e1e1e);
|
|
135
|
+
color: var(--shell-fg, #e0e0e0);
|
|
136
|
+
border: 1px solid var(--shell-border, #444);
|
|
137
|
+
border-radius: var(--shell-radius-md);
|
|
138
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
139
|
+
min-width: 360px;
|
|
140
|
+
max-width: 520px;
|
|
141
|
+
max-height: 80vh;
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
font-family: var(--shell-font-ui);
|
|
145
|
+
}
|
|
146
|
+
.perm-modal-header {
|
|
147
|
+
padding: 16px 20px 12px;
|
|
148
|
+
border-bottom: 1px solid var(--shell-border, #444);
|
|
149
|
+
}
|
|
150
|
+
.perm-modal-header h3 {
|
|
151
|
+
margin: 0 0 4px 0;
|
|
152
|
+
font-size: 1.0625rem;
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
}
|
|
155
|
+
.perm-modal-subtitle {
|
|
156
|
+
font-size: 0.8125rem;
|
|
157
|
+
color: var(--shell-fg-muted, #888);
|
|
158
|
+
}
|
|
159
|
+
.perm-modal-author {
|
|
160
|
+
margin-left: 8px;
|
|
161
|
+
}
|
|
162
|
+
.perm-modal-body {
|
|
163
|
+
padding: 16px 20px;
|
|
164
|
+
overflow-y: auto;
|
|
165
|
+
flex: 1;
|
|
166
|
+
}
|
|
167
|
+
.perm-modal-intro {
|
|
168
|
+
margin: 0 0 8px 0;
|
|
169
|
+
font-size: 0.875rem;
|
|
170
|
+
}
|
|
171
|
+
.perm-modal-empty {
|
|
172
|
+
margin: 0;
|
|
173
|
+
font-size: 0.875rem;
|
|
174
|
+
color: var(--shell-fg-muted, #888);
|
|
175
|
+
font-style: italic;
|
|
176
|
+
}
|
|
177
|
+
.perm-modal-list {
|
|
178
|
+
list-style: none;
|
|
179
|
+
margin: 0 0 12px 0;
|
|
180
|
+
padding: 0;
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
gap: 8px;
|
|
184
|
+
}
|
|
185
|
+
.perm-modal-item {
|
|
186
|
+
padding: 8px 10px;
|
|
187
|
+
background: var(--shell-input-bg, #2a2a2a);
|
|
188
|
+
border: 1px solid var(--shell-border, #444);
|
|
189
|
+
border-radius: var(--shell-radius-sm);
|
|
190
|
+
}
|
|
191
|
+
.perm-modal-item-title {
|
|
192
|
+
font-size: 0.875rem;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
}
|
|
195
|
+
.perm-modal-item-desc {
|
|
196
|
+
font-size: 0.75rem;
|
|
197
|
+
color: var(--shell-fg-muted, #888);
|
|
198
|
+
margin-top: 2px;
|
|
199
|
+
}
|
|
200
|
+
.perm-modal-added .perm-modal-item {
|
|
201
|
+
border-color: color-mix(in srgb, var(--shell-warning, #ff9800) 60%, var(--shell-border, #444));
|
|
202
|
+
}
|
|
203
|
+
.perm-modal-removed .perm-modal-item {
|
|
204
|
+
opacity: 0.75;
|
|
205
|
+
}
|
|
206
|
+
.perm-modal-footer {
|
|
207
|
+
padding: 12px 20px;
|
|
208
|
+
border-top: 1px solid var(--shell-border, #444);
|
|
209
|
+
display: flex;
|
|
210
|
+
justify-content: flex-end;
|
|
211
|
+
gap: 8px;
|
|
212
|
+
}
|
|
213
|
+
.perm-modal-cancel {
|
|
214
|
+
padding: 6px 14px;
|
|
215
|
+
background: transparent;
|
|
216
|
+
color: var(--shell-fg, #e0e0e0);
|
|
217
|
+
border: 1px solid var(--shell-border, #444);
|
|
218
|
+
border-radius: var(--shell-radius);
|
|
219
|
+
font-size: 0.8125rem;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
}
|
|
222
|
+
.perm-modal-confirm {
|
|
223
|
+
padding: 6px 14px;
|
|
224
|
+
background: var(--shell-accent, #007acc);
|
|
225
|
+
color: #fff;
|
|
226
|
+
border: 1px solid var(--shell-accent, #007acc);
|
|
227
|
+
border-radius: var(--shell-radius);
|
|
228
|
+
font-size: 0.8125rem;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
}
|
|
232
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
mode: 'install' | 'update';
|
|
3
|
+
pkg: {
|
|
4
|
+
label: string;
|
|
5
|
+
version: string;
|
|
6
|
+
author?: string;
|
|
7
|
+
};
|
|
8
|
+
fromVersion?: string;
|
|
9
|
+
permissions?: string[];
|
|
10
|
+
added?: string[];
|
|
11
|
+
removed?: string[];
|
|
12
|
+
onConfirm: () => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
declare const PermissionConfirmModal: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type PermissionConfirmModal = ReturnType<typeof PermissionConfirmModal>;
|
|
17
|
+
export default PermissionConfirmModal;
|
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
import { storeContext } from './storeShard.svelte';
|
|
10
10
|
import { fetchBundle, fetchServerBundle, buildPackageMeta } from '../../registry/client';
|
|
11
11
|
import { installPackage } from '../../registry/installer';
|
|
12
|
+
import { loadBundleModule, type LoadedBundle } from '../../registry/loader';
|
|
13
|
+
import { extractBundlePermissions } from '../../registry/permission-descriptions';
|
|
12
14
|
import { serverInstallPackage } from '../../env/client';
|
|
13
15
|
import { contract } from '../../contract';
|
|
14
16
|
import type { ResolvedPackage } from '../../registry/client';
|
|
15
17
|
import type { InstalledPackage } from '../../registry/types';
|
|
16
18
|
import { FRAMEWORK_SHARD_IDS } from '../../api';
|
|
19
|
+
import PermissionConfirmModal from './PermissionConfirmModal.svelte';
|
|
17
20
|
|
|
18
21
|
let search = $state('');
|
|
19
22
|
let typeFilter = $state<'all' | 'shard' | 'app'>('all');
|
|
@@ -22,6 +25,23 @@
|
|
|
22
25
|
let installError = $state<string | null>(null);
|
|
23
26
|
let newRegistryUrl = $state('');
|
|
24
27
|
|
|
28
|
+
let installModal = $state<null | {
|
|
29
|
+
pkg: ResolvedPackage;
|
|
30
|
+
permissions: string[];
|
|
31
|
+
loaded: LoadedBundle;
|
|
32
|
+
bundle: ArrayBuffer;
|
|
33
|
+
meta: ReturnType<typeof buildPackageMeta>;
|
|
34
|
+
serverBundle: ArrayBuffer | undefined;
|
|
35
|
+
}>(null);
|
|
36
|
+
|
|
37
|
+
let updateModal = $state<null | {
|
|
38
|
+
pkg: ResolvedPackage;
|
|
39
|
+
fromVersion: string;
|
|
40
|
+
added: string[];
|
|
41
|
+
removed: string[];
|
|
42
|
+
resolve: (ok: boolean) => void;
|
|
43
|
+
}>(null);
|
|
44
|
+
|
|
25
45
|
const ctx = storeContext;
|
|
26
46
|
|
|
27
47
|
async function handleAddRegistry() {
|
|
@@ -93,7 +113,25 @@
|
|
|
93
113
|
installError = null;
|
|
94
114
|
|
|
95
115
|
try {
|
|
96
|
-
await ctx.updatePackage(id)
|
|
116
|
+
await ctx.updatePackage(id, (added, removed) => {
|
|
117
|
+
return new Promise<boolean>((resolve) => {
|
|
118
|
+
const pkg = ctx.state.ephemeral.updatable[id];
|
|
119
|
+
const installed = ctx.state.ephemeral.installed.find(
|
|
120
|
+
(p: InstalledPackage) => p.id === id,
|
|
121
|
+
);
|
|
122
|
+
if (!pkg || !installed) {
|
|
123
|
+
resolve(true);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
updateModal = {
|
|
127
|
+
pkg,
|
|
128
|
+
fromVersion: installed.version,
|
|
129
|
+
added,
|
|
130
|
+
removed,
|
|
131
|
+
resolve,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
});
|
|
97
135
|
} catch (err) {
|
|
98
136
|
installError = err instanceof Error ? err.message : String(err);
|
|
99
137
|
} finally {
|
|
@@ -103,6 +141,20 @@
|
|
|
103
141
|
}
|
|
104
142
|
}
|
|
105
143
|
|
|
144
|
+
function confirmUpdate() {
|
|
145
|
+
const m = updateModal;
|
|
146
|
+
if (!m) return;
|
|
147
|
+
updateModal = null;
|
|
148
|
+
m.resolve(true);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cancelUpdate() {
|
|
152
|
+
const m = updateModal;
|
|
153
|
+
if (!m) return;
|
|
154
|
+
updateModal = null;
|
|
155
|
+
m.resolve(false);
|
|
156
|
+
}
|
|
157
|
+
|
|
106
158
|
async function handleInstall(pkg: ResolvedPackage) {
|
|
107
159
|
const id = pkg.entry.id;
|
|
108
160
|
if (installingIds.has(id)) return;
|
|
@@ -115,13 +167,37 @@
|
|
|
115
167
|
const bundle = await fetchBundle(pkg.latest, pkg.sourceRegistry);
|
|
116
168
|
const meta = buildPackageMeta(pkg, pkg.latest);
|
|
117
169
|
|
|
118
|
-
// 2.
|
|
170
|
+
// 2. Load the module once, extract permissions for the confirmation
|
|
171
|
+
// modal, and reuse the loaded reference on install.
|
|
172
|
+
const loaded = await loadBundleModule(bundle);
|
|
173
|
+
const permissions = extractBundlePermissions(loaded);
|
|
174
|
+
|
|
175
|
+
// 3. Fetch the server bundle upfront so the modal is the last blocker.
|
|
119
176
|
let serverBundle: ArrayBuffer | undefined;
|
|
120
177
|
if (pkg.latest.serverBundleUrl) {
|
|
121
178
|
serverBundle = await fetchServerBundle(pkg.latest, pkg.sourceRegistry);
|
|
122
179
|
}
|
|
123
180
|
|
|
124
|
-
//
|
|
181
|
+
// 4. Show the confirmation modal. The actual install happens in
|
|
182
|
+
// confirmInstall() once the user clicks Install.
|
|
183
|
+
installModal = { pkg, permissions, loaded, bundle, meta, serverBundle };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
installError = err instanceof Error ? err.message : String(err);
|
|
186
|
+
const next = new Set(installingIds);
|
|
187
|
+
next.delete(id);
|
|
188
|
+
installingIds = next;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function confirmInstall() {
|
|
193
|
+
const ctxModal = installModal;
|
|
194
|
+
if (!ctxModal) return;
|
|
195
|
+
installModal = null;
|
|
196
|
+
|
|
197
|
+
const { pkg, loaded, bundle, meta, serverBundle } = ctxModal;
|
|
198
|
+
const id = pkg.entry.id;
|
|
199
|
+
|
|
200
|
+
try {
|
|
125
201
|
const manifest = {
|
|
126
202
|
id: meta.id,
|
|
127
203
|
type: meta.type,
|
|
@@ -138,8 +214,7 @@
|
|
|
138
214
|
return;
|
|
139
215
|
}
|
|
140
216
|
|
|
141
|
-
|
|
142
|
-
const result = await installPackage(bundle, meta);
|
|
217
|
+
const result = await installPackage(bundle, meta, { loaded });
|
|
143
218
|
if (!result.success) {
|
|
144
219
|
console.warn(`[sh3-store] Server install ok but local hot-load failed: ${result.error}`);
|
|
145
220
|
}
|
|
@@ -154,6 +229,15 @@
|
|
|
154
229
|
}
|
|
155
230
|
}
|
|
156
231
|
|
|
232
|
+
function cancelInstall() {
|
|
233
|
+
if (!installModal) return;
|
|
234
|
+
const id = installModal.pkg.entry.id;
|
|
235
|
+
installModal = null;
|
|
236
|
+
const next = new Set(installingIds);
|
|
237
|
+
next.delete(id);
|
|
238
|
+
installingIds = next;
|
|
239
|
+
}
|
|
240
|
+
|
|
157
241
|
function handleRefresh() {
|
|
158
242
|
ctx.refreshCatalog();
|
|
159
243
|
ctx.refreshInstalled();
|
|
@@ -296,6 +380,36 @@
|
|
|
296
380
|
{/if}
|
|
297
381
|
</div>
|
|
298
382
|
|
|
383
|
+
{#if installModal}
|
|
384
|
+
<PermissionConfirmModal
|
|
385
|
+
mode="install"
|
|
386
|
+
pkg={{
|
|
387
|
+
label: installModal.pkg.entry.label,
|
|
388
|
+
version: installModal.pkg.latest.version,
|
|
389
|
+
author: installModal.pkg.entry.author.name,
|
|
390
|
+
}}
|
|
391
|
+
permissions={installModal.permissions}
|
|
392
|
+
onConfirm={confirmInstall}
|
|
393
|
+
onCancel={cancelInstall}
|
|
394
|
+
/>
|
|
395
|
+
{/if}
|
|
396
|
+
|
|
397
|
+
{#if updateModal}
|
|
398
|
+
<PermissionConfirmModal
|
|
399
|
+
mode="update"
|
|
400
|
+
pkg={{
|
|
401
|
+
label: updateModal.pkg.entry.label,
|
|
402
|
+
version: updateModal.pkg.latest.version,
|
|
403
|
+
author: updateModal.pkg.entry.author.name,
|
|
404
|
+
}}
|
|
405
|
+
fromVersion={updateModal.fromVersion}
|
|
406
|
+
added={updateModal.added}
|
|
407
|
+
removed={updateModal.removed}
|
|
408
|
+
onConfirm={confirmUpdate}
|
|
409
|
+
onCancel={cancelUpdate}
|
|
410
|
+
/>
|
|
411
|
+
{/if}
|
|
412
|
+
|
|
299
413
|
<style>
|
|
300
414
|
.store-view {
|
|
301
415
|
font-family: var(--shell-font-ui);
|
|
@@ -3,6 +3,15 @@ import type { StateZones } from '../../state/zones.svelte';
|
|
|
3
3
|
import type { ResolvedPackage } from '../../registry/client';
|
|
4
4
|
import type { InstalledPackage } from '../../registry/types';
|
|
5
5
|
import type { EnvState } from '../../env/types';
|
|
6
|
+
/**
|
|
7
|
+
* Compute added and removed permissions between two manifest snapshots.
|
|
8
|
+
* Order within each array follows the input order of `newPerms` (for added)
|
|
9
|
+
* and `oldPerms` (for removed). No duplicates — both inputs assumed unique.
|
|
10
|
+
*/
|
|
11
|
+
export declare function diffPermissions(oldPerms: string[], newPerms: string[]): {
|
|
12
|
+
added: string[];
|
|
13
|
+
removed: string[];
|
|
14
|
+
};
|
|
6
15
|
/** Env state shape — server-authoritative config. */
|
|
7
16
|
interface StoreEnvSchema {
|
|
8
17
|
[key: string]: unknown;
|
|
@@ -25,7 +34,7 @@ export interface StoreContext {
|
|
|
25
34
|
isAdmin: boolean;
|
|
26
35
|
refreshCatalog(): Promise<void>;
|
|
27
36
|
refreshInstalled(): Promise<void>;
|
|
28
|
-
updatePackage(id: string): Promise<void>;
|
|
37
|
+
updatePackage(id: string, confirmPermissionChange?: (added: string[], removed: string[]) => Promise<boolean>): Promise<void>;
|
|
29
38
|
addRegistry(url: string): Promise<void>;
|
|
30
39
|
removeRegistry(url: string): Promise<void>;
|
|
31
40
|
}
|
|
@@ -16,7 +16,9 @@ import StoreView from './StoreView.svelte';
|
|
|
16
16
|
import InstalledView from './InstalledView.svelte';
|
|
17
17
|
import { fetchRegistries, fetchBundle, fetchServerBundle, buildPackageMeta } from '../../registry/client';
|
|
18
18
|
import { installPackage, listInstalledPackages } from '../../registry/installer';
|
|
19
|
-
import { loadBundle, savePackage } from '../../registry/storage';
|
|
19
|
+
import { loadBundle, loadMeta, savePackage } from '../../registry/storage';
|
|
20
|
+
import { loadBundleModule } from '../../registry/loader';
|
|
21
|
+
import { extractBundlePermissions } from '../../registry/permission-descriptions';
|
|
20
22
|
import { serverInstallPackage, fetchServerPackages } from '../../env/client';
|
|
21
23
|
import { VERSION } from '../../version';
|
|
22
24
|
import { installVerb, uninstallVerb, appinfoVerb } from './verbs';
|
|
@@ -41,6 +43,19 @@ function isNewerVersion(available, installed) {
|
|
|
41
43
|
}
|
|
42
44
|
return false;
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Compute added and removed permissions between two manifest snapshots.
|
|
48
|
+
* Order within each array follows the input order of `newPerms` (for added)
|
|
49
|
+
* and `oldPerms` (for removed). No duplicates — both inputs assumed unique.
|
|
50
|
+
*/
|
|
51
|
+
export function diffPermissions(oldPerms, newPerms) {
|
|
52
|
+
const oldSet = new Set(oldPerms);
|
|
53
|
+
const newSet = new Set(newPerms);
|
|
54
|
+
return {
|
|
55
|
+
added: newPerms.filter((p) => !oldSet.has(p)),
|
|
56
|
+
removed: oldPerms.filter((p) => !newSet.has(p)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
44
59
|
/**
|
|
45
60
|
* Module-level context set during activate(). Imported by the Svelte
|
|
46
61
|
* view components so they can read/write store state and trigger refreshes.
|
|
@@ -105,6 +120,7 @@ export const storeShard = {
|
|
|
105
120
|
sourceRegistry: (_a = p.sourceRegistry) !== null && _a !== void 0 ? _a : '',
|
|
106
121
|
contractVersion: (_b = p.contractVersion) !== null && _b !== void 0 ? _b : '',
|
|
107
122
|
installedAt: (_c = p.installedAt) !== null && _c !== void 0 ? _c : '',
|
|
123
|
+
permissions: [],
|
|
108
124
|
});
|
|
109
125
|
});
|
|
110
126
|
recomputeUpdatable();
|
|
@@ -133,7 +149,7 @@ export const storeShard = {
|
|
|
133
149
|
const registries = env.registries.filter((r) => r !== url);
|
|
134
150
|
await ctx.envUpdate({ registries });
|
|
135
151
|
}
|
|
136
|
-
async function updatePackage(id) {
|
|
152
|
+
async function updatePackage(id, confirmPermissionChange) {
|
|
137
153
|
var _a, _b, _c, _d;
|
|
138
154
|
const catalogEntry = state.ephemeral.updatable[id];
|
|
139
155
|
if (!catalogEntry)
|
|
@@ -148,10 +164,37 @@ export const storeShard = {
|
|
|
148
164
|
serverBundle = await fetchServerBundle(catalogEntry.latest, catalogEntry.sourceRegistry);
|
|
149
165
|
}
|
|
150
166
|
const meta = buildPackageMeta(catalogEntry, catalogEntry.latest);
|
|
151
|
-
// 2.
|
|
167
|
+
// 2. Load the module once for permission extraction and install reuse.
|
|
168
|
+
const loaded = await loadBundleModule(bundle);
|
|
169
|
+
const newPerms = extractBundlePermissions(loaded);
|
|
170
|
+
// 3. Look up the locally persisted old permissions (server-sourced
|
|
171
|
+
// installed list doesn't carry permissions — per spec they live
|
|
172
|
+
// in the local IndexedDB record).
|
|
173
|
+
let oldPerms = [];
|
|
174
|
+
try {
|
|
175
|
+
const localMeta = await loadMeta(id);
|
|
176
|
+
if (localMeta === null || localMeta === void 0 ? void 0 : localMeta.permissions)
|
|
177
|
+
oldPerms = localMeta.permissions;
|
|
178
|
+
}
|
|
179
|
+
catch (_e) {
|
|
180
|
+
// No local record (e.g. installed on a different browser); treat as
|
|
181
|
+
// empty. The diff will show all new permissions as additions.
|
|
182
|
+
}
|
|
183
|
+
// 4. If the permission set changed and a confirmation callback was
|
|
184
|
+
// provided, await the user's decision before touching the server.
|
|
185
|
+
const { added, removed } = diffPermissions(oldPerms, newPerms);
|
|
186
|
+
if ((added.length > 0 || removed.length > 0) && confirmPermissionChange) {
|
|
187
|
+
const ok = await confirmPermissionChange(added, removed);
|
|
188
|
+
if (!ok)
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// 5. Snapshot current state for rollback. Preserve the locally-known
|
|
192
|
+
// permissions so the rollback write still satisfies the InstalledPackage
|
|
193
|
+
// contract (installedRecord came from the server-sourced list which
|
|
194
|
+
// lacks permissions).
|
|
152
195
|
const oldBundle = await loadBundle(id);
|
|
153
|
-
const oldRecord = Object.assign({}, installedRecord);
|
|
154
|
-
//
|
|
196
|
+
const oldRecord = Object.assign(Object.assign({}, installedRecord), { permissions: oldPerms });
|
|
197
|
+
// 6. Push to server.
|
|
155
198
|
const manifest = {
|
|
156
199
|
id: meta.id,
|
|
157
200
|
type: meta.type,
|
|
@@ -171,8 +214,9 @@ export const storeShard = {
|
|
|
171
214
|
}
|
|
172
215
|
throw new Error(message);
|
|
173
216
|
}
|
|
174
|
-
//
|
|
175
|
-
|
|
217
|
+
// 7. Install locally (overwrites IndexedDB + re-registers). Reuse the
|
|
218
|
+
// already-loaded bundle so the ESM is not evaluated twice.
|
|
219
|
+
const result = await installPackage(bundle, meta, { loaded });
|
|
176
220
|
if (!result.success) {
|
|
177
221
|
// Rollback: restore old bundle and metadata.
|
|
178
222
|
if (oldBundle) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|