@ifc-lite/viewer 1.23.0 → 1.25.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.
Files changed (109) hide show
  1. package/.turbo/turbo-build.log +34 -31
  2. package/CHANGELOG.md +96 -0
  3. package/dist/assets/{basketViewActivator-Dn_bHUl2.js → basketViewActivator-CU8_toGq.js} +7 -7
  4. package/dist/assets/{bcf-B9SFl84i.js → bcf-DXGDhw56.js} +23 -23
  5. package/dist/assets/{deflate-yMpdCIqk.js → deflate-Bb1_H2Yf.js} +1 -1
  6. package/dist/assets/{exporters-D-BvrNIg.js → exporters-DZhLN0ux.js} +1861 -1658
  7. package/dist/assets/geometry-controller.worker-DQOSYqtw.js +7 -0
  8. package/dist/assets/geometry.worker-B62e03Ao.js +1 -0
  9. package/dist/assets/{geotiff-D1tvcDCb.js → geotiff-y0ZxbRJd.js} +10 -10
  10. package/dist/assets/{ids-DZLs0snJ.js → ids-DruUNtfD.js} +4 -4
  11. package/dist/assets/ifc-lite-Ch2T9pP9.js +7 -0
  12. package/dist/assets/{ifc-lite_bg-DyHX37GQ.wasm → ifc-lite_bg-D7O1WHgP.wasm} +0 -0
  13. package/dist/assets/{ifc-lite_bg-BIryVCXQ.wasm → ifc-lite_bg-iH_07wf8.wasm} +0 -0
  14. package/dist/assets/index-Bws3UAkj.css +1 -0
  15. package/dist/assets/{index-CXSBhkcJ.js → index-Dr88ZlSY.js} +64100 -47030
  16. package/dist/assets/{jpeg-DUMcZp24.js → jpeg-B3_loqFe.js} +1 -1
  17. package/dist/assets/lens-PYsLu_MA.js +1 -0
  18. package/dist/assets/{lerc-IN4uWojP.js → lerc-nkwS8ZUe.js} +1 -1
  19. package/dist/assets/{lzw-Cnw0hH-m.js → lzw-D3cW5Wpg.js} +1 -1
  20. package/dist/assets/{native-bridge-BVf2uzoH.js → native-bridge-BcYJooq8.js} +2 -2
  21. package/dist/assets/{packbits-BskJCwk0.js → packbits-DDN4xzB5.js} +1 -1
  22. package/dist/assets/{parser.worker-BdtkkaGf.js → parser.worker-BW1IMUed.js} +3 -3
  23. package/dist/assets/raw-CoIXstQ-.js +1 -0
  24. package/dist/assets/{sandbox-VLI_y7cl.js → sandbox-DETNEyQb.js} +498 -470
  25. package/dist/assets/{server-client-BLcKaWQB.js → server-client-CmzJOeS7.js} +1 -1
  26. package/dist/assets/{wasm-bridge-BAfZh7YT.js → wasm-bridge-CT7mK9W0.js} +1 -1
  27. package/dist/assets/{webimage-Db2xzze3.js → webimage-CBjgg4up.js} +1 -1
  28. package/dist/assets/{workerHelpers--sAYm9yN.js → workerHelpers-IEQDo8r3.js} +1 -1
  29. package/dist/assets/{zstd-BDToOQyD.js → zstd-C8oQ6qdS.js} +1 -1
  30. package/dist/index.html +8 -8
  31. package/package.json +11 -9
  32. package/src/App.tsx +5 -2
  33. package/src/components/extensions/AuditLogPanel.tsx +259 -0
  34. package/src/components/extensions/BundlePreview.tsx +102 -0
  35. package/src/components/extensions/CapabilityReview.tsx +333 -0
  36. package/src/components/extensions/ExtensionDockHost.tsx +192 -0
  37. package/src/components/extensions/ExtensionToolbarSlot.tsx +106 -0
  38. package/src/components/extensions/ExtensionsPanel.tsx +481 -0
  39. package/src/components/extensions/FlavorDialog.tsx +398 -0
  40. package/src/components/extensions/FlavorImportPreview.tsx +79 -0
  41. package/src/components/extensions/FlavorIndicator.tsx +81 -0
  42. package/src/components/extensions/FlavorListView.tsx +318 -0
  43. package/src/components/extensions/FlavorMergeDialog.tsx +326 -0
  44. package/src/components/extensions/HelpHint.tsx +182 -0
  45. package/src/components/extensions/IdeasPanel.tsx +344 -0
  46. package/src/components/extensions/PlanCard.tsx +227 -0
  47. package/src/components/extensions/PrivacyPanel.tsx +312 -0
  48. package/src/components/extensions/PromoteToolDialog.tsx +313 -0
  49. package/src/components/extensions/RepairQueuePanel.tsx +222 -0
  50. package/src/components/extensions/icon-registry.ts +92 -0
  51. package/src/components/extensions/toast-helpers.ts +49 -0
  52. package/src/components/extensions/widget/WidgetErrorBoundary.tsx +62 -0
  53. package/src/components/extensions/widget/WidgetRenderer.tsx +428 -0
  54. package/src/components/viewer/ChatPanel.tsx +251 -3
  55. package/src/components/viewer/CommandPalette.tsx +74 -4
  56. package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
  57. package/src/components/viewer/EntityContextMenu.tsx +70 -0
  58. package/src/components/viewer/ExportDialog.tsx +9 -1
  59. package/src/components/viewer/KeyboardShortcutsDialog.tsx +21 -6
  60. package/src/components/viewer/LensPanel.tsx +50 -0
  61. package/src/components/viewer/MainToolbar.tsx +170 -87
  62. package/src/components/viewer/ScriptPanel.tsx +105 -1
  63. package/src/components/viewer/Section2DPanel.tsx +58 -2
  64. package/src/components/viewer/StatusBar.tsx +18 -0
  65. package/src/components/viewer/ViewerLayout.tsx +53 -4
  66. package/src/components/viewer/Viewport.tsx +72 -0
  67. package/src/hooks/useActionLogger.test.ts +161 -0
  68. package/src/hooks/useActionLogger.ts +141 -0
  69. package/src/hooks/useForkExtension.ts +51 -0
  70. package/src/hooks/useIfcFederation.ts +7 -1
  71. package/src/hooks/useInstalledExtensions.ts +43 -0
  72. package/src/hooks/usePrivacyDisclosure.ts +48 -0
  73. package/src/hooks/useRunExtensionTests.ts +67 -0
  74. package/src/hooks/useSlotContributions.ts +38 -0
  75. package/src/hooks/useSymbolicAnnotations.test.ts +124 -0
  76. package/src/hooks/useSymbolicAnnotations.ts +776 -0
  77. package/src/lib/desktop-product.ts +7 -1
  78. package/src/lib/lens/adapter.ts +14 -0
  79. package/src/lib/llm/prompt-cache.ts +77 -0
  80. package/src/lib/llm/stream-client.ts +20 -2
  81. package/src/lib/llm/stream-direct.ts +11 -1
  82. package/src/lib/llm/system-prompt.ts +42 -0
  83. package/src/lib/safe-mode.ts +30 -0
  84. package/src/sdk/ExtensionHostProvider.tsx +103 -0
  85. package/src/services/extensions/flavor-service.ts +183 -0
  86. package/src/services/extensions/host-commands.ts +112 -0
  87. package/src/services/extensions/host-installer.ts +289 -0
  88. package/src/services/extensions/host.ts +514 -0
  89. package/src/services/extensions/idb-flavor-storage.test.ts +140 -0
  90. package/src/services/extensions/idb-flavor-storage.ts +241 -0
  91. package/src/services/extensions/idb-log-storage.test.ts +110 -0
  92. package/src/services/extensions/idb-log-storage.ts +171 -0
  93. package/src/services/extensions/idb-storage.ts +228 -0
  94. package/src/services/extensions/runtime-errors.ts +26 -0
  95. package/src/services/extensions/sandbox-factory.ts +217 -0
  96. package/src/store/constants.ts +48 -6
  97. package/src/store/index.ts +6 -1
  98. package/src/store/slices/drawing2DSlice.ts +8 -0
  99. package/src/store/slices/extensionsSlice.ts +90 -0
  100. package/src/store/slices/lensSlice.ts +28 -0
  101. package/src/store/slices/visibilitySlice.test.ts +6 -0
  102. package/src/store/slices/visibilitySlice.ts +17 -8
  103. package/src/store/types.ts +2 -0
  104. package/dist/assets/geometry-controller.worker-Cm5pvyR6.js +0 -7
  105. package/dist/assets/geometry.worker-ClNvXIrj.js +0 -1
  106. package/dist/assets/ifc-lite-BDg0iIbj.js +0 -7
  107. package/dist/assets/index-DS_xJQfP.css +0 -1
  108. package/dist/assets/lens-CpjUdqpw.js +0 -1
  109. package/dist/assets/raw-DzTtEZIY.js +0 -1
