sh3-core 0.8.2 → 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.
Files changed (102) hide show
  1. package/dist/api.d.ts +4 -7
  2. package/dist/api.js +2 -4
  3. package/dist/app/store/InstalledView.svelte +55 -1
  4. package/dist/app/store/PermissionConfirmModal.svelte +232 -0
  5. package/dist/app/store/PermissionConfirmModal.svelte.d.ts +17 -0
  6. package/dist/app/store/StoreView.svelte +119 -5
  7. package/dist/app/store/storeShard.svelte.d.ts +10 -1
  8. package/dist/app/store/storeShard.svelte.js +51 -7
  9. package/dist/app/store/storeShard.svelte.test.js +34 -0
  10. package/dist/apps/types.d.ts +3 -5
  11. package/dist/documents/backends.d.ts +2 -0
  12. package/dist/documents/backends.js +6 -0
  13. package/dist/documents/browse.d.ts +31 -1
  14. package/dist/documents/browse.js +18 -2
  15. package/dist/documents/browse.test.js +81 -0
  16. package/dist/documents/handle.js +13 -5
  17. package/dist/documents/handle.test.js +55 -0
  18. package/dist/documents/http-backend.d.ts +11 -4
  19. package/dist/documents/http-backend.js +37 -11
  20. package/dist/documents/index.d.ts +2 -1
  21. package/dist/documents/index.js +1 -1
  22. package/dist/documents/sync-types.d.ts +45 -0
  23. package/dist/documents/sync-types.js +11 -0
  24. package/dist/documents/types.d.ts +69 -2
  25. package/dist/documents/types.js +32 -2
  26. package/dist/keys/ConsentDialog.svelte +4 -4
  27. package/dist/keys/consent.test.js +4 -3
  28. package/dist/keys/types.d.ts +4 -2
  29. package/dist/registry/client.js +3 -0
  30. package/dist/registry/installer.d.ts +4 -1
  31. package/dist/registry/installer.js +25 -11
  32. package/dist/registry/permission-descriptions.d.ts +21 -0
  33. package/dist/registry/permission-descriptions.js +67 -0
  34. package/dist/registry/permission-descriptions.test.js +86 -0
  35. package/dist/registry/schema.js +19 -6
  36. package/dist/registry/types.d.ts +17 -5
  37. package/dist/server-shard/types.d.ts +55 -8
  38. package/dist/shards/activate-browse.test.js +87 -3
  39. package/dist/shards/activate.svelte.js +9 -31
  40. package/dist/shards/types.d.ts +0 -15
  41. package/dist/shell/views/KeysAndPeers.svelte +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +2 -10
  45. package/dist/documents/journal-hook.d.ts +0 -6
  46. package/dist/documents/journal-hook.js +0 -16
  47. package/dist/documents/sync/activate-integration.test.js +0 -37
  48. package/dist/documents/sync/components/DocumentSyncExplorer.svelte +0 -99
  49. package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +0 -15
  50. package/dist/documents/sync/components/SyncGrantPicker.svelte +0 -70
  51. package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +0 -12
  52. package/dist/documents/sync/conflicts.d.ts +0 -30
  53. package/dist/documents/sync/conflicts.js +0 -77
  54. package/dist/documents/sync/conflicts.test.js +0 -71
  55. package/dist/documents/sync/engine.d.ts +0 -19
  56. package/dist/documents/sync/engine.js +0 -188
  57. package/dist/documents/sync/engine.test.d.ts +0 -1
  58. package/dist/documents/sync/engine.test.js +0 -169
  59. package/dist/documents/sync/handle.d.ts +0 -11
  60. package/dist/documents/sync/handle.js +0 -79
  61. package/dist/documents/sync/handle.test.js +0 -56
  62. package/dist/documents/sync/hash.d.ts +0 -1
  63. package/dist/documents/sync/hash.js +0 -13
  64. package/dist/documents/sync/hash.test.d.ts +0 -1
  65. package/dist/documents/sync/hash.test.js +0 -20
  66. package/dist/documents/sync/index.d.ts +0 -5
  67. package/dist/documents/sync/index.js +0 -10
  68. package/dist/documents/sync/journal.d.ts +0 -30
  69. package/dist/documents/sync/journal.js +0 -179
  70. package/dist/documents/sync/journal.test.d.ts +0 -1
  71. package/dist/documents/sync/journal.test.js +0 -87
  72. package/dist/documents/sync/observer.d.ts +0 -3
  73. package/dist/documents/sync/observer.js +0 -45
  74. package/dist/documents/sync/registry.d.ts +0 -13
  75. package/dist/documents/sync/registry.js +0 -73
  76. package/dist/documents/sync/registry.test.d.ts +0 -1
  77. package/dist/documents/sync/registry.test.js +0 -53
  78. package/dist/documents/sync/serialization.d.ts +0 -5
  79. package/dist/documents/sync/serialization.js +0 -24
  80. package/dist/documents/sync/serialization.test.d.ts +0 -1
  81. package/dist/documents/sync/serialization.test.js +0 -26
  82. package/dist/documents/sync/singleton.d.ts +0 -11
  83. package/dist/documents/sync/singleton.js +0 -26
  84. package/dist/documents/sync/tombstones.d.ts +0 -19
  85. package/dist/documents/sync/tombstones.js +0 -58
  86. package/dist/documents/sync/tombstones.test.d.ts +0 -1
  87. package/dist/documents/sync/tombstones.test.js +0 -37
  88. package/dist/documents/sync/types.d.ts +0 -116
  89. package/dist/documents/sync/types.js +0 -27
  90. package/dist/documents/sync/write-hook.test.d.ts +0 -1
  91. package/dist/documents/sync/write-hook.test.js +0 -36
  92. package/dist/server-sync.d.ts +0 -6
  93. package/dist/server-sync.js +0 -634
  94. package/dist/server-sync.js.map +0 -7
  95. package/dist/shards/activate-sync-registry.test.d.ts +0 -1
  96. package/dist/shards/activate-sync-registry.test.js +0 -42
  97. package/dist/testing.d.ts +0 -3
  98. package/dist/testing.js +0 -77
  99. package/dist/testing.js.map +0 -7
  100. /package/dist/{documents/sync/activate-integration.test.d.ts → app/store/storeShard.svelte.test.d.ts} +0 -0
  101. /package/dist/documents/{sync/handle.test.d.ts → handle.test.d.ts} +0 -0
  102. /package/dist/{documents/sync/conflicts.test.d.ts → registry/permission-descriptions.test.d.ts} +0 -0
