sh3-core 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -17,7 +17,7 @@ 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
22
  export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranch, } from './documents/sync-types';
23
23
  export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
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} &rarr; {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. Fetch the server bundle if the package declares one.
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
- // 3. Upload to server for persistent storage.
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
- // 4. Also install locally for immediate hot-load.
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. Snapshot current state for rollback.
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
- // 3. Push to server.
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
- // 4. Install locally (overwrites IndexedDB + re-registers).
175
- const result = await installPackage(bundle, meta);
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 {};