@@ -0,0 +1,112 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Command-dispatch helpers extracted from `host.ts`.
7
+ *
8
+ * `runExtensionCommand` finds the owning extension for a commandId,
9
+ * activates it, fires the matching `onCommand:<id>` event, loads the
10
+ * handler source, wraps it via `wrapEntrySource`, and runs the entry
11
+ * inside the activation's sandbox. Throws if no extension owns the
12
+ * command or the bundle is missing the entry path.
13
+ *
14
+ * Factored out so `ExtensionHostService` stays focused on lifecycle.
15
+ */
16
+
17
+ import {
18
+ parseCapabilities,
19
+ wrapEntrySource,
20
+ type ActivationDispatcher,
21
+ type ExtensionContextV1,
22
+ type ExtensionLoader,
23
+ type ExtensionRuntime,
24
+ type RuntimeRunResult,
25
+ } from '@ifc-lite/extensions';
26
+ import type { BimContext } from '@ifc-lite/sdk';
27
+ import type { IdbExtensionStorage } from './idb-storage.js';
28
+
29
+ export interface RunCommandDeps {
30
+ storage: IdbExtensionStorage;
31
+ loader: ExtensionLoader;
32
+ runtime: ExtensionRuntime;
33
+ dispatcher: ActivationDispatcher;
34
+ sdk: BimContext;
35
+ }
36
+
37
+ /**
38
+ * Dispatch an extension command end-to-end. Pure function — no
39
+ * `this`. Callers (host service) inject the dependencies.
40
+ */
41
+ export async function runExtensionCommand(
42
+ deps: RunCommandDeps,
43
+ commandId: string,
44
+ ): Promise<RuntimeRunResult | undefined> {
45
+ const records = await deps.storage.listExtensions();
46
+ for (const record of records) {
47
+ if (!record.enabled) continue;
48
+ const bundle = deps.loader.getBundle(record.id);
49
+ if (!bundle) continue;
50
+ const entry = bundle.manifest.entry.commands?.[commandId];
51
+ const declared = bundle.manifest.contributes?.commands?.some((c) => c.id === commandId);
52
+ if (!entry || !declared) continue;
53
+
54
+ const grantsResult = parseCapabilities(record.grantedCapabilities);
55
+ if (!grantsResult.ok) {
56
+ throw new Error(`Cannot run ${commandId}: stored capabilities for ${record.id} are invalid.`);
57
+ }
58
+ const grants = grantsResult.value;
59
+
60
+ const file = bundle.files.get(entry);
61
+ if (!file) {
62
+ throw new Error(`Command handler "${entry}" missing from bundle ${record.id}.`);
63
+ }
64
+ const source = file.text ?? new TextDecoder().decode(file.bytes);
65
+ const wrapResult = wrapEntrySource(source, {
66
+ entryFnName: 'run',
67
+ filename: entry,
68
+ });
69
+ if (!wrapResult.ok) {
70
+ throw new Error(
71
+ `Failed to prepare command "${commandId}": ${wrapResult.errors[0]?.message ?? 'wrap error'}`,
72
+ );
73
+ }
74
+ const wrappedSource = wrapResult.value;
75
+
76
+ // Reuse the cached activation across runs — that is the behaviour
77
+ // command tools shipped with originally and it works. Forcing a
78
+ // brand-new sandbox per run (an earlier experiment) regressed it:
79
+ // the dispose→recreate cycle is what surfaced "Lifetime not alive".
80
+ // The single retry below is kept purely as a safety net for a
81
+ // genuinely dead sandbox; it does NOT pre-emptively tear anything
82
+ // down.
83
+ const runOnce = async (isRetry: boolean): Promise<RuntimeRunResult> => {
84
+ try {
85
+ const activation = await deps.runtime.activate(record.id, grants, bundle);
86
+ await deps.dispatcher.fire(`onCommand:${commandId}` as const);
87
+ // Set ctx via setGlobal. The BimSandboxHandle special-cases
88
+ // `__ifclite_ctx__` to synthesize from the bridge-installed
89
+ // `globalThis.bim` (the wrapped SDK is cyclic and would crash
90
+ // JSON.stringify). The wrap also falls back to globalThis.bim
91
+ // if ctx is somehow unset. setGlobal is inside the try so a
92
+ // "Sandbox disposed" on a dead handle also triggers the retry.
93
+ const ctx: ExtensionContextV1 = { bim: deps.sdk };
94
+ await activation.sandbox.setGlobal('__ifclite_ctx__', ctx);
95
+ return await activation.sandbox.run(wrappedSource, { filename: entry });
96
+ } catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ // Only on an actual failure that looks like a dead realm do we
99
+ // tear down and retry once — never pre-emptively.
100
+ const isDeadSandbox =
101
+ /Lifetime not alive|QuickJSUseAfterFree|Sandbox was torn down|Sandbox disposed|not initialized/i.test(msg);
102
+ if (isDeadSandbox && !isRetry) {
103
+ await deps.runtime.deactivate(record.id);
104
+ return runOnce(true);
105
+ }
106
+ throw err;
107
+ }
108
+ };
109
+ return runOnce(false);
110
+ }
111
+ throw new Error(`No installed, enabled extension owns command "${commandId}".`);
112
+ }
@@ -0,0 +1,289 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Install / uninstall / enable-disable lifecycle for the extension
7
+ * host, extracted from `host.ts`. The class methods delegate here so
8
+ * the lifecycle logic — including rollback on failed updates — can be
9
+ * read and tested as a unit.
10
+ *
11
+ * All functions take the host's primitives as a `deps` object and an
12
+ * optional `notify` callback for emitting changes back to the host.
13
+ */
14
+
15
+ import {
16
+ sha256Hex,
17
+ unpackBundleWithSignature,
18
+ verifyBundle,
19
+ type ActionIntent,
20
+ type ActionParams,
21
+ type ActivationDispatcher,
22
+ type AuditLog,
23
+ type Bundle,
24
+ type ExtensionLoader,
25
+ type ExtensionRuntime,
26
+ type InstalledExtensionRecord,
27
+ type LoadedExtensionStatus,
28
+ type SignatureInfo,
29
+ type ValidationError,
30
+ type ValidationResult,
31
+ } from '@ifc-lite/extensions';
32
+ import type { IdbExtensionStorage } from './idb-storage.js';
33
+
34
+ export interface ExtensionInstallSummary {
35
+ id: string;
36
+ version: string;
37
+ bundleHash: string;
38
+ capabilities: string[];
39
+ bundle: Bundle;
40
+ /**
41
+ * True iff the bundle envelope contained a signature block and that
42
+ * signature verified against the bundle's canonical content hash.
43
+ * UI should surface signed/unsigned status alongside the capability
44
+ * review so users can refuse unsigned bundles for sensitive grants.
45
+ */
46
+ signed: boolean;
47
+ /** Verified signer info — only present when `signed` is true. */
48
+ signature?: SignatureInfo;
49
+ }
50
+
51
+ export interface InstallerDeps {
52
+ storage: IdbExtensionStorage;
53
+ runtime: ExtensionRuntime;
54
+ loader: ExtensionLoader;
55
+ dispatcher: ActivationDispatcher;
56
+ audit: AuditLog;
57
+ emitAction: <K extends ActionIntent>(intent: K, params: ActionParams[K]) => void;
58
+ emit: () => void;
59
+ }
60
+
61
+ export class ExtensionInstallError extends Error {
62
+ readonly validationErrors: readonly ValidationError[];
63
+ constructor(message: string, errors: readonly ValidationError[]) {
64
+ super(message);
65
+ this.name = 'ExtensionInstallError';
66
+ this.validationErrors = errors;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Inspect a `.iflx` byte string without installing it.
72
+ *
73
+ * If the bundle envelope carries a signature block we verify it here
74
+ * — failing the preview rather than the later install, so the user
75
+ * never gets as far as the capability-review screen for a tampered
76
+ * bundle. Unsigned bundles preview successfully but are flagged via
77
+ * `summary.signed = false` so the UI can warn (or block, depending
78
+ * on policy) before granting capabilities.
79
+ */
80
+ export async function previewBundleBytes(
81
+ bytes: Uint8Array,
82
+ ): Promise<ValidationResult<ExtensionInstallSummary>> {
83
+ const unpacked = unpackBundleWithSignature(bytes);
84
+ if (!unpacked.ok) return unpacked;
85
+ const { bundle, signature: sigBlock } = unpacked.value;
86
+
87
+ let signature: SignatureInfo | undefined;
88
+ if (sigBlock) {
89
+ try {
90
+ signature = await verifyBundle(bundle, sigBlock);
91
+ } catch (err) {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ return {
94
+ ok: false,
95
+ errors: [{
96
+ path: 'signature',
97
+ code: 'invalid_format',
98
+ message: `Signature verification failed: ${message}`,
99
+ }],
100
+ };
101
+ }
102
+ }
103
+
104
+ const hash = await sha256Hex(bytes);
105
+ return {
106
+ ok: true,
107
+ value: {
108
+ id: bundle.manifest.id,
109
+ version: bundle.manifest.version,
110
+ bundleHash: hash,
111
+ capabilities: bundle.manifest.capabilities,
112
+ bundle,
113
+ signed: signature !== undefined,
114
+ signature,
115
+ },
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Install a previewed bundle. On failure, restore the previous install
121
+ * (if any) so a bad update doesn't wipe a working extension.
122
+ */
123
+ export async function installFromBytes(
124
+ deps: InstallerDeps,
125
+ bytes: Uint8Array,
126
+ grantedCapabilities: string[],
127
+ ): Promise<LoadedExtensionStatus> {
128
+ const preview = await previewBundleBytes(bytes);
129
+ if (!preview.ok) {
130
+ throw new ExtensionInstallError('Bundle did not unpack', preview.errors);
131
+ }
132
+ const { bundle, bundleHash, id, version, signed, signature } = preview.value;
133
+
134
+ // Callers can only grant capabilities the manifest declares. Drops
135
+ // accidental grant escalation if the review screen pre-filled state
136
+ // from an earlier version of the bundle.
137
+ const declaredCaps = new Set(bundle.manifest.capabilities);
138
+ const unexpected = grantedCapabilities.filter((cap) => !declaredCaps.has(cap));
139
+ if (unexpected.length > 0) {
140
+ throw new ExtensionInstallError(
141
+ `Unexpected capability grants not declared by manifest: ${unexpected.join(', ')}`,
142
+ unexpected.map((cap) => ({
143
+ path: 'grantedCapabilities',
144
+ code: 'invalid_capability' as const,
145
+ message: `Capability "${cap}" was not requested by the bundle manifest.`,
146
+ })),
147
+ );
148
+ }
149
+
150
+ // Snapshot the previous install so we can restore it if the new
151
+ // bundle fails to load. Without this, a bad update wipes the
152
+ // user's previously-working install entirely.
153
+ const previous = await deps.storage.getExtension(id);
154
+ let previousBundleBytes: Uint8Array | undefined;
155
+ if (previous && previous.version !== version) {
156
+ previousBundleBytes = await deps.storage.getBundle(id, previous.version);
157
+ await teardownExtension(deps, id);
158
+ }
159
+
160
+ const record: InstalledExtensionRecord = {
161
+ id,
162
+ version,
163
+ bundleHash,
164
+ grantedCapabilities,
165
+ enabled: true,
166
+ installedAt: new Date().toISOString(),
167
+ source: 'local',
168
+ };
169
+ await deps.storage.putBundle(id, version, bytes);
170
+ await deps.storage.putExtension(record);
171
+
172
+ const status = await deps.loader.load(id);
173
+ if (!status || !status.ok) {
174
+ // Roll back. Delete the new bundle + record we just wrote.
175
+ await deps.storage.deleteBundle(id, version);
176
+ await deps.storage.deleteExtension(id);
177
+
178
+ // Restore the previous install if we had one — re-write its
179
+ // record and bundle bytes, then re-load. Best effort: log if
180
+ // restore itself fails, don't mask the original error.
181
+ if (previous && previousBundleBytes) {
182
+ try {
183
+ await deps.storage.putBundle(id, previous.version, previousBundleBytes);
184
+ await deps.storage.putExtension(previous);
185
+ await deps.loader.load(id);
186
+ } catch (restoreErr) {
187
+ console.error(
188
+ `[ext-host] Failed to restore previous install of ${id}:`,
189
+ restoreErr,
190
+ );
191
+ }
192
+ }
193
+ throw new ExtensionInstallError(
194
+ 'Loader rejected the new bundle',
195
+ status?.errors ?? [],
196
+ );
197
+ }
198
+
199
+ deps.audit.append({
200
+ kind: previous ? 'update' : 'install',
201
+ extensionId: id,
202
+ version,
203
+ previousVersion: previous?.version,
204
+ grantedCapabilities,
205
+ signed,
206
+ signerFingerprint: signature?.fingerprint,
207
+ });
208
+ deps.emitAction('extension.install', { id });
209
+
210
+ // Re-fire onStartup so the freshly-loaded extension activates if it
211
+ // declared the event. Other startup-subscribed extensions are
212
+ // unaffected (the dispatcher dedupes activations per session).
213
+ await deps.dispatcher.fire('onStartup');
214
+ deps.emit();
215
+ return status;
216
+ }
217
+
218
+ /** Uninstall an extension and remove its bundle. */
219
+ export async function uninstall(deps: InstallerDeps, id: string): Promise<void> {
220
+ const record = await deps.storage.getExtension(id);
221
+ if (!record) return;
222
+ await teardownExtension(deps, id);
223
+ // Delete bundle bytes too — the storage's cascade already handles
224
+ // this on deleteExtension, but call it explicitly so the contract
225
+ // is clear at this layer and a future storage impl can't drift.
226
+ await deps.storage.deleteBundle(id, record.version);
227
+ await deps.storage.deleteExtension(id);
228
+ deps.audit.append({
229
+ kind: 'uninstall',
230
+ extensionId: id,
231
+ version: record.version,
232
+ });
233
+ deps.emitAction('extension.uninstall', { id });
234
+ deps.emit();
235
+ }
236
+
237
+ /** Enable/disable without uninstalling. */
238
+ export async function setEnabled(
239
+ deps: InstallerDeps,
240
+ id: string,
241
+ enabled: boolean,
242
+ ): Promise<void> {
243
+ const record = await deps.storage.getExtension(id);
244
+ if (!record) return;
245
+ if (record.enabled === enabled) return;
246
+
247
+ if (enabled) {
248
+ // Persist enabled=true only after the loader confirms it can
249
+ // bring the extension up. Without this, a failed load leaves the
250
+ // persisted state lying about runtime reality.
251
+ const tentative = { ...record, enabled: true };
252
+ await deps.storage.putExtension(tentative);
253
+ const status = await deps.loader.load(id);
254
+ if (!status?.ok) {
255
+ await deps.storage.putExtension(record);
256
+ throw new ExtensionInstallError(
257
+ `Failed to enable extension ${id}`,
258
+ status?.errors ?? [],
259
+ );
260
+ }
261
+ } else {
262
+ await teardownExtension(deps, id);
263
+ await deps.storage.putExtension({ ...record, enabled: false });
264
+ }
265
+ deps.audit.append({
266
+ kind: enabled ? 'enable' : 'disable',
267
+ extensionId: id,
268
+ version: record.version,
269
+ });
270
+ deps.emitAction(enabled ? 'extension.enable' : 'extension.disable', { id });
271
+ deps.emit();
272
+ }
273
+
274
+ /**
275
+ * Tear an extension down: run its `entry.deactivate` hook (via the
276
+ * loaded bundle), unload it, and clear the dispatcher's
277
+ * "already activated" flag so a later enable / event re-fire genuinely
278
+ * re-activates it. Used by update / uninstall / disable.
279
+ */
280
+ async function teardownExtension(deps: InstallerDeps, id: string): Promise<void> {
281
+ const bundle = deps.loader.getBundle(id);
282
+ if (bundle) {
283
+ await deps.runtime.deactivateWithBundle(id, bundle);
284
+ } else {
285
+ await deps.runtime.deactivate(id);
286
+ }
287
+ await deps.loader.unload(id);
288
+ deps.dispatcher.resetActivation(id);
289
+ }