package/dist/api.d.ts CHANGED
@@ -17,13 +17,10 @@ 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 { SyncHandle, SyncScope, ManifestEntry, ApplyEntry, ApplyOpts, ApplyOutcome, ApplyBatchResult, ConflictPolicy, ConflictResolution, ConflictContext, JournalEntry, ChangePage, GrantRecord, } from './documents/sync/types';
23
- export { PERMISSION_DOCUMENTS_SYNC, ScopeNotGrantedError, ScopeRevokedError, TenantMismatchError, } from './documents/sync/types';
24
- export type { SyncRegistry } from './documents/sync/registry';
25
- export { default as SyncGrantPicker } from './documents/sync/components/SyncGrantPicker.svelte';
26
- export { default as DocumentSyncExplorer } from './documents/sync/components/DocumentSyncExplorer.svelte';
22
+ export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranch, } from './documents/sync-types';
23
+ export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
27
24
  export { registeredShards, activeShards } from './shards/activate.svelte';
28
25
  export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, } from './registry/types';
29
26
  export type { ResolvedPackage } from './registry/client';
@@ -39,7 +36,7 @@ export declare const capabilities: {
39
36
  /** Whether this target supports hot-installing packages via dynamic import from blob URL. */
40
37
  readonly hotInstall: boolean;
41
38
  };
42
- export type { ServerShard, ServerShardContext } from './server-shard/types';
39
+ export type { ServerShard, ServerShardContext, TenantDocumentAPI } from './server-shard/types';
43
40
  export type { Verb, VerbContext, ShellApi } from './verbs/types';
44
41
  export type { Scrollback } from './shell-shard/scrollback.svelte';
45
42
  export type { SessionClient } from './shell-shard/session-client.svelte';
package/dist/api.js CHANGED
@@ -29,10 +29,8 @@ 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';
33
- export { PERMISSION_DOCUMENTS_SYNC, ScopeNotGrantedError, ScopeRevokedError, TenantMismatchError, } from './documents/sync/types';
34
- export { default as SyncGrantPicker } from './documents/sync/components/SyncGrantPicker.svelte';
35
- export { default as DocumentSyncExplorer } from './documents/sync/components/DocumentSyncExplorer.svelte';
32
+ export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
33
+ export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
36
34
  // Shard introspection — read-only reactive maps exposing which shards are
37
35
  // known to the host and which are currently active. Intended for diagnostic
38
36
  // and tooling shards that need to visualize framework state. Phase 9
@@ -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
  }