@metamask/snaps-controllers 9.7.0 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/cronjob/CronjobController.cjs +21 -34
  3. package/dist/cronjob/CronjobController.cjs.map +1 -1
  4. package/dist/cronjob/CronjobController.mjs +21 -34
  5. package/dist/cronjob/CronjobController.mjs.map +1 -1
  6. package/dist/insights/SnapInsightsController.cjs +194 -144
  7. package/dist/insights/SnapInsightsController.cjs.map +1 -1
  8. package/dist/insights/SnapInsightsController.mjs +193 -143
  9. package/dist/insights/SnapInsightsController.mjs.map +1 -1
  10. package/dist/interface/SnapInterfaceController.cjs +90 -65
  11. package/dist/interface/SnapInterfaceController.cjs.map +1 -1
  12. package/dist/interface/SnapInterfaceController.d.cts +2 -1
  13. package/dist/interface/SnapInterfaceController.d.cts.map +1 -1
  14. package/dist/interface/SnapInterfaceController.d.mts +2 -1
  15. package/dist/interface/SnapInterfaceController.d.mts.map +1 -1
  16. package/dist/interface/SnapInterfaceController.mjs +90 -65
  17. package/dist/interface/SnapInterfaceController.mjs.map +1 -1
  18. package/dist/services/AbstractExecutionService.cjs +71 -77
  19. package/dist/services/AbstractExecutionService.cjs.map +1 -1
  20. package/dist/services/AbstractExecutionService.mjs +71 -77
  21. package/dist/services/AbstractExecutionService.mjs.map +1 -1
  22. package/dist/services/ProxyPostMessageStream.cjs +19 -26
  23. package/dist/services/ProxyPostMessageStream.cjs.map +1 -1
  24. package/dist/services/ProxyPostMessageStream.mjs +19 -26
  25. package/dist/services/ProxyPostMessageStream.mjs.map +1 -1
  26. package/dist/services/iframe/IframeExecutionService.cjs +1 -0
  27. package/dist/services/iframe/IframeExecutionService.cjs.map +1 -1
  28. package/dist/services/iframe/IframeExecutionService.mjs +1 -0
  29. package/dist/services/iframe/IframeExecutionService.mjs.map +1 -1
  30. package/dist/services/offscreen/OffscreenExecutionService.cjs +3 -16
  31. package/dist/services/offscreen/OffscreenExecutionService.cjs.map +1 -1
  32. package/dist/services/offscreen/OffscreenExecutionService.mjs +3 -16
  33. package/dist/services/offscreen/OffscreenExecutionService.mjs.map +1 -1
  34. package/dist/services/proxy/ProxyExecutionService.cjs +4 -17
  35. package/dist/services/proxy/ProxyExecutionService.cjs.map +1 -1
  36. package/dist/services/proxy/ProxyExecutionService.mjs +4 -17
  37. package/dist/services/proxy/ProxyExecutionService.mjs.map +1 -1
  38. package/dist/services/webview/WebViewExecutionService.cjs +9 -23
  39. package/dist/services/webview/WebViewExecutionService.cjs.map +1 -1
  40. package/dist/services/webview/WebViewExecutionService.mjs +9 -23
  41. package/dist/services/webview/WebViewExecutionService.mjs.map +1 -1
  42. package/dist/services/webview/WebViewMessageStream.cjs +12 -25
  43. package/dist/services/webview/WebViewMessageStream.cjs.map +1 -1
  44. package/dist/services/webview/WebViewMessageStream.d.cts +1 -2
  45. package/dist/services/webview/WebViewMessageStream.d.cts.map +1 -1
  46. package/dist/services/webview/WebViewMessageStream.d.mts +1 -2
  47. package/dist/services/webview/WebViewMessageStream.d.mts.map +1 -1
  48. package/dist/services/webview/WebViewMessageStream.mjs +12 -25
  49. package/dist/services/webview/WebViewMessageStream.mjs.map +1 -1
  50. package/dist/services/webview/index.cjs +1 -0
  51. package/dist/services/webview/index.cjs.map +1 -1
  52. package/dist/services/webview/index.d.cts +1 -0
  53. package/dist/services/webview/index.d.cts.map +1 -1
  54. package/dist/services/webview/index.d.mts +1 -0
  55. package/dist/services/webview/index.d.mts.map +1 -1
  56. package/dist/services/webview/index.mjs +1 -0
  57. package/dist/services/webview/index.mjs.map +1 -1
  58. package/dist/services/webworker/WebWorkerExecutionService.cjs +10 -23
  59. package/dist/services/webworker/WebWorkerExecutionService.cjs.map +1 -1
  60. package/dist/services/webworker/WebWorkerExecutionService.mjs +10 -23
  61. package/dist/services/webworker/WebWorkerExecutionService.mjs.map +1 -1
  62. package/dist/snaps/RequestQueue.cjs +2 -0
  63. package/dist/snaps/RequestQueue.cjs.map +1 -1
  64. package/dist/snaps/RequestQueue.mjs +2 -0
  65. package/dist/snaps/RequestQueue.mjs.map +1 -1
  66. package/dist/snaps/SnapController.cjs +1141 -1001
  67. package/dist/snaps/SnapController.cjs.map +1 -1
  68. package/dist/snaps/SnapController.mjs +1140 -1000
  69. package/dist/snaps/SnapController.mjs.map +1 -1
  70. package/dist/snaps/Timer.cjs +1 -0
  71. package/dist/snaps/Timer.cjs.map +1 -1
  72. package/dist/snaps/Timer.mjs +1 -0
  73. package/dist/snaps/Timer.mjs.map +1 -1
  74. package/dist/snaps/location/http.cjs +11 -7
  75. package/dist/snaps/location/http.cjs.map +1 -1
  76. package/dist/snaps/location/http.mjs +11 -7
  77. package/dist/snaps/location/http.mjs.map +1 -1
  78. package/dist/snaps/location/local.cjs +4 -17
  79. package/dist/snaps/location/local.cjs.map +1 -1
  80. package/dist/snaps/location/local.mjs +4 -17
  81. package/dist/snaps/location/local.mjs.map +1 -1
  82. package/dist/snaps/location/npm.cjs +25 -37
  83. package/dist/snaps/location/npm.cjs.map +1 -1
  84. package/dist/snaps/location/npm.mjs +25 -37
  85. package/dist/snaps/location/npm.mjs.map +1 -1
  86. package/dist/snaps/registry/json.cjs +172 -173
  87. package/dist/snaps/registry/json.cjs.map +1 -1
  88. package/dist/snaps/registry/json.mjs +171 -172
  89. package/dist/snaps/registry/json.mjs.map +1 -1
  90. package/package.json +38 -23
@@ -1,15 +1,3 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _SnapController_instances, _SnapController_closeAllConnections, _SnapController_dynamicPermissions, _SnapController_environmentEndowmentPermissions, _SnapController_excludedPermissions, _SnapController_featureFlags, _SnapController_fetchFunction, _SnapController_idleTimeCheckInterval, _SnapController_maxIdleTime, _SnapController_encryptor, _SnapController_getMnemonic, _SnapController_getFeatureFlags, _SnapController_detectSnapLocation, _SnapController_snapsRuntimeData, _SnapController_rollbackSnapshots, _SnapController_timeoutForLastRequestStatus, _SnapController_statusMachine, _SnapController_preinstalledSnaps, _SnapController_initializeStateMachine, _SnapController_registerMessageHandlers, _SnapController_handlePreinstalledSnaps, _SnapController_pollForLastRequestStatus, _SnapController_blockSnap, _SnapController_unblockSnap, _SnapController_assertIsInstallAllowed, _SnapController_assertCanInstallSnaps, _SnapController_assertCanUsePlatform, _SnapController_stopSnapsLastRequestPastMax, _SnapController_transition, _SnapController_terminateSnap, _SnapController_getSnapEncryptionKey, _SnapController_decryptSnapState, _SnapController_encryptSnapState, _SnapController_handleInitialConnections, _SnapController_addSnapToSubject, _SnapController_removeSnapFromSubjects, _SnapController_revokeAllSnapPermissions, _SnapController_createApproval, _SnapController_updateApproval, _SnapController_resolveAllowlistVersion, _SnapController_add, _SnapController_startSnap, _SnapController_getEndowments, _SnapController_set, _SnapController_validateSnapPermissions, _SnapController_getExecutionTimeout, _SnapController_getRpcRequestHandler, _SnapController_createInterface, _SnapController_assertInterfaceExists, _SnapController_transformSnapRpcRequestResult, _SnapController_assertSnapRpcRequestResult, _SnapController_recordSnapRpcRequestStart, _SnapController_recordSnapRpcRequestFinish, _SnapController_getRollbackSnapshot, _SnapController_createRollbackSnapshot, _SnapController_rollbackSnap, _SnapController_rollbackSnaps, _SnapController_getRuntime, _SnapController_getRuntimeExpect, _SnapController_setupRuntime, _SnapController_calculatePermissionsChange, _SnapController_isSubjectConnectedToSnap, _SnapController_calculateConnectionsChange, _SnapController_updatePermissions, _SnapController_isValidUpdate, _SnapController_callLifecycleHook;
13
1
  import { BaseController } from "@metamask/base-controller";
14
2
  import { SubjectType } from "@metamask/permission-controller";
15
3
  import { rpcErrors } from "@metamask/rpc-errors";
@@ -68,6 +56,25 @@ const name = 'SnapController';
68
56
  * - Start: Initializes the snap in its SES realm with the authorized permissions.
69
57
  */
70
58
  export class SnapController extends BaseController {
59
+ #closeAllConnections;
60
+ #dynamicPermissions;
61
+ #environmentEndowmentPermissions;
62
+ #excludedPermissions;
63
+ #featureFlags;
64
+ #fetchFunction;
65
+ #idleTimeCheckInterval;
66
+ #maxIdleTime;
67
+ // This property cannot be hash private yet because of tests.
68
+ maxRequestTime;
69
+ #encryptor;
70
+ #getMnemonic;
71
+ #getFeatureFlags;
72
+ #detectSnapLocation;
73
+ #snapsRuntimeData;
74
+ #rollbackSnapshots;
75
+ #timeoutForLastRequestStatus;
76
+ #statusMachine;
77
+ #preinstalledSnaps;
71
78
  constructor({ closeAllConnections, messenger, state, dynamicPermissions = ['eth_accounts'], environmentEndowmentPermissions = [], excludedPermissions = {}, idleTimeCheckInterval = inMilliseconds(5, Duration.Second), maxIdleTime = inMilliseconds(30, Duration.Second), maxRequestTime = inMilliseconds(60, Duration.Second), fetchFunction = globalThis.fetch.bind(undefined), featureFlags = {}, detectSnapLocation: detectSnapLocationFunction = detectSnapLocation, preinstalledSnaps = null, encryptor, getMnemonic, getFeatureFlags = () => ({}), }) {
72
79
  super({
73
80
  messenger,
@@ -107,65 +114,202 @@ export class SnapController extends BaseController {
107
114
  ...state,
108
115
  },
109
116
  });
110
- _SnapController_instances.add(this);
111
- _SnapController_closeAllConnections.set(this, void 0);
112
- _SnapController_dynamicPermissions.set(this, void 0);
113
- _SnapController_environmentEndowmentPermissions.set(this, void 0);
114
- _SnapController_excludedPermissions.set(this, void 0);
115
- _SnapController_featureFlags.set(this, void 0);
116
- _SnapController_fetchFunction.set(this, void 0);
117
- _SnapController_idleTimeCheckInterval.set(this, void 0);
118
- _SnapController_maxIdleTime.set(this, void 0);
119
- _SnapController_encryptor.set(this, void 0);
120
- _SnapController_getMnemonic.set(this, void 0);
121
- _SnapController_getFeatureFlags.set(this, void 0);
122
- _SnapController_detectSnapLocation.set(this, void 0);
123
- _SnapController_snapsRuntimeData.set(this, void 0);
124
- _SnapController_rollbackSnapshots.set(this, void 0);
125
- _SnapController_timeoutForLastRequestStatus.set(this, void 0);
126
- _SnapController_statusMachine.set(this, void 0);
127
- _SnapController_preinstalledSnaps.set(this, void 0);
128
- __classPrivateFieldSet(this, _SnapController_closeAllConnections, closeAllConnections, "f");
129
- __classPrivateFieldSet(this, _SnapController_dynamicPermissions, dynamicPermissions, "f");
130
- __classPrivateFieldSet(this, _SnapController_environmentEndowmentPermissions, environmentEndowmentPermissions, "f");
131
- __classPrivateFieldSet(this, _SnapController_excludedPermissions, excludedPermissions, "f");
132
- __classPrivateFieldSet(this, _SnapController_featureFlags, featureFlags, "f");
133
- __classPrivateFieldSet(this, _SnapController_fetchFunction, fetchFunction, "f");
134
- __classPrivateFieldSet(this, _SnapController_idleTimeCheckInterval, idleTimeCheckInterval, "f");
135
- __classPrivateFieldSet(this, _SnapController_maxIdleTime, maxIdleTime, "f");
117
+ this.#closeAllConnections = closeAllConnections;
118
+ this.#dynamicPermissions = dynamicPermissions;
119
+ this.#environmentEndowmentPermissions = environmentEndowmentPermissions;
120
+ this.#excludedPermissions = excludedPermissions;
121
+ this.#featureFlags = featureFlags;
122
+ this.#fetchFunction = fetchFunction;
123
+ this.#idleTimeCheckInterval = idleTimeCheckInterval;
124
+ this.#maxIdleTime = maxIdleTime;
136
125
  this.maxRequestTime = maxRequestTime;
137
- __classPrivateFieldSet(this, _SnapController_detectSnapLocation, detectSnapLocationFunction, "f");
138
- __classPrivateFieldSet(this, _SnapController_encryptor, encryptor, "f");
139
- __classPrivateFieldSet(this, _SnapController_getMnemonic, getMnemonic, "f");
140
- __classPrivateFieldSet(this, _SnapController_getFeatureFlags, getFeatureFlags, "f");
141
- __classPrivateFieldSet(this, _SnapController_preinstalledSnaps, preinstalledSnaps, "f");
126
+ this.#detectSnapLocation = detectSnapLocationFunction;
127
+ this.#encryptor = encryptor;
128
+ this.#getMnemonic = getMnemonic;
129
+ this.#getFeatureFlags = getFeatureFlags;
130
+ this.#preinstalledSnaps = preinstalledSnaps;
142
131
  this._onUnhandledSnapError = this._onUnhandledSnapError.bind(this);
143
132
  this._onOutboundRequest = this._onOutboundRequest.bind(this);
144
133
  this._onOutboundResponse = this._onOutboundResponse.bind(this);
145
- __classPrivateFieldSet(this, _SnapController_rollbackSnapshots, new Map(), "f");
146
- __classPrivateFieldSet(this, _SnapController_snapsRuntimeData, new Map(), "f");
147
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_pollForLastRequestStatus).call(this);
134
+ this.#rollbackSnapshots = new Map();
135
+ this.#snapsRuntimeData = new Map();
136
+ this.#pollForLastRequestStatus();
148
137
  /* eslint-disable @typescript-eslint/unbound-method */
149
138
  this.messagingSystem.subscribe('ExecutionService:unhandledError', this._onUnhandledSnapError);
150
139
  this.messagingSystem.subscribe('ExecutionService:outboundRequest', this._onOutboundRequest);
151
140
  this.messagingSystem.subscribe('ExecutionService:outboundResponse', this._onOutboundResponse);
152
141
  /* eslint-enable @typescript-eslint/unbound-method */
153
142
  this.messagingSystem.subscribe('SnapController:snapInstalled', ({ id }, origin) => {
154
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_callLifecycleHook).call(this, origin, id, HandlerType.OnInstall).catch((error) => {
143
+ this.#callLifecycleHook(origin, id, HandlerType.OnInstall).catch((error) => {
155
144
  logError(`Error when calling \`onInstall\` lifecycle hook for snap "${id}": ${getErrorMessage(error)}`);
156
145
  });
157
146
  });
158
147
  this.messagingSystem.subscribe('SnapController:snapUpdated', ({ id }, _oldVersion, origin) => {
159
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_callLifecycleHook).call(this, origin, id, HandlerType.OnUpdate).catch((error) => {
148
+ this.#callLifecycleHook(origin, id, HandlerType.OnUpdate).catch((error) => {
160
149
  logError(`Error when calling \`onUpdate\` lifecycle hook for snap "${id}": ${getErrorMessage(error)}`);
161
150
  });
162
151
  });
163
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_initializeStateMachine).call(this);
164
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_registerMessageHandlers).call(this);
165
- if (__classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f")) {
166
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handlePreinstalledSnaps).call(this, __classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f"));
152
+ this.#initializeStateMachine();
153
+ this.#registerMessageHandlers();
154
+ if (this.#preinstalledSnaps) {
155
+ this.#handlePreinstalledSnaps(this.#preinstalledSnaps);
156
+ }
157
+ Object.values(this.state?.snaps ?? {}).forEach((snap) => this.#setupRuntime(snap.id));
158
+ }
159
+ /**
160
+ * We track status of a Snap using a finite-state-machine.
161
+ * It keeps track of whether the snap is started / stopped / etc.
162
+ *
163
+ * @see {@link SnapController.transition} for interacting with the machine.
164
+ */
165
+ // We initialize the machine in the instance because the status is currently tightly coupled
166
+ // with the SnapController - the guard checks for enabled status inside the SnapController state.
167
+ // In the future, side-effects could be added to the machine during transitions.
168
+ #initializeStateMachine() {
169
+ const disableGuard = ({ snapId }) => {
170
+ return this.getExpect(snapId).enabled;
171
+ };
172
+ const statusConfig = {
173
+ initial: SnapStatus.Installing,
174
+ states: {
175
+ [SnapStatus.Installing]: {
176
+ on: {
177
+ [SnapStatusEvents.Start]: {
178
+ target: SnapStatus.Running,
179
+ cond: disableGuard,
180
+ },
181
+ },
182
+ },
183
+ [SnapStatus.Updating]: {
184
+ on: {
185
+ [SnapStatusEvents.Start]: {
186
+ target: SnapStatus.Running,
187
+ cond: disableGuard,
188
+ },
189
+ [SnapStatusEvents.Stop]: SnapStatus.Stopped,
190
+ },
191
+ },
192
+ [SnapStatus.Running]: {
193
+ on: {
194
+ [SnapStatusEvents.Stop]: SnapStatus.Stopped,
195
+ [SnapStatusEvents.Crash]: SnapStatus.Crashed,
196
+ },
197
+ },
198
+ [SnapStatus.Stopped]: {
199
+ on: {
200
+ [SnapStatusEvents.Start]: {
201
+ target: SnapStatus.Running,
202
+ cond: disableGuard,
203
+ },
204
+ [SnapStatusEvents.Update]: SnapStatus.Updating,
205
+ },
206
+ },
207
+ [SnapStatus.Crashed]: {
208
+ on: {
209
+ [SnapStatusEvents.Start]: {
210
+ target: SnapStatus.Running,
211
+ cond: disableGuard,
212
+ },
213
+ [SnapStatusEvents.Update]: SnapStatus.Updating,
214
+ },
215
+ },
216
+ },
217
+ };
218
+ this.#statusMachine = createMachine(statusConfig);
219
+ validateMachine(this.#statusMachine);
220
+ }
221
+ /**
222
+ * Constructor helper for registering the controller's messaging system
223
+ * actions.
224
+ */
225
+ #registerMessageHandlers() {
226
+ this.messagingSystem.registerActionHandler(`${controllerName}:clearSnapState`, (...args) => this.clearSnapState(...args));
227
+ this.messagingSystem.registerActionHandler(`${controllerName}:get`, (...args) => this.get(...args));
228
+ this.messagingSystem.registerActionHandler(`${controllerName}:getSnapState`, async (...args) => this.getSnapState(...args));
229
+ this.messagingSystem.registerActionHandler(`${controllerName}:handleRequest`, async (...args) => this.handleRequest(...args));
230
+ this.messagingSystem.registerActionHandler(`${controllerName}:has`, (...args) => this.has(...args));
231
+ this.messagingSystem.registerActionHandler(`${controllerName}:updateBlockedSnaps`, async () => this.updateBlockedSnaps());
232
+ this.messagingSystem.registerActionHandler(`${controllerName}:updateSnapState`, async (...args) => this.updateSnapState(...args));
233
+ this.messagingSystem.registerActionHandler(`${controllerName}:enable`, (...args) => this.enableSnap(...args));
234
+ this.messagingSystem.registerActionHandler(`${controllerName}:disable`, async (...args) => this.disableSnap(...args));
235
+ this.messagingSystem.registerActionHandler(`${controllerName}:remove`, async (...args) => this.removeSnap(...args));
236
+ this.messagingSystem.registerActionHandler(`${controllerName}:getPermitted`, (...args) => this.getPermittedSnaps(...args));
237
+ this.messagingSystem.registerActionHandler(`${controllerName}:install`, async (...args) => this.installSnaps(...args));
238
+ this.messagingSystem.registerActionHandler(`${controllerName}:getAll`, (...args) => this.getAllSnaps(...args));
239
+ this.messagingSystem.registerActionHandler(`${controllerName}:incrementActiveReferences`, (...args) => this.incrementActiveReferences(...args));
240
+ this.messagingSystem.registerActionHandler(`${controllerName}:decrementActiveReferences`, (...args) => this.decrementActiveReferences(...args));
241
+ this.messagingSystem.registerActionHandler(`${controllerName}:getRegistryMetadata`, async (...args) => this.getRegistryMetadata(...args));
242
+ this.messagingSystem.registerActionHandler(`${controllerName}:disconnectOrigin`, (...args) => this.removeSnapFromSubject(...args));
243
+ this.messagingSystem.registerActionHandler(`${controllerName}:revokeDynamicPermissions`, (...args) => this.revokeDynamicSnapPermissions(...args));
244
+ this.messagingSystem.registerActionHandler(`${controllerName}:getFile`, async (...args) => this.getSnapFile(...args));
245
+ this.messagingSystem.registerActionHandler(`${controllerName}:stopAllSnaps`, async (...args) => this.stopAllSnaps(...args));
246
+ }
247
+ #handlePreinstalledSnaps(preinstalledSnaps) {
248
+ for (const { snapId, manifest, files, removable, hidden, hideSnapBranding, } of preinstalledSnaps) {
249
+ const existingSnap = this.get(snapId);
250
+ const isAlreadyInstalled = existingSnap !== undefined;
251
+ const isUpdate = isAlreadyInstalled && gtVersion(manifest.version, existingSnap.version);
252
+ // Disallow downgrades and overwriting non preinstalled snaps
253
+ if (isAlreadyInstalled &&
254
+ (!isUpdate || existingSnap.preinstalled !== true)) {
255
+ continue;
256
+ }
257
+ const manifestFile = new VirtualFile({
258
+ path: NpmSnapFileNames.Manifest,
259
+ value: JSON.stringify(manifest),
260
+ result: manifest,
261
+ });
262
+ const virtualFiles = files.map(({ path, value }) => new VirtualFile({ value, path }));
263
+ const { filePath, iconPath } = manifest.source.location.npm;
264
+ const sourceCode = virtualFiles.find((file) => file.path === filePath);
265
+ const svgIcon = iconPath
266
+ ? virtualFiles.find((file) => file.path === iconPath)
267
+ : undefined;
268
+ assert(sourceCode, 'Source code not provided for preinstalled snap.');
269
+ assert(!iconPath || (iconPath && svgIcon), 'Icon not provided for preinstalled snap.');
270
+ assert(manifest.source.files === undefined, 'Auxiliary files are not currently supported for preinstalled snaps.');
271
+ const localizationFiles = manifest.source.locales?.map((path) => virtualFiles.find((file) => file.path === path)) ?? [];
272
+ const validatedLocalizationFiles = getValidatedLocalizationFiles(localizationFiles.filter(Boolean));
273
+ assert(localizationFiles.length === validatedLocalizationFiles.length, 'Missing localization files for preinstalled snap.');
274
+ const filesObject = {
275
+ manifest: manifestFile,
276
+ sourceCode,
277
+ svgIcon,
278
+ auxiliaryFiles: [],
279
+ localizationFiles: validatedLocalizationFiles,
280
+ };
281
+ // Add snap to the SnapController state
282
+ this.#set({
283
+ id: snapId,
284
+ origin: 'metamask',
285
+ files: filesObject,
286
+ removable,
287
+ hidden,
288
+ hideSnapBranding,
289
+ preinstalled: true,
290
+ });
291
+ // Setup permissions
292
+ const processedPermissions = processSnapPermissions(manifest.initialPermissions);
293
+ this.#validateSnapPermissions(processedPermissions);
294
+ const { newPermissions, unusedPermissions } = this.#calculatePermissionsChange(snapId, processedPermissions);
295
+ this.#updatePermissions({ snapId, newPermissions, unusedPermissions });
296
+ if (manifest.initialConnections) {
297
+ this.#handleInitialConnections(snapId, existingSnap?.initialConnections ?? null, manifest.initialConnections);
298
+ }
299
+ // Set status
300
+ this.update((state) => {
301
+ state.snaps[snapId].status = SnapStatus.Stopped;
302
+ });
167
303
  }
168
- Object.values(this.state?.snaps ?? {}).forEach((snap) => __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_setupRuntime).call(this, snap.id));
304
+ }
305
+ #pollForLastRequestStatus() {
306
+ this.#timeoutForLastRequestStatus = setTimeout(() => {
307
+ this.#stopSnapsLastRequestPastMax().catch((error) => {
308
+ // TODO: Decide how to handle errors.
309
+ logError(error);
310
+ });
311
+ this.#pollForLastRequestStatus();
312
+ }, this.#idleTimeCheckInterval);
169
313
  }
170
314
  /**
171
315
  * Checks all installed snaps against the block list and
@@ -173,7 +317,7 @@ export class SnapController extends BaseController {
173
317
  * for more information.
174
318
  */
175
319
  async updateBlockedSnaps() {
176
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
320
+ this.#assertCanUsePlatform();
177
321
  await this.messagingSystem.call('SnapsRegistry:update');
178
322
  const blockedSnaps = await this.messagingSystem.call('SnapsRegistry:get', Object.values(this.state.snaps).reduce((blockListArg, snap) => {
179
323
  blockListArg[snap.id] = {
@@ -184,11 +328,91 @@ export class SnapController extends BaseController {
184
328
  }, {}));
185
329
  await Promise.all(Object.entries(blockedSnaps).map(async ([snapId, { status, reason }]) => {
186
330
  if (status === SnapsRegistryStatus.Blocked) {
187
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_blockSnap).call(this, snapId, reason);
331
+ return this.#blockSnap(snapId, reason);
188
332
  }
189
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_unblockSnap).call(this, snapId);
333
+ return this.#unblockSnap(snapId);
190
334
  }));
191
335
  }
336
+ /**
337
+ * Blocks an installed snap and prevents it from being started again. Emits
338
+ * {@link SnapBlocked}. Does nothing if the snap is not installed.
339
+ *
340
+ * @param snapId - The snap to block.
341
+ * @param blockedSnapInfo - Information detailing why the snap is blocked.
342
+ */
343
+ async #blockSnap(snapId, blockedSnapInfo) {
344
+ if (!this.has(snapId)) {
345
+ return;
346
+ }
347
+ try {
348
+ this.update((state) => {
349
+ state.snaps[snapId].blocked = true;
350
+ state.snaps[snapId].blockInformation = blockedSnapInfo;
351
+ });
352
+ await this.disableSnap(snapId);
353
+ }
354
+ catch (error) {
355
+ logError(`Encountered error when stopping blocked snap "${snapId}".`, error);
356
+ }
357
+ this.messagingSystem.publish(`${controllerName}:snapBlocked`, snapId, blockedSnapInfo);
358
+ }
359
+ /**
360
+ * Unblocks a snap so that it can be enabled and started again. Emits
361
+ * {@link SnapUnblocked}. Does nothing if the snap is not installed or already
362
+ * unblocked.
363
+ *
364
+ * @param snapId - The id of the snap to unblock.
365
+ */
366
+ #unblockSnap(snapId) {
367
+ if (!this.has(snapId) || !this.state.snaps[snapId].blocked) {
368
+ return;
369
+ }
370
+ this.update((state) => {
371
+ state.snaps[snapId].blocked = false;
372
+ delete state.snaps[snapId].blockInformation;
373
+ });
374
+ this.messagingSystem.publish(`${controllerName}:snapUnblocked`, snapId);
375
+ }
376
+ async #assertIsInstallAllowed(snapId, snapInfo) {
377
+ const results = await this.messagingSystem.call('SnapsRegistry:get', {
378
+ [snapId]: snapInfo,
379
+ });
380
+ const result = results[snapId];
381
+ if (result.status === SnapsRegistryStatus.Blocked) {
382
+ throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": The version is blocked. ${result.reason?.explanation ?? ''}`);
383
+ }
384
+ const isAllowlistingRequired = Object.keys(snapInfo.permissions).some((permission) => !ALLOWED_PERMISSIONS.includes(permission));
385
+ if (this.#featureFlags.requireAllowlist &&
386
+ isAllowlistingRequired &&
387
+ result.status !== SnapsRegistryStatus.Verified) {
388
+ throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": ${result.status === SnapsRegistryStatus.Unavailable
389
+ ? 'The registry is temporarily unavailable.'
390
+ : 'The snap is not on the allowlist.'}`);
391
+ }
392
+ }
393
+ /**
394
+ * Asserts whether new Snaps are allowed to be installed.
395
+ */
396
+ #assertCanInstallSnaps() {
397
+ assert(this.#featureFlags.disableSnapInstallation !== true, 'Installing Snaps is currently disabled in this version of MetaMask.');
398
+ }
399
+ /**
400
+ * Asserts whether the Snaps platform is allowed to run.
401
+ */
402
+ #assertCanUsePlatform() {
403
+ const flags = this.#getFeatureFlags();
404
+ assert(flags.disableSnaps !== true, 'The Snaps platform requires basic functionality to be used. Enable basic functionality in the settings to use the Snaps platform.');
405
+ }
406
+ async #stopSnapsLastRequestPastMax() {
407
+ const entries = [...this.#snapsRuntimeData.entries()];
408
+ return Promise.all(entries
409
+ .filter(([_snapId, runtime]) => runtime.activeReferences === 0 &&
410
+ runtime.pendingInboundRequests.length === 0 &&
411
+ runtime.lastRequest &&
412
+ this.#maxIdleTime &&
413
+ timeSince(runtime.lastRequest) > this.#maxIdleTime)
414
+ .map(async ([snapId]) => this.stopSnap(snapId, SnapStatusEvents.Stop)));
415
+ }
192
416
  _onUnhandledSnapError(snapId, _error) {
193
417
  this.stopSnap(snapId, SnapStatusEvents.Crash).catch((stopSnapError) => {
194
418
  // TODO: Decide how to handle errors.
@@ -196,7 +420,7 @@ export class SnapController extends BaseController {
196
420
  });
197
421
  }
198
422
  _onOutboundRequest(snapId) {
199
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
423
+ const runtime = this.#getRuntimeExpect(snapId);
200
424
  // Ideally we would only pause the pending request that is making the outbound request
201
425
  // but right now we don't have a way to know which request initiated the outbound request
202
426
  runtime.pendingInboundRequests
@@ -205,7 +429,7 @@ export class SnapController extends BaseController {
205
429
  runtime.pendingOutboundRequests += 1;
206
430
  }
207
431
  _onOutboundResponse(snapId) {
208
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
432
+ const runtime = this.#getRuntimeExpect(snapId);
209
433
  runtime.pendingOutboundRequests -= 1;
210
434
  if (runtime.pendingOutboundRequests === 0) {
211
435
  runtime.pendingInboundRequests
@@ -213,6 +437,25 @@ export class SnapController extends BaseController {
213
437
  .forEach((pendingRequest) => pendingRequest.timer.resume());
214
438
  }
215
439
  }
440
+ /**
441
+ * Transitions between states using `snapStatusStateMachineConfig` as the template to figure out
442
+ * the next state. This transition function uses a very minimal subset of XState conventions:
443
+ * - supports initial state
444
+ * - .on supports raw event target string
445
+ * - .on supports {target, cond} object
446
+ * - the arguments for `cond` is the `SerializedSnap` instead of Xstate convention of `(event,
447
+ * context) => boolean`
448
+ *
449
+ * @param snapId - The id of the snap to transition.
450
+ * @param event - The event enum to use to transition.
451
+ */
452
+ #transition(snapId, event) {
453
+ const { interpreter } = this.#getRuntimeExpect(snapId);
454
+ interpreter.send(event);
455
+ this.update((state) => {
456
+ state.snaps[snapId].status = interpreter.state.value;
457
+ });
458
+ }
216
459
  /**
217
460
  * Starts the given snap. Throws an error if no such snap exists
218
461
  * or if it is already running.
@@ -220,12 +463,12 @@ export class SnapController extends BaseController {
220
463
  * @param snapId - The id of the Snap to start.
221
464
  */
222
465
  async startSnap(snapId) {
223
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
466
+ this.#assertCanUsePlatform();
224
467
  const snap = this.state.snaps[snapId];
225
468
  if (snap.enabled === false) {
226
469
  throw new Error(`Snap "${snapId}" is disabled.`);
227
470
  }
228
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_startSnap).call(this, {
471
+ await this.#startSnap({
229
472
  snapId,
230
473
  sourceCode: snap.sourceCode,
231
474
  });
@@ -273,7 +516,7 @@ export class SnapController extends BaseController {
273
516
  * stopped.
274
517
  */
275
518
  async stopSnap(snapId, statusEvent = SnapStatusEvents.Stop) {
276
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntime).call(this, snapId);
519
+ const runtime = this.#getRuntime(snapId);
277
520
  if (!runtime) {
278
521
  throw new Error(`The snap "${snapId}" is not running.`);
279
522
  }
@@ -286,8 +529,8 @@ export class SnapController extends BaseController {
286
529
  runtime.stopping = true;
287
530
  try {
288
531
  if (this.isRunning(snapId)) {
289
- __classPrivateFieldGet(this, _SnapController_closeAllConnections, "f")?.call(this, snapId);
290
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_terminateSnap).call(this, snapId);
532
+ this.#closeAllConnections?.(snapId);
533
+ await this.#terminateSnap(snapId);
291
534
  }
292
535
  }
293
536
  finally {
@@ -297,7 +540,7 @@ export class SnapController extends BaseController {
297
540
  runtime.pendingOutboundRequests = 0;
298
541
  runtime.stopping = false;
299
542
  if (this.isRunning(snapId)) {
300
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, statusEvent);
543
+ this.#transition(snapId, statusEvent);
301
544
  }
302
545
  }
303
546
  }
@@ -313,6 +556,24 @@ export class SnapController extends BaseController {
313
556
  const promises = snaps.map(async (snap) => this.stopSnap(snap.id, statusEvent));
314
557
  await Promise.allSettled(promises);
315
558
  }
559
+ /**
560
+ * Terminates the specified snap and emits the `snapTerminated` event.
561
+ *
562
+ * @param snapId - The snap to terminate.
563
+ */
564
+ async #terminateSnap(snapId) {
565
+ await this.messagingSystem.call('ExecutionService:terminateSnap', snapId);
566
+ // Hack to give up execution for a bit to let gracefully terminating Snaps return.
567
+ await new Promise((resolve) => setTimeout(resolve, 1));
568
+ const runtime = this.#getRuntimeExpect(snapId);
569
+ // Unresponsive requests may still be timed, time them out.
570
+ runtime.pendingInboundRequests
571
+ .filter((pendingRequest) => pendingRequest.timer.status !== 'finished')
572
+ .forEach((pendingRequest) => pendingRequest.timer.finish());
573
+ // Hack to give up execution for a bit to let timed out requests return.
574
+ await new Promise((resolve) => setTimeout(resolve, 1));
575
+ this.messagingSystem.publish('SnapController:snapTerminated', this.getTruncatedExpect(snapId));
576
+ }
316
577
  /**
317
578
  * Returns whether the given snap is running.
318
579
  * Throws an error if the snap doesn't exist.
@@ -380,6 +641,86 @@ export class SnapController extends BaseController {
380
641
  getTruncatedExpect(snapId) {
381
642
  return truncateSnap(this.getExpect(snapId));
382
643
  }
644
+ /**
645
+ * Generate an encryption key to be used for state encryption for a given Snap.
646
+ *
647
+ * @param options - An options bag.
648
+ * @param options.snapId - The Snap ID.
649
+ * @param options.salt - A salt to be used for the encryption key.
650
+ * @param options.useCache - Whether to use caching or not.
651
+ * @param options.keyMetadata - Optional metadata about how to derive the encryption key.
652
+ * @returns An encryption key.
653
+ */
654
+ async #getSnapEncryptionKey({ snapId, salt: passedSalt, useCache, keyMetadata, }) {
655
+ const runtime = this.#getRuntimeExpect(snapId);
656
+ if (runtime.encryptionKey && runtime.encryptionSalt && useCache) {
657
+ return {
658
+ key: await this.#encryptor.importKey(runtime.encryptionKey),
659
+ salt: runtime.encryptionSalt,
660
+ };
661
+ }
662
+ const salt = passedSalt ?? this.#encryptor.generateSalt();
663
+ const mnemonicPhrase = await this.#getMnemonic();
664
+ const entropy = await getEncryptionEntropy({ snapId, mnemonicPhrase });
665
+ const encryptionKey = await this.#encryptor.keyFromPassword(entropy, salt, true, keyMetadata);
666
+ const exportedKey = await this.#encryptor.exportKey(encryptionKey);
667
+ // Cache exported encryption key in runtime
668
+ if (useCache) {
669
+ runtime.encryptionKey = exportedKey;
670
+ runtime.encryptionSalt = salt;
671
+ }
672
+ return { key: encryptionKey, salt };
673
+ }
674
+ /**
675
+ * Decrypt the encrypted state for a given Snap.
676
+ *
677
+ * @param snapId - The Snap ID.
678
+ * @param state - The encrypted state as a string.
679
+ * @returns A valid JSON object derived from the encrypted state.
680
+ * @throws If the decryption fails or the decrypted state is not valid JSON.
681
+ */
682
+ async #decryptSnapState(snapId, state) {
683
+ try {
684
+ const parsed = parseJson(state);
685
+ const { salt, keyMetadata } = parsed;
686
+ const useCache = this.#encryptor.isVaultUpdated(state);
687
+ const { key } = await this.#getSnapEncryptionKey({
688
+ snapId,
689
+ salt,
690
+ useCache,
691
+ // When decrypting state we expect key metadata to be present.
692
+ // If it isn't present, we assume that the Snap state we are decrypting is old enough to use the legacy encryption params.
693
+ keyMetadata: keyMetadata ?? LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS,
694
+ });
695
+ const decryptedState = await this.#encryptor.decryptWithKey(key, parsed);
696
+ assert(isValidJson(decryptedState));
697
+ return decryptedState;
698
+ }
699
+ catch {
700
+ throw rpcErrors.internal({
701
+ message: 'Failed to decrypt snap state, the state must be corrupted.',
702
+ });
703
+ }
704
+ }
705
+ /**
706
+ * Encrypt a JSON state object for a given Snap.
707
+ *
708
+ * Note: This function does not assert the validity of the object,
709
+ * please ensure only valid JSON is passed to it.
710
+ *
711
+ * @param snapId - The Snap ID.
712
+ * @param state - The state object.
713
+ * @returns A string containing the encrypted JSON object.
714
+ */
715
+ async #encryptSnapState(snapId, state) {
716
+ const { key, salt } = await this.#getSnapEncryptionKey({
717
+ snapId,
718
+ useCache: true,
719
+ });
720
+ const encryptedState = await this.#encryptor.encryptWithKey(key, state);
721
+ encryptedState.salt = salt;
722
+ return JSON.stringify(encryptedState);
723
+ }
383
724
  /**
384
725
  * Updates the own state of the snap with the given id.
385
726
  * This is distinct from the state MetaMask uses to manage snaps.
@@ -390,7 +731,7 @@ export class SnapController extends BaseController {
390
731
  */
391
732
  async updateSnapState(snapId, newSnapState, encrypted) {
392
733
  if (encrypted) {
393
- const encryptedState = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_encryptSnapState).call(this, snapId, newSnapState);
734
+ const encryptedState = await this.#encryptSnapState(snapId, newSnapState);
394
735
  this.update((state) => {
395
736
  state.snapStates[snapId] = encryptedState;
396
737
  });
@@ -436,7 +777,7 @@ export class SnapController extends BaseController {
436
777
  if (!encrypted) {
437
778
  return parseJson(state);
438
779
  }
439
- const decrypted = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_decryptSnapState).call(this, snapId, state);
780
+ const decrypted = await this.#decryptSnapState(snapId, state);
440
781
  return decrypted;
441
782
  }
442
783
  /**
@@ -464,22 +805,22 @@ export class SnapController extends BaseController {
464
805
  */
465
806
  async clearState() {
466
807
  const snapIds = Object.keys(this.state.snaps);
467
- if (__classPrivateFieldGet(this, _SnapController_closeAllConnections, "f")) {
808
+ if (this.#closeAllConnections) {
468
809
  snapIds.forEach((snapId) => {
469
- __classPrivateFieldGet(this, _SnapController_closeAllConnections, "f")?.call(this, snapId);
810
+ this.#closeAllConnections?.(snapId);
470
811
  });
471
812
  }
472
813
  await this.messagingSystem.call('ExecutionService:terminateAllSnaps');
473
- snapIds.forEach((snapId) => __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_revokeAllSnapPermissions).call(this, snapId));
814
+ snapIds.forEach((snapId) => this.#revokeAllSnapPermissions(snapId));
474
815
  this.update((state) => {
475
816
  state.snaps = {};
476
817
  state.snapStates = {};
477
818
  });
478
- __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").clear();
819
+ this.#snapsRuntimeData.clear();
479
820
  // We want to remove all snaps & permissions, except for preinstalled snaps
480
- if (__classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f")) {
481
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handlePreinstalledSnaps).call(this, __classPrivateFieldGet(this, _SnapController_preinstalledSnaps, "f"));
482
- Object.values(this.state?.snaps).forEach((snap) => __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_setupRuntime).call(this, snap.id));
821
+ if (this.#preinstalledSnaps) {
822
+ this.#handlePreinstalledSnaps(this.#preinstalledSnaps);
823
+ Object.values(this.state?.snaps).forEach((snap) => this.#setupRuntime(snap.id));
483
824
  }
484
825
  }
485
826
  /**
@@ -513,9 +854,9 @@ export class SnapController extends BaseController {
513
854
  // it. This ensures that the snap will not be restarted or otherwise
514
855
  // affect the host environment while we are deleting it.
515
856
  await this.disableSnap(snapId);
516
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_revokeAllSnapPermissions).call(this, snapId);
517
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_removeSnapFromSubjects).call(this, snapId);
518
- __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").delete(snapId);
857
+ this.#revokeAllSnapPermissions(snapId);
858
+ this.#removeSnapFromSubjects(snapId);
859
+ this.#snapsRuntimeData.delete(snapId);
519
860
  this.update((state) => {
520
861
  delete state.snaps[snapId];
521
862
  delete state.snapStates[snapId];
@@ -527,6 +868,47 @@ export class SnapController extends BaseController {
527
868
  }
528
869
  }));
529
870
  }
871
+ #handleInitialConnections(snapId, previousInitialConnections, initialConnections) {
872
+ if (previousInitialConnections) {
873
+ const revokedInitialConnections = setDiff(previousInitialConnections, initialConnections);
874
+ for (const origin of Object.keys(revokedInitialConnections)) {
875
+ this.removeSnapFromSubject(origin, snapId);
876
+ }
877
+ }
878
+ for (const origin of Object.keys(initialConnections)) {
879
+ this.#addSnapToSubject(origin, snapId);
880
+ }
881
+ }
882
+ #addSnapToSubject(origin, snapId) {
883
+ const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
884
+ const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
885
+ const subjectHasSnap = Boolean(existingCaveat?.value?.[snapId]);
886
+ // If the subject is already connected to the snap, this is a no-op.
887
+ if (subjectHasSnap) {
888
+ return;
889
+ }
890
+ // If an existing caveat exists, we add the snap to that.
891
+ if (existingCaveat) {
892
+ this.messagingSystem.call('PermissionController:updateCaveat', origin, WALLET_SNAP_PERMISSION_KEY, SnapCaveatType.SnapIds, { ...existingCaveat.value, [snapId]: {} });
893
+ return;
894
+ }
895
+ const approvedPermissions = {
896
+ [WALLET_SNAP_PERMISSION_KEY]: {
897
+ caveats: [
898
+ {
899
+ type: SnapCaveatType.SnapIds,
900
+ value: {
901
+ [snapId]: {},
902
+ },
903
+ },
904
+ ],
905
+ },
906
+ };
907
+ this.messagingSystem.call('PermissionController:grantPermissions', {
908
+ approvedPermissions,
909
+ subject: { origin },
910
+ });
911
+ }
530
912
  /**
531
913
  * Removes a snap's permission (caveat) from the specified subject.
532
914
  *
@@ -563,18 +945,39 @@ export class SnapController extends BaseController {
563
945
  * @throws If non-dynamic permissions are passed.
564
946
  */
565
947
  revokeDynamicSnapPermissions(snapId, permissionNames) {
566
- assert(permissionNames.every((permissionName) => __classPrivateFieldGet(this, _SnapController_dynamicPermissions, "f").includes(permissionName)), 'Non-dynamic permissions cannot be revoked');
948
+ assert(permissionNames.every((permissionName) => this.#dynamicPermissions.includes(permissionName)), 'Non-dynamic permissions cannot be revoked');
567
949
  this.messagingSystem.call('PermissionController:revokePermissions', {
568
950
  [snapId]: permissionNames,
569
951
  });
570
952
  }
953
+ /**
954
+ * Removes a snap's permission (caveat) from all subjects.
955
+ *
956
+ * @param snapId - The id of the Snap.
957
+ */
958
+ #removeSnapFromSubjects(snapId) {
959
+ const subjects = this.messagingSystem.call('PermissionController:getSubjectNames');
960
+ for (const subject of subjects) {
961
+ this.removeSnapFromSubject(subject, snapId);
962
+ }
963
+ }
964
+ /**
965
+ * Safely revokes all permissions granted to a Snap.
966
+ *
967
+ * @param snapId - The snap ID.
968
+ */
969
+ #revokeAllSnapPermissions(snapId) {
970
+ if (this.messagingSystem.call('PermissionController:hasPermissions', snapId)) {
971
+ this.messagingSystem.call('PermissionController:revokeAllPermissions', snapId);
972
+ }
973
+ }
571
974
  /**
572
975
  * Handles incrementing the activeReferences counter.
573
976
  *
574
977
  * @param snapId - The snap id of the snap that was referenced.
575
978
  */
576
979
  incrementActiveReferences(snapId) {
577
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
980
+ const runtime = this.#getRuntimeExpect(snapId);
578
981
  runtime.activeReferences += 1;
579
982
  }
580
983
  /**
@@ -583,7 +986,7 @@ export class SnapController extends BaseController {
583
986
  * @param snapId - The snap id of the snap that was referenced..
584
987
  */
585
988
  decrementActiveReferences(snapId) {
586
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
989
+ const runtime = this.#getRuntimeExpect(snapId);
587
990
  assert(runtime.activeReferences > 0, 'SnapController reference management is in an invalid state.');
588
991
  runtime.activeReferences -= 1;
589
992
  }
@@ -624,7 +1027,7 @@ export class SnapController extends BaseController {
624
1027
  * snap couldn't be installed.
625
1028
  */
626
1029
  async installSnaps(origin, requestedSnaps) {
627
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
1030
+ this.#assertCanUsePlatform();
628
1031
  const result = {};
629
1032
  const snapIds = Object.keys(requestedSnaps);
630
1033
  const pendingUpdates = [];
@@ -636,23 +1039,23 @@ export class SnapController extends BaseController {
636
1039
  if (error) {
637
1040
  throw rpcErrors.invalidParams(`The "version" field must be a valid SemVer version range if specified. Received: "${rawVersion}".`);
638
1041
  }
639
- const location = __classPrivateFieldGet(this, _SnapController_detectSnapLocation, "f").call(this, snapId, {
1042
+ const location = this.#detectSnapLocation(snapId, {
640
1043
  versionRange: version,
641
- fetch: __classPrivateFieldGet(this, _SnapController_fetchFunction, "f"),
642
- allowLocal: __classPrivateFieldGet(this, _SnapController_featureFlags, "f").allowLocalSnaps,
643
- resolveVersion: async (range) => __classPrivateFieldGet(this, _SnapController_featureFlags, "f").requireAllowlist
644
- ? await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_resolveAllowlistVersion).call(this, snapId, range)
1044
+ fetch: this.#fetchFunction,
1045
+ allowLocal: this.#featureFlags.allowLocalSnaps,
1046
+ resolveVersion: async (range) => this.#featureFlags.requireAllowlist
1047
+ ? await this.#resolveAllowlistVersion(snapId, range)
645
1048
  : range,
646
1049
  });
647
1050
  // Existing snaps may need to be updated, unless they should be re-installed (e.g. local snaps)
648
1051
  // Everything else is treated as an install
649
1052
  const isUpdate = this.has(snapId) && !location.shouldAlwaysReload;
650
- if (isUpdate && __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_isValidUpdate).call(this, snapId, version)) {
1053
+ if (isUpdate && this.#isValidUpdate(snapId, version)) {
651
1054
  const existingSnap = this.getExpect(snapId);
652
1055
  pendingUpdates.push({ snapId, oldVersion: existingSnap.version });
653
- let rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1056
+ let rollbackSnapshot = this.#getRollbackSnapshot(snapId);
654
1057
  if (rollbackSnapshot === undefined) {
655
- rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createRollbackSnapshot).call(this, snapId);
1058
+ rollbackSnapshot = this.#createRollbackSnapshot(snapId);
656
1059
  rollbackSnapshot.newVersion = version;
657
1060
  }
658
1061
  else {
@@ -667,16 +1070,16 @@ export class SnapController extends BaseController {
667
1070
  // Once we finish all installs / updates, emit events.
668
1071
  pendingInstalls.forEach((snapId) => this.messagingSystem.publish(`SnapController:snapInstalled`, this.getTruncatedExpect(snapId), origin));
669
1072
  pendingUpdates.forEach(({ snapId, oldVersion }) => this.messagingSystem.publish(`SnapController:snapUpdated`, this.getTruncatedExpect(snapId), oldVersion, origin));
670
- snapIds.forEach((snapId) => __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").delete(snapId));
1073
+ snapIds.forEach((snapId) => this.#rollbackSnapshots.delete(snapId));
671
1074
  }
672
1075
  catch (error) {
673
1076
  const installed = pendingInstalls.filter((snapId) => this.has(snapId));
674
1077
  await this.removeSnaps(installed);
675
- const snapshottedSnaps = [...__classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").keys()];
1078
+ const snapshottedSnaps = [...this.#rollbackSnapshots.keys()];
676
1079
  const snapsToRollback = pendingUpdates
677
1080
  .map(({ snapId }) => snapId)
678
1081
  .filter((snapId) => snapshottedSnaps.includes(snapId));
679
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_rollbackSnaps).call(this, snapsToRollback);
1082
+ await this.#rollbackSnaps(snapsToRollback);
680
1083
  throw error;
681
1084
  }
682
1085
  return result;
@@ -706,8 +1109,8 @@ export class SnapController extends BaseController {
706
1109
  // and we don't want to emit events prematurely.
707
1110
  false);
708
1111
  }
709
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanInstallSnaps).call(this);
710
- let pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1112
+ this.#assertCanInstallSnaps();
1113
+ let pendingApproval = this.#createApproval({
711
1114
  origin,
712
1115
  snapId,
713
1116
  type: SNAP_APPROVAL_INSTALL,
@@ -719,27 +1122,27 @@ export class SnapController extends BaseController {
719
1122
  }
720
1123
  // Existing snaps that should be re-installed should not maintain their existing permissions
721
1124
  if (existingSnap && location.shouldAlwaysReload) {
722
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_revokeAllSnapPermissions).call(this, snapId);
1125
+ this.#revokeAllSnapPermissions(snapId);
723
1126
  }
724
1127
  try {
725
- const { sourceCode } = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_add).call(this, {
1128
+ const { sourceCode } = await this.#add({
726
1129
  origin,
727
1130
  id: snapId,
728
1131
  location,
729
1132
  versionRange,
730
1133
  });
731
1134
  await this.authorize(snapId, pendingApproval);
732
- pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1135
+ pendingApproval = this.#createApproval({
733
1136
  origin,
734
1137
  snapId,
735
1138
  type: SNAP_APPROVAL_RESULT,
736
1139
  });
737
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_startSnap).call(this, {
1140
+ await this.#startSnap({
738
1141
  snapId,
739
1142
  sourceCode,
740
1143
  });
741
1144
  const truncated = this.getTruncatedExpect(snapId);
742
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1145
+ this.#updateApproval(pendingApproval.id, {
743
1146
  loading: false,
744
1147
  type: SNAP_APPROVAL_INSTALL,
745
1148
  });
@@ -748,7 +1151,7 @@ export class SnapController extends BaseController {
748
1151
  catch (error) {
749
1152
  logError(`Error when adding ${snapId}.`, error);
750
1153
  const errorString = error instanceof Error ? error.message : error.toString();
751
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1154
+ this.#updateApproval(pendingApproval.id, {
752
1155
  loading: false,
753
1156
  type: SNAP_APPROVAL_INSTALL,
754
1157
  error: errorString,
@@ -757,6 +1160,34 @@ export class SnapController extends BaseController {
757
1160
  throw error;
758
1161
  }
759
1162
  }
1163
+ #createApproval({ origin, snapId, type, }) {
1164
+ const id = nanoid();
1165
+ const promise = this.messagingSystem.call('ApprovalController:addRequest', {
1166
+ origin,
1167
+ id,
1168
+ type,
1169
+ requestData: {
1170
+ // Mirror previous installation metadata
1171
+ metadata: { id, origin: snapId, dappOrigin: origin },
1172
+ snapId,
1173
+ },
1174
+ requestState: {
1175
+ loading: true,
1176
+ },
1177
+ }, true);
1178
+ return { id, promise };
1179
+ }
1180
+ #updateApproval(id, requestState) {
1181
+ try {
1182
+ this.messagingSystem.call('ApprovalController:updateRequestState', {
1183
+ id,
1184
+ requestState,
1185
+ });
1186
+ }
1187
+ catch {
1188
+ // Do nothing
1189
+ }
1190
+ }
760
1191
  /**
761
1192
  * Updates an installed snap. The flow is similar to
762
1193
  * {@link SnapController.installSnaps}. The user will be asked if they want
@@ -777,12 +1208,12 @@ export class SnapController extends BaseController {
777
1208
  * @returns The snap metadata if updated, `null` otherwise.
778
1209
  */
779
1210
  async updateSnap(origin, snapId, location, newVersionRange = DEFAULT_REQUESTED_SNAP_VERSION, emitEvent = true) {
780
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanInstallSnaps).call(this);
781
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
1211
+ this.#assertCanInstallSnaps();
1212
+ this.#assertCanUsePlatform();
782
1213
  if (!isValidSemVerRange(newVersionRange)) {
783
1214
  throw new Error(`Received invalid snap version range: "${newVersionRange}".`);
784
1215
  }
785
- let pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1216
+ let pendingApproval = this.#createApproval({
786
1217
  origin,
787
1218
  snapId,
788
1219
  type: SNAP_APPROVAL_UPDATE,
@@ -801,16 +1232,16 @@ export class SnapController extends BaseController {
801
1232
  if (!satisfiesVersionRange(newVersion, newVersionRange)) {
802
1233
  throw new Error(`Version mismatch. Manifest for "${snapId}" specifies version "${newVersion}" which doesn't satisfy requested version range "${newVersionRange}".`);
803
1234
  }
804
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertIsInstallAllowed).call(this, snapId, {
1235
+ await this.#assertIsInstallAllowed(snapId, {
805
1236
  version: newVersion,
806
1237
  checksum: manifest.source.shasum,
807
1238
  permissions: manifest.initialPermissions,
808
1239
  });
809
1240
  const processedPermissions = processSnapPermissions(manifest.initialPermissions);
810
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_validateSnapPermissions).call(this, processedPermissions);
811
- const { newPermissions, unusedPermissions, approvedPermissions } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_calculatePermissionsChange).call(this, snapId, processedPermissions);
812
- const { newConnections, unusedConnections, approvedConnections } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_calculateConnectionsChange).call(this, snapId, oldManifest.initialConnections ?? {}, manifest.initialConnections ?? {});
813
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1241
+ this.#validateSnapPermissions(processedPermissions);
1242
+ const { newPermissions, unusedPermissions, approvedPermissions } = this.#calculatePermissionsChange(snapId, processedPermissions);
1243
+ const { newConnections, unusedConnections, approvedConnections } = this.#calculateConnectionsChange(snapId, oldManifest.initialConnections ?? {}, manifest.initialConnections ?? {});
1244
+ this.#updateApproval(pendingApproval.id, {
814
1245
  permissions: newPermissions,
815
1246
  newVersion: manifest.version,
816
1247
  newPermissions,
@@ -822,7 +1253,7 @@ export class SnapController extends BaseController {
822
1253
  loading: false,
823
1254
  });
824
1255
  const { permissions: approvedNewPermissions, ...requestData } = (await pendingApproval.promise);
825
- pendingApproval = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createApproval).call(this, {
1256
+ pendingApproval = this.#createApproval({
826
1257
  origin,
827
1258
  snapId,
828
1259
  type: SNAP_APPROVAL_RESULT,
@@ -830,23 +1261,23 @@ export class SnapController extends BaseController {
830
1261
  if (this.isRunning(snapId)) {
831
1262
  await this.stopSnap(snapId, SnapStatusEvents.Stop);
832
1263
  }
833
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, SnapStatusEvents.Update);
834
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_set).call(this, {
1264
+ this.#transition(snapId, SnapStatusEvents.Update);
1265
+ this.#set({
835
1266
  origin,
836
1267
  id: snapId,
837
1268
  files: newSnap,
838
1269
  isUpdate: true,
839
1270
  });
840
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, {
1271
+ this.#updatePermissions({
841
1272
  snapId,
842
1273
  unusedPermissions,
843
1274
  newPermissions: approvedNewPermissions,
844
1275
  requestData,
845
1276
  });
846
1277
  if (manifest.initialConnections) {
847
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handleInitialConnections).call(this, snapId, oldManifest.initialConnections ?? null, manifest.initialConnections);
1278
+ this.#handleInitialConnections(snapId, oldManifest.initialConnections ?? null, manifest.initialConnections);
848
1279
  }
849
- const rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1280
+ const rollbackSnapshot = this.#getRollbackSnapshot(snapId);
850
1281
  if (rollbackSnapshot !== undefined) {
851
1282
  rollbackSnapshot.permissions.revoked = unusedPermissions;
852
1283
  rollbackSnapshot.permissions.granted = approvedNewPermissions;
@@ -855,7 +1286,7 @@ export class SnapController extends BaseController {
855
1286
  const sourceCode = sourceCodeFile.toString();
856
1287
  assert(typeof sourceCode === 'string' && sourceCode.length > 0, `Invalid source code for snap "${snapId}".`);
857
1288
  try {
858
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_startSnap).call(this, { snapId, sourceCode });
1289
+ await this.#startSnap({ snapId, sourceCode });
859
1290
  }
860
1291
  catch {
861
1292
  throw new Error(`Snap ${snapId} crashed with updated source code.`);
@@ -864,7 +1295,7 @@ export class SnapController extends BaseController {
864
1295
  if (emitEvent) {
865
1296
  this.messagingSystem.publish('SnapController:snapUpdated', truncatedSnap, snap.version, origin);
866
1297
  }
867
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1298
+ this.#updateApproval(pendingApproval.id, {
868
1299
  loading: false,
869
1300
  type: SNAP_APPROVAL_UPDATE,
870
1301
  });
@@ -873,7 +1304,7 @@ export class SnapController extends BaseController {
873
1304
  catch (error) {
874
1305
  logError(`Error when updating ${snapId},`, error);
875
1306
  const errorString = error instanceof Error ? error.message : error.toString();
876
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1307
+ this.#updateApproval(pendingApproval.id, {
877
1308
  loading: false,
878
1309
  error: errorString,
879
1310
  type: SNAP_APPROVAL_UPDATE,
@@ -882,6 +1313,9 @@ export class SnapController extends BaseController {
882
1313
  throw error;
883
1314
  }
884
1315
  }
1316
+ async #resolveAllowlistVersion(snapId, versionRange) {
1317
+ return await this.messagingSystem.call('SnapsRegistry:resolveVersion', snapId, versionRange);
1318
+ }
885
1319
  /**
886
1320
  * Get metadata for the given snap ID.
887
1321
  *
@@ -890,9 +1324,215 @@ export class SnapController extends BaseController {
890
1324
  * verified.
891
1325
  */
892
1326
  async getRegistryMetadata(snapId) {
893
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
1327
+ this.#assertCanUsePlatform();
894
1328
  return await this.messagingSystem.call('SnapsRegistry:getMetadata', snapId);
895
1329
  }
1330
+ /**
1331
+ * Returns a promise representing the complete installation of the requested snap.
1332
+ * If the snap is already being installed, the previously pending promise will be returned.
1333
+ *
1334
+ * @param args - Object containing the snap id and either the URL of the snap's manifest,
1335
+ * or the snap's manifest and source code. The object may also optionally contain a target
1336
+ * version.
1337
+ * @returns The resulting snap object.
1338
+ */
1339
+ async #add(args) {
1340
+ const { id: snapId, location, versionRange } = args;
1341
+ this.#setupRuntime(snapId);
1342
+ const runtime = this.#getRuntimeExpect(snapId);
1343
+ if (!runtime.installPromise) {
1344
+ log(`Adding snap: ${snapId}`);
1345
+ // If fetching and setting the snap succeeds, this property will be set
1346
+ // to null in the authorize() method.
1347
+ runtime.installPromise = (async () => {
1348
+ const fetchedSnap = await fetchSnap(snapId, location);
1349
+ const manifest = fetchedSnap.manifest.result;
1350
+ if (!satisfiesVersionRange(manifest.version, versionRange)) {
1351
+ throw new Error(`Version mismatch. Manifest for "${snapId}" specifies version "${manifest.version}" which doesn't satisfy requested version range "${versionRange}".`);
1352
+ }
1353
+ await this.#assertIsInstallAllowed(snapId, {
1354
+ version: manifest.version,
1355
+ checksum: manifest.source.shasum,
1356
+ permissions: manifest.initialPermissions,
1357
+ });
1358
+ return this.#set({
1359
+ ...args,
1360
+ files: fetchedSnap,
1361
+ id: snapId,
1362
+ });
1363
+ })();
1364
+ }
1365
+ try {
1366
+ return await runtime.installPromise;
1367
+ }
1368
+ catch (error) {
1369
+ // Reset promise so users can retry installation in case the problem is
1370
+ // temporary.
1371
+ runtime.installPromise = null;
1372
+ throw error;
1373
+ }
1374
+ }
1375
+ async #startSnap(snapData) {
1376
+ const { snapId } = snapData;
1377
+ if (this.isRunning(snapId)) {
1378
+ throw new Error(`Snap "${snapId}" is already started.`);
1379
+ }
1380
+ try {
1381
+ const runtime = this.#getRuntimeExpect(snapId);
1382
+ const result = await this.messagingSystem.call('ExecutionService:executeSnap', {
1383
+ ...snapData,
1384
+ endowments: await this.#getEndowments(snapId),
1385
+ });
1386
+ this.#transition(snapId, SnapStatusEvents.Start);
1387
+ // We treat the initialization of the snap as the first request, for idle timing purposes.
1388
+ runtime.lastRequest = Date.now();
1389
+ return result;
1390
+ }
1391
+ catch (error) {
1392
+ await this.#terminateSnap(snapId);
1393
+ throw error;
1394
+ }
1395
+ }
1396
+ /**
1397
+ * Gets the names of all endowments that will be added to the Snap's
1398
+ * Compartment when it executes. These should be the names of global
1399
+ * JavaScript APIs accessible in the root realm of the execution environment.
1400
+ *
1401
+ * Throws an error if the endowment getter for a permission returns a truthy
1402
+ * value that is not an array of strings.
1403
+ *
1404
+ * @param snapId - The id of the snap whose SES endowments to get.
1405
+ * @returns An array of the names of the endowments.
1406
+ */
1407
+ async #getEndowments(snapId) {
1408
+ let allEndowments = [];
1409
+ for (const permissionName of this.#environmentEndowmentPermissions) {
1410
+ if (this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName)) {
1411
+ const endowments = await this.messagingSystem.call('PermissionController:getEndowments', snapId, permissionName);
1412
+ if (endowments) {
1413
+ // We don't have any guarantees about the type of the endowments
1414
+ // value, so we have to guard at runtime.
1415
+ if (!Array.isArray(endowments) ||
1416
+ endowments.some((value) => typeof value !== 'string')) {
1417
+ throw new Error('Expected an array of string endowment names.');
1418
+ }
1419
+ allEndowments = allEndowments.concat(endowments);
1420
+ }
1421
+ }
1422
+ }
1423
+ const dedupedEndowments = [
1424
+ ...new Set([...DEFAULT_ENDOWMENTS, ...allEndowments]),
1425
+ ];
1426
+ if (dedupedEndowments.length <
1427
+ // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313
1428
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
1429
+ DEFAULT_ENDOWMENTS.length + allEndowments.length) {
1430
+ logError(`Duplicate endowments found for ${snapId}. Default endowments should not be requested.`, allEndowments);
1431
+ }
1432
+ return dedupedEndowments;
1433
+ }
1434
+ /**
1435
+ * Sets a snap in state. Called when a snap is installed or updated. Performs
1436
+ * various validation checks on the received arguments, and will throw if
1437
+ * validation fails.
1438
+ *
1439
+ * The snap will be enabled and unblocked by the time this method returns,
1440
+ * regardless of its previous state.
1441
+ *
1442
+ * See {@link SnapController.add} and {@link SnapController.updateSnap} for
1443
+ * usage.
1444
+ *
1445
+ * @param args - The add snap args.
1446
+ * @returns The resulting snap object.
1447
+ */
1448
+ #set(args) {
1449
+ const { id: snapId, origin, files, isUpdate = false, removable, preinstalled, hidden, hideSnapBranding, } = args;
1450
+ const { manifest, sourceCode: sourceCodeFile, svgIcon, auxiliaryFiles: rawAuxiliaryFiles, localizationFiles, } = files;
1451
+ assertIsSnapManifest(manifest.result);
1452
+ const { version } = manifest.result;
1453
+ const sourceCode = sourceCodeFile.toString();
1454
+ assert(typeof sourceCode === 'string' && sourceCode.length > 0, `Invalid source code for snap "${snapId}".`);
1455
+ const auxiliaryFiles = rawAuxiliaryFiles.map((file) => {
1456
+ assert(typeof file.data.base64 === 'string');
1457
+ return {
1458
+ path: file.path,
1459
+ value: file.data.base64,
1460
+ };
1461
+ });
1462
+ const snapsState = this.state.snaps;
1463
+ const existingSnap = snapsState[snapId];
1464
+ const previousVersionHistory = existingSnap?.versionHistory ?? [];
1465
+ const versionHistory = [
1466
+ ...previousVersionHistory,
1467
+ {
1468
+ version,
1469
+ date: Date.now(),
1470
+ origin,
1471
+ },
1472
+ ];
1473
+ const localizedFiles = localizationFiles.map((file) => file.result);
1474
+ const snap = {
1475
+ // Restore relevant snap state if it exists
1476
+ ...existingSnap,
1477
+ // Note that the snap will be unblocked and enabled, regardless of its
1478
+ // previous state.
1479
+ blocked: false,
1480
+ enabled: true,
1481
+ removable,
1482
+ preinstalled,
1483
+ hidden,
1484
+ hideSnapBranding,
1485
+ id: snapId,
1486
+ initialConnections: manifest.result.initialConnections,
1487
+ initialPermissions: manifest.result.initialPermissions,
1488
+ manifest: manifest.result,
1489
+ status: this.#statusMachine.config.initial,
1490
+ sourceCode,
1491
+ version,
1492
+ versionHistory,
1493
+ auxiliaryFiles,
1494
+ localizationFiles: localizedFiles,
1495
+ };
1496
+ // If the snap was blocked, it isn't any longer
1497
+ delete snap.blockInformation;
1498
+ // store the snap back in state
1499
+ const { inversePatches } = this.update((state) => {
1500
+ state.snaps[snapId] = snap;
1501
+ });
1502
+ // checking for isUpdate here as this function is also used in
1503
+ // the install flow, we do not care to create snapshots for installs
1504
+ if (isUpdate) {
1505
+ const rollbackSnapshot = this.#getRollbackSnapshot(snapId);
1506
+ if (rollbackSnapshot !== undefined) {
1507
+ rollbackSnapshot.statePatches = inversePatches;
1508
+ }
1509
+ }
1510
+ // In case the Snap uses a localized manifest, we need to get the
1511
+ // proposed name from the localized manifest.
1512
+ const { proposedName } = getLocalizedSnapManifest(manifest.result, 'en', localizedFiles);
1513
+ this.messagingSystem.call('SubjectMetadataController:addSubjectMetadata', {
1514
+ subjectType: SubjectType.Snap,
1515
+ name: proposedName,
1516
+ origin: snap.id,
1517
+ version,
1518
+ svgIcon: svgIcon?.toString() ?? null,
1519
+ });
1520
+ return { ...snap, sourceCode };
1521
+ }
1522
+ #validateSnapPermissions(processedPermissions) {
1523
+ const permissionKeys = Object.keys(processedPermissions);
1524
+ const handlerPermissions = Array.from(new Set(Object.values(handlerEndowments)));
1525
+ assert(permissionKeys.some((key) => handlerPermissions.includes(key)), `A snap must request at least one of the following permissions: ${handlerPermissions
1526
+ .filter((handler) => handler !== null)
1527
+ .join(', ')}.`);
1528
+ const excludedPermissionErrors = permissionKeys.reduce((errors, permission) => {
1529
+ if (hasProperty(this.#excludedPermissions, permission)) {
1530
+ errors.push(this.#excludedPermissions[permission]);
1531
+ }
1532
+ return errors;
1533
+ }, []);
1534
+ assert(excludedPermissionErrors.length === 0, `One or more permissions are not allowed:\n${excludedPermissionErrors.join('\n')}`);
1535
+ }
896
1536
  /**
897
1537
  * Initiates a request for the given snap's initial permissions.
898
1538
  * Must be called in order. See processRequestedSnap.
@@ -910,31 +1550,31 @@ export class SnapController extends BaseController {
910
1550
  const { initialPermissions, initialConnections } = snap;
911
1551
  try {
912
1552
  const processedPermissions = processSnapPermissions(initialPermissions);
913
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_validateSnapPermissions).call(this, processedPermissions);
914
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updateApproval).call(this, pendingApproval.id, {
1553
+ this.#validateSnapPermissions(processedPermissions);
1554
+ this.#updateApproval(pendingApproval.id, {
915
1555
  loading: false,
916
1556
  connections: initialConnections ?? {},
917
1557
  permissions: processedPermissions,
918
1558
  });
919
1559
  const { permissions: approvedPermissions, ...requestData } = (await pendingApproval.promise);
920
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, {
1560
+ this.#updatePermissions({
921
1561
  snapId,
922
1562
  newPermissions: approvedPermissions,
923
1563
  requestData,
924
1564
  });
925
1565
  if (snap.manifest.initialConnections) {
926
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handleInitialConnections).call(this, snapId, null, snap.manifest.initialConnections);
1566
+ this.#handleInitialConnections(snapId, null, snap.manifest.initialConnections);
927
1567
  }
928
1568
  }
929
1569
  finally {
930
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1570
+ const runtime = this.#getRuntimeExpect(snapId);
931
1571
  runtime.installPromise = null;
932
1572
  }
933
1573
  }
934
1574
  destroy() {
935
1575
  super.destroy();
936
- if (__classPrivateFieldGet(this, _SnapController_timeoutForLastRequestStatus, "f")) {
937
- clearTimeout(__classPrivateFieldGet(this, _SnapController_timeoutForLastRequestStatus, "f"));
1576
+ if (this.#timeoutForLastRequestStatus) {
1577
+ clearTimeout(this.#timeoutForLastRequestStatus);
938
1578
  }
939
1579
  /* eslint-disable @typescript-eslint/unbound-method */
940
1580
  this.messagingSystem.unsubscribe('ExecutionService:unhandledError', this._onUnhandledSnapError);
@@ -955,7 +1595,7 @@ export class SnapController extends BaseController {
955
1595
  * @returns The result of the JSON-RPC request.
956
1596
  */
957
1597
  async handleRequest({ snapId, origin, handler: handlerType, request: rawRequest, }) {
958
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertCanUsePlatform).call(this);
1598
+ this.#assertCanUsePlatform();
959
1599
  const request = {
960
1600
  jsonrpc: '2.0',
961
1601
  id: nanoid(),
@@ -985,910 +1625,410 @@ export class SnapController extends BaseController {
985
1625
  throw new Error(`Snap "${snapId}" is not permitted to handle requests from "${origin}".`);
986
1626
  }
987
1627
  }
988
- const handler = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRpcRequestHandler).call(this, snapId);
1628
+ const handler = this.#getRpcRequestHandler(snapId);
989
1629
  if (!handler) {
990
1630
  throw new Error(`Snap RPC message handler not found for snap "${snapId}".`);
991
1631
  }
992
- const timeout = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getExecutionTimeout).call(this, handlerPermissions);
1632
+ const timeout = this.#getExecutionTimeout(handlerPermissions);
993
1633
  return handler({ origin, handler: handlerType, request, timeout });
994
1634
  }
995
- }
996
- _SnapController_closeAllConnections = new WeakMap(), _SnapController_dynamicPermissions = new WeakMap(), _SnapController_environmentEndowmentPermissions = new WeakMap(), _SnapController_excludedPermissions = new WeakMap(), _SnapController_featureFlags = new WeakMap(), _SnapController_fetchFunction = new WeakMap(), _SnapController_idleTimeCheckInterval = new WeakMap(), _SnapController_maxIdleTime = new WeakMap(), _SnapController_encryptor = new WeakMap(), _SnapController_getMnemonic = new WeakMap(), _SnapController_getFeatureFlags = new WeakMap(), _SnapController_detectSnapLocation = new WeakMap(), _SnapController_snapsRuntimeData = new WeakMap(), _SnapController_rollbackSnapshots = new WeakMap(), _SnapController_timeoutForLastRequestStatus = new WeakMap(), _SnapController_statusMachine = new WeakMap(), _SnapController_preinstalledSnaps = new WeakMap(), _SnapController_instances = new WeakSet(), _SnapController_initializeStateMachine = function _SnapController_initializeStateMachine() {
997
- const disableGuard = ({ snapId }) => {
998
- return this.getExpect(snapId).enabled;
999
- };
1000
- const statusConfig = {
1001
- initial: SnapStatus.Installing,
1002
- states: {
1003
- [SnapStatus.Installing]: {
1004
- on: {
1005
- [SnapStatusEvents.Start]: {
1006
- target: SnapStatus.Running,
1007
- cond: disableGuard,
1008
- },
1009
- },
1010
- },
1011
- [SnapStatus.Updating]: {
1012
- on: {
1013
- [SnapStatusEvents.Start]: {
1014
- target: SnapStatus.Running,
1015
- cond: disableGuard,
1016
- },
1017
- [SnapStatusEvents.Stop]: SnapStatus.Stopped,
1018
- },
1019
- },
1020
- [SnapStatus.Running]: {
1021
- on: {
1022
- [SnapStatusEvents.Stop]: SnapStatus.Stopped,
1023
- [SnapStatusEvents.Crash]: SnapStatus.Crashed,
1024
- },
1025
- },
1026
- [SnapStatus.Stopped]: {
1027
- on: {
1028
- [SnapStatusEvents.Start]: {
1029
- target: SnapStatus.Running,
1030
- cond: disableGuard,
1031
- },
1032
- [SnapStatusEvents.Update]: SnapStatus.Updating,
1033
- },
1034
- },
1035
- [SnapStatus.Crashed]: {
1036
- on: {
1037
- [SnapStatusEvents.Start]: {
1038
- target: SnapStatus.Running,
1039
- cond: disableGuard,
1040
- },
1041
- [SnapStatusEvents.Update]: SnapStatus.Updating,
1042
- },
1043
- },
1044
- },
1045
- };
1046
- __classPrivateFieldSet(this, _SnapController_statusMachine, createMachine(statusConfig), "f");
1047
- validateMachine(__classPrivateFieldGet(this, _SnapController_statusMachine, "f"));
1048
- }, _SnapController_registerMessageHandlers = function _SnapController_registerMessageHandlers() {
1049
- this.messagingSystem.registerActionHandler(`${controllerName}:clearSnapState`, (...args) => this.clearSnapState(...args));
1050
- this.messagingSystem.registerActionHandler(`${controllerName}:get`, (...args) => this.get(...args));
1051
- this.messagingSystem.registerActionHandler(`${controllerName}:getSnapState`, async (...args) => this.getSnapState(...args));
1052
- this.messagingSystem.registerActionHandler(`${controllerName}:handleRequest`, async (...args) => this.handleRequest(...args));
1053
- this.messagingSystem.registerActionHandler(`${controllerName}:has`, (...args) => this.has(...args));
1054
- this.messagingSystem.registerActionHandler(`${controllerName}:updateBlockedSnaps`, async () => this.updateBlockedSnaps());
1055
- this.messagingSystem.registerActionHandler(`${controllerName}:updateSnapState`, async (...args) => this.updateSnapState(...args));
1056
- this.messagingSystem.registerActionHandler(`${controllerName}:enable`, (...args) => this.enableSnap(...args));
1057
- this.messagingSystem.registerActionHandler(`${controllerName}:disable`, async (...args) => this.disableSnap(...args));
1058
- this.messagingSystem.registerActionHandler(`${controllerName}:remove`, async (...args) => this.removeSnap(...args));
1059
- this.messagingSystem.registerActionHandler(`${controllerName}:getPermitted`, (...args) => this.getPermittedSnaps(...args));
1060
- this.messagingSystem.registerActionHandler(`${controllerName}:install`, async (...args) => this.installSnaps(...args));
1061
- this.messagingSystem.registerActionHandler(`${controllerName}:getAll`, (...args) => this.getAllSnaps(...args));
1062
- this.messagingSystem.registerActionHandler(`${controllerName}:incrementActiveReferences`, (...args) => this.incrementActiveReferences(...args));
1063
- this.messagingSystem.registerActionHandler(`${controllerName}:decrementActiveReferences`, (...args) => this.decrementActiveReferences(...args));
1064
- this.messagingSystem.registerActionHandler(`${controllerName}:getRegistryMetadata`, async (...args) => this.getRegistryMetadata(...args));
1065
- this.messagingSystem.registerActionHandler(`${controllerName}:disconnectOrigin`, (...args) => this.removeSnapFromSubject(...args));
1066
- this.messagingSystem.registerActionHandler(`${controllerName}:revokeDynamicPermissions`, (...args) => this.revokeDynamicSnapPermissions(...args));
1067
- this.messagingSystem.registerActionHandler(`${controllerName}:getFile`, async (...args) => this.getSnapFile(...args));
1068
- this.messagingSystem.registerActionHandler(`${controllerName}:stopAllSnaps`, async (...args) => this.stopAllSnaps(...args));
1069
- }, _SnapController_handlePreinstalledSnaps = function _SnapController_handlePreinstalledSnaps(preinstalledSnaps) {
1070
- for (const { snapId, manifest, files, removable, hidden, hideSnapBranding, } of preinstalledSnaps) {
1071
- const existingSnap = this.get(snapId);
1072
- const isAlreadyInstalled = existingSnap !== undefined;
1073
- const isUpdate = isAlreadyInstalled && gtVersion(manifest.version, existingSnap.version);
1074
- // Disallow downgrades and overwriting non preinstalled snaps
1075
- if (isAlreadyInstalled &&
1076
- (!isUpdate || existingSnap.preinstalled !== true)) {
1077
- continue;
1078
- }
1079
- const manifestFile = new VirtualFile({
1080
- path: NpmSnapFileNames.Manifest,
1081
- value: JSON.stringify(manifest),
1082
- result: manifest,
1083
- });
1084
- const virtualFiles = files.map(({ path, value }) => new VirtualFile({ value, path }));
1085
- const { filePath, iconPath } = manifest.source.location.npm;
1086
- const sourceCode = virtualFiles.find((file) => file.path === filePath);
1087
- const svgIcon = iconPath
1088
- ? virtualFiles.find((file) => file.path === iconPath)
1089
- : undefined;
1090
- assert(sourceCode, 'Source code not provided for preinstalled snap.');
1091
- assert(!iconPath || (iconPath && svgIcon), 'Icon not provided for preinstalled snap.');
1092
- assert(manifest.source.files === undefined, 'Auxiliary files are not currently supported for preinstalled snaps.');
1093
- const localizationFiles = manifest.source.locales?.map((path) => virtualFiles.find((file) => file.path === path)) ?? [];
1094
- const validatedLocalizationFiles = getValidatedLocalizationFiles(localizationFiles.filter(Boolean));
1095
- assert(localizationFiles.length === validatedLocalizationFiles.length, 'Missing localization files for preinstalled snap.');
1096
- const filesObject = {
1097
- manifest: manifestFile,
1098
- sourceCode,
1099
- svgIcon,
1100
- auxiliaryFiles: [],
1101
- localizationFiles: validatedLocalizationFiles,
1102
- };
1103
- // Add snap to the SnapController state
1104
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_set).call(this, {
1105
- id: snapId,
1106
- origin: 'metamask',
1107
- files: filesObject,
1108
- removable,
1109
- hidden,
1110
- hideSnapBranding,
1111
- preinstalled: true,
1112
- });
1113
- // Setup permissions
1114
- const processedPermissions = processSnapPermissions(manifest.initialPermissions);
1115
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_validateSnapPermissions).call(this, processedPermissions);
1116
- const { newPermissions, unusedPermissions } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_calculatePermissionsChange).call(this, snapId, processedPermissions);
1117
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, { snapId, newPermissions, unusedPermissions });
1118
- if (manifest.initialConnections) {
1119
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_handleInitialConnections).call(this, snapId, existingSnap?.initialConnections ?? null, manifest.initialConnections);
1120
- }
1121
- // Set status
1122
- this.update((state) => {
1123
- state.snaps[snapId].status = SnapStatus.Stopped;
1124
- });
1125
- }
1126
- }, _SnapController_pollForLastRequestStatus = function _SnapController_pollForLastRequestStatus() {
1127
- __classPrivateFieldSet(this, _SnapController_timeoutForLastRequestStatus, setTimeout(() => {
1128
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_stopSnapsLastRequestPastMax).call(this).catch((error) => {
1129
- // TODO: Decide how to handle errors.
1130
- logError(error);
1131
- });
1132
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_pollForLastRequestStatus).call(this);
1133
- }, __classPrivateFieldGet(this, _SnapController_idleTimeCheckInterval, "f")), "f");
1134
- }, _SnapController_blockSnap =
1135
- /**
1136
- * Blocks an installed snap and prevents it from being started again. Emits
1137
- * {@link SnapBlocked}. Does nothing if the snap is not installed.
1138
- *
1139
- * @param snapId - The snap to block.
1140
- * @param blockedSnapInfo - Information detailing why the snap is blocked.
1141
- */
1142
- async function _SnapController_blockSnap(snapId, blockedSnapInfo) {
1143
- if (!this.has(snapId)) {
1144
- return;
1635
+ /**
1636
+ * Determine the execution timeout for a given handler permission.
1637
+ *
1638
+ * If no permission is specified or the permission itself has no execution timeout defined
1639
+ * the constructor argument `maxRequestTime` will be used.
1640
+ *
1641
+ * @param permission - An optional permission constraint for the handler being called.
1642
+ * @returns The execution timeout for the given handler.
1643
+ */
1644
+ #getExecutionTimeout(permission) {
1645
+ return getMaxRequestTimeCaveat(permission) ?? this.maxRequestTime;
1145
1646
  }
1146
- try {
1147
- this.update((state) => {
1148
- state.snaps[snapId].blocked = true;
1149
- state.snaps[snapId].blockInformation = blockedSnapInfo;
1150
- });
1151
- await this.disableSnap(snapId);
1152
- }
1153
- catch (error) {
1154
- logError(`Encountered error when stopping blocked snap "${snapId}".`, error);
1155
- }
1156
- this.messagingSystem.publish(`${controllerName}:snapBlocked`, snapId, blockedSnapInfo);
1157
- }, _SnapController_unblockSnap = function _SnapController_unblockSnap(snapId) {
1158
- if (!this.has(snapId) || !this.state.snaps[snapId].blocked) {
1159
- return;
1160
- }
1161
- this.update((state) => {
1162
- state.snaps[snapId].blocked = false;
1163
- delete state.snaps[snapId].blockInformation;
1164
- });
1165
- this.messagingSystem.publish(`${controllerName}:snapUnblocked`, snapId);
1166
- }, _SnapController_assertIsInstallAllowed = async function _SnapController_assertIsInstallAllowed(snapId, snapInfo) {
1167
- const results = await this.messagingSystem.call('SnapsRegistry:get', {
1168
- [snapId]: snapInfo,
1169
- });
1170
- const result = results[snapId];
1171
- if (result.status === SnapsRegistryStatus.Blocked) {
1172
- throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": The version is blocked. ${result.reason?.explanation ?? ''}`);
1173
- }
1174
- const isAllowlistingRequired = Object.keys(snapInfo.permissions).some((permission) => !ALLOWED_PERMISSIONS.includes(permission));
1175
- if (__classPrivateFieldGet(this, _SnapController_featureFlags, "f").requireAllowlist &&
1176
- isAllowlistingRequired &&
1177
- result.status !== SnapsRegistryStatus.Verified) {
1178
- throw new Error(`Cannot install version "${snapInfo.version}" of snap "${snapId}": ${result.status === SnapsRegistryStatus.Unavailable
1179
- ? 'The registry is temporarily unavailable.'
1180
- : 'The snap is not on the allowlist.'}`);
1181
- }
1182
- }, _SnapController_assertCanInstallSnaps = function _SnapController_assertCanInstallSnaps() {
1183
- assert(__classPrivateFieldGet(this, _SnapController_featureFlags, "f").disableSnapInstallation !== true, 'Installing Snaps is currently disabled in this version of MetaMask.');
1184
- }, _SnapController_assertCanUsePlatform = function _SnapController_assertCanUsePlatform() {
1185
- const flags = __classPrivateFieldGet(this, _SnapController_getFeatureFlags, "f").call(this);
1186
- assert(flags.disableSnaps !== true, 'The Snaps platform requires basic functionality to be used. Enable basic functionality in the settings to use the Snaps platform.');
1187
- }, _SnapController_stopSnapsLastRequestPastMax = async function _SnapController_stopSnapsLastRequestPastMax() {
1188
- const entries = [...__classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").entries()];
1189
- return Promise.all(entries
1190
- .filter(([_snapId, runtime]) => runtime.activeReferences === 0 &&
1191
- runtime.pendingInboundRequests.length === 0 &&
1192
- runtime.lastRequest &&
1193
- __classPrivateFieldGet(this, _SnapController_maxIdleTime, "f") &&
1194
- timeSince(runtime.lastRequest) > __classPrivateFieldGet(this, _SnapController_maxIdleTime, "f"))
1195
- .map(async ([snapId]) => this.stopSnap(snapId, SnapStatusEvents.Stop)));
1196
- }, _SnapController_transition = function _SnapController_transition(snapId, event) {
1197
- const { interpreter } = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1198
- interpreter.send(event);
1199
- this.update((state) => {
1200
- state.snaps[snapId].status = interpreter.state.value;
1201
- });
1202
- }, _SnapController_terminateSnap =
1203
- /**
1204
- * Terminates the specified snap and emits the `snapTerminated` event.
1205
- *
1206
- * @param snapId - The snap to terminate.
1207
- */
1208
- async function _SnapController_terminateSnap(snapId) {
1209
- await this.messagingSystem.call('ExecutionService:terminateSnap', snapId);
1210
- // Hack to give up execution for a bit to let gracefully terminating Snaps return.
1211
- await new Promise((resolve) => setTimeout(resolve, 1));
1212
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1213
- // Unresponsive requests may still be timed, time them out.
1214
- runtime.pendingInboundRequests
1215
- .filter((pendingRequest) => pendingRequest.timer.status !== 'finished')
1216
- .forEach((pendingRequest) => pendingRequest.timer.finish());
1217
- // Hack to give up execution for a bit to let timed out requests return.
1218
- await new Promise((resolve) => setTimeout(resolve, 1));
1219
- this.messagingSystem.publish('SnapController:snapTerminated', this.getTruncatedExpect(snapId));
1220
- }, _SnapController_getSnapEncryptionKey =
1221
- /**
1222
- * Generate an encryption key to be used for state encryption for a given Snap.
1223
- *
1224
- * @param options - An options bag.
1225
- * @param options.snapId - The Snap ID.
1226
- * @param options.salt - A salt to be used for the encryption key.
1227
- * @param options.useCache - Whether to use caching or not.
1228
- * @param options.keyMetadata - Optional metadata about how to derive the encryption key.
1229
- * @returns An encryption key.
1230
- */
1231
- async function _SnapController_getSnapEncryptionKey({ snapId, salt: passedSalt, useCache, keyMetadata, }) {
1232
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1233
- if (runtime.encryptionKey && runtime.encryptionSalt && useCache) {
1234
- return {
1235
- key: await __classPrivateFieldGet(this, _SnapController_encryptor, "f").importKey(runtime.encryptionKey),
1236
- salt: runtime.encryptionSalt,
1647
+ /**
1648
+ * Gets the RPC message handler for the given snap.
1649
+ *
1650
+ * @param snapId - The id of the Snap whose message handler to get.
1651
+ * @returns The RPC handler for the given snap.
1652
+ */
1653
+ #getRpcRequestHandler(snapId) {
1654
+ const runtime = this.#getRuntimeExpect(snapId);
1655
+ const existingHandler = runtime.rpcHandler;
1656
+ if (existingHandler) {
1657
+ return existingHandler;
1658
+ }
1659
+ const requestQueue = new RequestQueue(5);
1660
+ // We need to set up this promise map to map snapIds to their respective startPromises,
1661
+ // because otherwise we would lose context on the correct startPromise.
1662
+ const startPromises = new Map();
1663
+ const rpcHandler = async ({ origin, handler: handlerType, request, timeout, }) => {
1664
+ if (this.state.snaps[snapId].enabled === false) {
1665
+ throw new Error(`Snap "${snapId}" is disabled.`);
1666
+ }
1667
+ if (this.state.snaps[snapId].status === SnapStatus.Installing) {
1668
+ throw new Error(`Snap "${snapId}" is currently being installed. Please try again later.`);
1669
+ }
1670
+ if (!this.isRunning(snapId)) {
1671
+ let localStartPromise = startPromises.get(snapId);
1672
+ if (!localStartPromise) {
1673
+ localStartPromise = this.startSnap(snapId);
1674
+ startPromises.set(snapId, localStartPromise);
1675
+ }
1676
+ else if (requestQueue.get(origin) >= requestQueue.maxQueueSize) {
1677
+ throw new Error('Exceeds maximum number of requests waiting to be resolved, please try again.');
1678
+ }
1679
+ requestQueue.increment(origin);
1680
+ try {
1681
+ await localStartPromise;
1682
+ }
1683
+ finally {
1684
+ requestQueue.decrement(origin);
1685
+ // Only delete startPromise for a snap if its value hasn't changed
1686
+ if (startPromises.get(snapId) === localStartPromise) {
1687
+ startPromises.delete(snapId);
1688
+ }
1689
+ }
1690
+ }
1691
+ const timer = new Timer(timeout);
1692
+ this.#recordSnapRpcRequestStart(snapId, request.id, timer);
1693
+ const handleRpcRequestPromise = this.messagingSystem.call('ExecutionService:handleRpcRequest', snapId, { origin, handler: handlerType, request });
1694
+ // This will either get the result or reject due to the timeout.
1695
+ try {
1696
+ const result = await withTimeout(handleRpcRequestPromise, timer);
1697
+ if (result === hasTimedOut) {
1698
+ throw new Error(`${snapId} failed to respond to the request in time.`);
1699
+ }
1700
+ await this.#assertSnapRpcRequestResult(snapId, handlerType, result);
1701
+ const transformedResult = await this.#transformSnapRpcRequestResult(snapId, handlerType, result);
1702
+ this.#recordSnapRpcRequestFinish(snapId, request.id);
1703
+ return transformedResult;
1704
+ }
1705
+ catch (error) {
1706
+ // We flag the RPC request as finished early since termination may affect pending requests
1707
+ this.#recordSnapRpcRequestFinish(snapId, request.id);
1708
+ const [jsonRpcError, handled] = unwrapError(error);
1709
+ if (!handled) {
1710
+ await this.stopSnap(snapId, SnapStatusEvents.Crash);
1711
+ }
1712
+ throw jsonRpcError;
1713
+ }
1237
1714
  };
1715
+ runtime.rpcHandler = rpcHandler;
1716
+ return rpcHandler;
1238
1717
  }
1239
- const salt = passedSalt ?? __classPrivateFieldGet(this, _SnapController_encryptor, "f").generateSalt();
1240
- const mnemonicPhrase = await __classPrivateFieldGet(this, _SnapController_getMnemonic, "f").call(this);
1241
- const entropy = await getEncryptionEntropy({ snapId, mnemonicPhrase });
1242
- const encryptionKey = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").keyFromPassword(entropy, salt, true, keyMetadata);
1243
- const exportedKey = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").exportKey(encryptionKey);
1244
- // Cache exported encryption key in runtime
1245
- if (useCache) {
1246
- runtime.encryptionKey = exportedKey;
1247
- runtime.encryptionSalt = salt;
1248
- }
1249
- return { key: encryptionKey, salt };
1250
- }, _SnapController_decryptSnapState =
1251
- /**
1252
- * Decrypt the encrypted state for a given Snap.
1253
- *
1254
- * @param snapId - The Snap ID.
1255
- * @param state - The encrypted state as a string.
1256
- * @returns A valid JSON object derived from the encrypted state.
1257
- * @throws If the decryption fails or the decrypted state is not valid JSON.
1258
- */
1259
- async function _SnapController_decryptSnapState(snapId, state) {
1260
- try {
1261
- const parsed = parseJson(state);
1262
- const { salt, keyMetadata } = parsed;
1263
- const useCache = __classPrivateFieldGet(this, _SnapController_encryptor, "f").isVaultUpdated(state);
1264
- const { key } = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getSnapEncryptionKey).call(this, {
1265
- snapId,
1266
- salt,
1267
- useCache,
1268
- // When decrypting state we expect key metadata to be present.
1269
- // If it isn't present, we assume that the Snap state we are decrypting is old enough to use the legacy encryption params.
1270
- keyMetadata: keyMetadata ?? LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS,
1271
- });
1272
- const decryptedState = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").decryptWithKey(key, parsed);
1273
- assert(isValidJson(decryptedState));
1274
- return decryptedState;
1275
- }
1276
- catch {
1277
- throw rpcErrors.internal({
1278
- message: 'Failed to decrypt snap state, the state must be corrupted.',
1279
- });
1280
- }
1281
- }, _SnapController_encryptSnapState =
1282
- /**
1283
- * Encrypt a JSON state object for a given Snap.
1284
- *
1285
- * Note: This function does not assert the validity of the object,
1286
- * please ensure only valid JSON is passed to it.
1287
- *
1288
- * @param snapId - The Snap ID.
1289
- * @param state - The state object.
1290
- * @returns A string containing the encrypted JSON object.
1291
- */
1292
- async function _SnapController_encryptSnapState(snapId, state) {
1293
- const { key, salt } = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getSnapEncryptionKey).call(this, {
1294
- snapId,
1295
- useCache: true,
1296
- });
1297
- const encryptedState = await __classPrivateFieldGet(this, _SnapController_encryptor, "f").encryptWithKey(key, state);
1298
- encryptedState.salt = salt;
1299
- return JSON.stringify(encryptedState);
1300
- }, _SnapController_handleInitialConnections = function _SnapController_handleInitialConnections(snapId, previousInitialConnections, initialConnections) {
1301
- if (previousInitialConnections) {
1302
- const revokedInitialConnections = setDiff(previousInitialConnections, initialConnections);
1303
- for (const origin of Object.keys(revokedInitialConnections)) {
1304
- this.removeSnapFromSubject(origin, snapId);
1305
- }
1306
- }
1307
- for (const origin of Object.keys(initialConnections)) {
1308
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_addSnapToSubject).call(this, origin, snapId);
1309
- }
1310
- }, _SnapController_addSnapToSubject = function _SnapController_addSnapToSubject(origin, snapId) {
1311
- const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
1312
- const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
1313
- const subjectHasSnap = Boolean(existingCaveat?.value?.[snapId]);
1314
- // If the subject is already connected to the snap, this is a no-op.
1315
- if (subjectHasSnap) {
1316
- return;
1317
- }
1318
- // If an existing caveat exists, we add the snap to that.
1319
- if (existingCaveat) {
1320
- this.messagingSystem.call('PermissionController:updateCaveat', origin, WALLET_SNAP_PERMISSION_KEY, SnapCaveatType.SnapIds, { ...existingCaveat.value, [snapId]: {} });
1321
- return;
1322
- }
1323
- const approvedPermissions = {
1324
- [WALLET_SNAP_PERMISSION_KEY]: {
1325
- caveats: [
1326
- {
1327
- type: SnapCaveatType.SnapIds,
1328
- value: {
1329
- [snapId]: {},
1330
- },
1331
- },
1332
- ],
1333
- },
1334
- };
1335
- this.messagingSystem.call('PermissionController:grantPermissions', {
1336
- approvedPermissions,
1337
- subject: { origin },
1338
- });
1339
- }, _SnapController_removeSnapFromSubjects = function _SnapController_removeSnapFromSubjects(snapId) {
1340
- const subjects = this.messagingSystem.call('PermissionController:getSubjectNames');
1341
- for (const subject of subjects) {
1342
- this.removeSnapFromSubject(subject, snapId);
1343
- }
1344
- }, _SnapController_revokeAllSnapPermissions = function _SnapController_revokeAllSnapPermissions(snapId) {
1345
- if (this.messagingSystem.call('PermissionController:hasPermissions', snapId)) {
1346
- this.messagingSystem.call('PermissionController:revokeAllPermissions', snapId);
1347
- }
1348
- }, _SnapController_createApproval = function _SnapController_createApproval({ origin, snapId, type, }) {
1349
- const id = nanoid();
1350
- const promise = this.messagingSystem.call('ApprovalController:addRequest', {
1351
- origin,
1352
- id,
1353
- type,
1354
- requestData: {
1355
- // Mirror previous installation metadata
1356
- metadata: { id, origin: snapId, dappOrigin: origin },
1357
- snapId,
1358
- },
1359
- requestState: {
1360
- loading: true,
1361
- },
1362
- }, true);
1363
- return { id, promise };
1364
- }, _SnapController_updateApproval = function _SnapController_updateApproval(id, requestState) {
1365
- try {
1366
- this.messagingSystem.call('ApprovalController:updateRequestState', {
1367
- id,
1368
- requestState,
1369
- });
1370
- }
1371
- catch {
1372
- // Do nothing
1373
- }
1374
- }, _SnapController_resolveAllowlistVersion = async function _SnapController_resolveAllowlistVersion(snapId, versionRange) {
1375
- return await this.messagingSystem.call('SnapsRegistry:resolveVersion', snapId, versionRange);
1376
- }, _SnapController_add =
1377
- /**
1378
- * Returns a promise representing the complete installation of the requested snap.
1379
- * If the snap is already being installed, the previously pending promise will be returned.
1380
- *
1381
- * @param args - Object containing the snap id and either the URL of the snap's manifest,
1382
- * or the snap's manifest and source code. The object may also optionally contain a target
1383
- * version.
1384
- * @returns The resulting snap object.
1385
- */
1386
- async function _SnapController_add(args) {
1387
- const { id: snapId, location, versionRange } = args;
1388
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_setupRuntime).call(this, snapId);
1389
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1390
- if (!runtime.installPromise) {
1391
- log(`Adding snap: ${snapId}`);
1392
- // If fetching and setting the snap succeeds, this property will be set
1393
- // to null in the authorize() method.
1394
- runtime.installPromise = (async () => {
1395
- const fetchedSnap = await fetchSnap(snapId, location);
1396
- const manifest = fetchedSnap.manifest.result;
1397
- if (!satisfiesVersionRange(manifest.version, versionRange)) {
1398
- throw new Error(`Version mismatch. Manifest for "${snapId}" specifies version "${manifest.version}" which doesn't satisfy requested version range "${versionRange}".`);
1399
- }
1400
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertIsInstallAllowed).call(this, snapId, {
1401
- version: manifest.version,
1402
- checksum: manifest.source.shasum,
1403
- permissions: manifest.initialPermissions,
1404
- });
1405
- return __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_set).call(this, {
1406
- ...args,
1407
- files: fetchedSnap,
1408
- id: snapId,
1409
- });
1410
- })();
1411
- }
1412
- try {
1413
- return await runtime.installPromise;
1414
- }
1415
- catch (error) {
1416
- // Reset promise so users can retry installation in case the problem is
1417
- // temporary.
1418
- runtime.installPromise = null;
1419
- throw error;
1420
- }
1421
- }, _SnapController_startSnap = async function _SnapController_startSnap(snapData) {
1422
- const { snapId } = snapData;
1423
- if (this.isRunning(snapId)) {
1424
- throw new Error(`Snap "${snapId}" is already started.`);
1425
- }
1426
- try {
1427
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1428
- const result = await this.messagingSystem.call('ExecutionService:executeSnap', {
1429
- ...snapData,
1430
- endowments: await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getEndowments).call(this, snapId),
1431
- });
1432
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, SnapStatusEvents.Start);
1433
- // We treat the initialization of the snap as the first request, for idle timing purposes.
1434
- runtime.lastRequest = Date.now();
1435
- return result;
1718
+ /**
1719
+ * Create a dynamic interface in the SnapInterfaceController.
1720
+ *
1721
+ * @param snapId - The snap ID.
1722
+ * @param content - The initial interface content.
1723
+ * @returns An identifier that can be used to identify the interface.
1724
+ */
1725
+ async #createInterface(snapId, content) {
1726
+ return this.messagingSystem.call('SnapInterfaceController:createInterface', snapId, content);
1436
1727
  }
1437
- catch (error) {
1438
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_terminateSnap).call(this, snapId);
1439
- throw error;
1728
+ #assertInterfaceExists(snapId, id) {
1729
+ // This will throw if the interface isn't accessible, but we assert nevertheless.
1730
+ assert(this.messagingSystem.call('SnapInterfaceController:getInterface', snapId, id));
1440
1731
  }
1441
- }, _SnapController_getEndowments =
1442
- /**
1443
- * Gets the names of all endowments that will be added to the Snap's
1444
- * Compartment when it executes. These should be the names of global
1445
- * JavaScript APIs accessible in the root realm of the execution environment.
1446
- *
1447
- * Throws an error if the endowment getter for a permission returns a truthy
1448
- * value that is not an array of strings.
1449
- *
1450
- * @param snapId - The id of the snap whose SES endowments to get.
1451
- * @returns An array of the names of the endowments.
1452
- */
1453
- async function _SnapController_getEndowments(snapId) {
1454
- let allEndowments = [];
1455
- for (const permissionName of __classPrivateFieldGet(this, _SnapController_environmentEndowmentPermissions, "f")) {
1456
- if (this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName)) {
1457
- const endowments = await this.messagingSystem.call('PermissionController:getEndowments', snapId, permissionName);
1458
- if (endowments) {
1459
- // We don't have any guarantees about the type of the endowments
1460
- // value, so we have to guard at runtime.
1461
- if (!Array.isArray(endowments) ||
1462
- endowments.some((value) => typeof value !== 'string')) {
1463
- throw new Error('Expected an array of string endowment names.');
1732
+ /**
1733
+ * Transform a RPC request result if necessary.
1734
+ *
1735
+ * @param snapId - The snap ID of the snap that produced the result.
1736
+ * @param handlerType - The handler type that produced the result.
1737
+ * @param result - The result.
1738
+ * @returns The transformed result if applicable, otherwise the original result.
1739
+ */
1740
+ async #transformSnapRpcRequestResult(snapId, handlerType, result) {
1741
+ switch (handlerType) {
1742
+ case HandlerType.OnTransaction:
1743
+ case HandlerType.OnSignature:
1744
+ case HandlerType.OnHomePage: {
1745
+ // Since this type has been asserted earlier we can cast
1746
+ const castResult = result;
1747
+ // If a handler returns static content, we turn it into a dynamic UI
1748
+ if (castResult && hasProperty(castResult, 'content')) {
1749
+ const { content, ...rest } = castResult;
1750
+ const id = await this.#createInterface(snapId, content);
1751
+ return { ...rest, id };
1464
1752
  }
1465
- allEndowments = allEndowments.concat(endowments);
1753
+ return result;
1466
1754
  }
1755
+ default:
1756
+ return result;
1467
1757
  }
1468
1758
  }
1469
- const dedupedEndowments = [
1470
- ...new Set([...DEFAULT_ENDOWMENTS, ...allEndowments]),
1471
- ];
1472
- if (dedupedEndowments.length <
1473
- // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313
1474
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
1475
- DEFAULT_ENDOWMENTS.length + allEndowments.length) {
1476
- logError(`Duplicate endowments found for ${snapId}. Default endowments should not be requested.`, allEndowments);
1477
- }
1478
- return dedupedEndowments;
1479
- }, _SnapController_set = function _SnapController_set(args) {
1480
- const { id: snapId, origin, files, isUpdate = false, removable, preinstalled, hidden, hideSnapBranding, } = args;
1481
- const { manifest, sourceCode: sourceCodeFile, svgIcon, auxiliaryFiles: rawAuxiliaryFiles, localizationFiles, } = files;
1482
- assertIsSnapManifest(manifest.result);
1483
- const { version } = manifest.result;
1484
- const sourceCode = sourceCodeFile.toString();
1485
- assert(typeof sourceCode === 'string' && sourceCode.length > 0, `Invalid source code for snap "${snapId}".`);
1486
- const auxiliaryFiles = rawAuxiliaryFiles.map((file) => {
1487
- assert(typeof file.data.base64 === 'string');
1488
- return {
1489
- path: file.path,
1490
- value: file.data.base64,
1491
- };
1492
- });
1493
- const snapsState = this.state.snaps;
1494
- const existingSnap = snapsState[snapId];
1495
- const previousVersionHistory = existingSnap?.versionHistory ?? [];
1496
- const versionHistory = [
1497
- ...previousVersionHistory,
1498
- {
1499
- version,
1500
- date: Date.now(),
1501
- origin,
1502
- },
1503
- ];
1504
- const localizedFiles = localizationFiles.map((file) => file.result);
1505
- const snap = {
1506
- // Restore relevant snap state if it exists
1507
- ...existingSnap,
1508
- // Note that the snap will be unblocked and enabled, regardless of its
1509
- // previous state.
1510
- blocked: false,
1511
- enabled: true,
1512
- removable,
1513
- preinstalled,
1514
- hidden,
1515
- hideSnapBranding,
1516
- id: snapId,
1517
- initialConnections: manifest.result.initialConnections,
1518
- initialPermissions: manifest.result.initialPermissions,
1519
- manifest: manifest.result,
1520
- status: __classPrivateFieldGet(this, _SnapController_statusMachine, "f").config.initial,
1521
- sourceCode,
1522
- version,
1523
- versionHistory,
1524
- auxiliaryFiles,
1525
- localizationFiles: localizedFiles,
1526
- };
1527
- // If the snap was blocked, it isn't any longer
1528
- delete snap.blockInformation;
1529
- // store the snap back in state
1530
- const { inversePatches } = this.update((state) => {
1531
- state.snaps[snapId] = snap;
1532
- });
1533
- // checking for isUpdate here as this function is also used in
1534
- // the install flow, we do not care to create snapshots for installs
1535
- if (isUpdate) {
1536
- const rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1537
- if (rollbackSnapshot !== undefined) {
1538
- rollbackSnapshot.statePatches = inversePatches;
1539
- }
1540
- }
1541
- // In case the Snap uses a localized manifest, we need to get the
1542
- // proposed name from the localized manifest.
1543
- const { proposedName } = getLocalizedSnapManifest(manifest.result, 'en', localizedFiles);
1544
- this.messagingSystem.call('SubjectMetadataController:addSubjectMetadata', {
1545
- subjectType: SubjectType.Snap,
1546
- name: proposedName,
1547
- origin: snap.id,
1548
- version,
1549
- svgIcon: svgIcon?.toString() ?? null,
1550
- });
1551
- return { ...snap, sourceCode };
1552
- }, _SnapController_validateSnapPermissions = function _SnapController_validateSnapPermissions(processedPermissions) {
1553
- const permissionKeys = Object.keys(processedPermissions);
1554
- const handlerPermissions = Array.from(new Set(Object.values(handlerEndowments)));
1555
- assert(permissionKeys.some((key) => handlerPermissions.includes(key)), `A snap must request at least one of the following permissions: ${handlerPermissions
1556
- .filter((handler) => handler !== null)
1557
- .join(', ')}.`);
1558
- const excludedPermissionErrors = permissionKeys.reduce((errors, permission) => {
1559
- if (hasProperty(__classPrivateFieldGet(this, _SnapController_excludedPermissions, "f"), permission)) {
1560
- errors.push(__classPrivateFieldGet(this, _SnapController_excludedPermissions, "f")[permission]);
1561
- }
1562
- return errors;
1563
- }, []);
1564
- assert(excludedPermissionErrors.length === 0, `One or more permissions are not allowed:\n${excludedPermissionErrors.join('\n')}`);
1565
- }, _SnapController_getExecutionTimeout = function _SnapController_getExecutionTimeout(permission) {
1566
- return getMaxRequestTimeCaveat(permission) ?? this.maxRequestTime;
1567
- }, _SnapController_getRpcRequestHandler = function _SnapController_getRpcRequestHandler(snapId) {
1568
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1569
- const existingHandler = runtime.rpcHandler;
1570
- if (existingHandler) {
1571
- return existingHandler;
1572
- }
1573
- const requestQueue = new RequestQueue(5);
1574
- // We need to set up this promise map to map snapIds to their respective startPromises,
1575
- // because otherwise we would lose context on the correct startPromise.
1576
- const startPromises = new Map();
1577
- const rpcHandler = async ({ origin, handler: handlerType, request, timeout, }) => {
1578
- if (this.state.snaps[snapId].enabled === false) {
1579
- throw new Error(`Snap "${snapId}" is disabled.`);
1580
- }
1581
- if (this.state.snaps[snapId].status === SnapStatus.Installing) {
1582
- throw new Error(`Snap "${snapId}" is currently being installed. Please try again later.`);
1583
- }
1584
- if (!this.isRunning(snapId)) {
1585
- let localStartPromise = startPromises.get(snapId);
1586
- if (!localStartPromise) {
1587
- localStartPromise = this.startSnap(snapId);
1588
- startPromises.set(snapId, localStartPromise);
1589
- }
1590
- else if (requestQueue.get(origin) >= requestQueue.maxQueueSize) {
1591
- throw new Error('Exceeds maximum number of requests waiting to be resolved, please try again.');
1759
+ /**
1760
+ * Assert that the returned result of a Snap RPC call is the expected shape.
1761
+ *
1762
+ * @param snapId - The snap ID.
1763
+ * @param handlerType - The handler type of the RPC Request.
1764
+ * @param result - The result of the RPC request.
1765
+ */
1766
+ async #assertSnapRpcRequestResult(snapId, handlerType, result) {
1767
+ switch (handlerType) {
1768
+ case HandlerType.OnTransaction: {
1769
+ assertStruct(result, OnTransactionResponseStruct);
1770
+ if (result && hasProperty(result, 'id')) {
1771
+ this.#assertInterfaceExists(snapId, result.id);
1772
+ }
1773
+ break;
1592
1774
  }
1593
- requestQueue.increment(origin);
1594
- try {
1595
- await localStartPromise;
1775
+ case HandlerType.OnSignature: {
1776
+ assertStruct(result, OnSignatureResponseStruct);
1777
+ if (result && hasProperty(result, 'id')) {
1778
+ this.#assertInterfaceExists(snapId, result.id);
1779
+ }
1780
+ break;
1596
1781
  }
1597
- finally {
1598
- requestQueue.decrement(origin);
1599
- // Only delete startPromise for a snap if its value hasn't changed
1600
- if (startPromises.get(snapId) === localStartPromise) {
1601
- startPromises.delete(snapId);
1782
+ case HandlerType.OnHomePage: {
1783
+ assertStruct(result, OnHomePageResponseStruct);
1784
+ if (result && hasProperty(result, 'id')) {
1785
+ this.#assertInterfaceExists(snapId, result.id);
1602
1786
  }
1787
+ break;
1603
1788
  }
1789
+ case HandlerType.OnNameLookup:
1790
+ assertStruct(result, OnNameLookupResponseStruct);
1791
+ break;
1792
+ default:
1793
+ break;
1604
1794
  }
1605
- const timer = new Timer(timeout);
1606
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_recordSnapRpcRequestStart).call(this, snapId, request.id, timer);
1607
- const handleRpcRequestPromise = this.messagingSystem.call('ExecutionService:handleRpcRequest', snapId, { origin, handler: handlerType, request });
1608
- // This will either get the result or reject due to the timeout.
1609
- try {
1610
- const result = await withTimeout(handleRpcRequestPromise, timer);
1611
- if (result === hasTimedOut) {
1612
- throw new Error(`${snapId} failed to respond to the request in time.`);
1613
- }
1614
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertSnapRpcRequestResult).call(this, snapId, handlerType, result);
1615
- const transformedResult = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transformSnapRpcRequestResult).call(this, snapId, handlerType, result);
1616
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_recordSnapRpcRequestFinish).call(this, snapId, request.id);
1617
- return transformedResult;
1795
+ }
1796
+ #recordSnapRpcRequestStart(snapId, requestId, timer) {
1797
+ const runtime = this.#getRuntimeExpect(snapId);
1798
+ runtime.pendingInboundRequests.push({ requestId, timer });
1799
+ runtime.lastRequest = null;
1800
+ }
1801
+ #recordSnapRpcRequestFinish(snapId, requestId) {
1802
+ const runtime = this.#getRuntimeExpect(snapId);
1803
+ runtime.pendingInboundRequests = runtime.pendingInboundRequests.filter((request) => request.requestId !== requestId);
1804
+ if (runtime.pendingInboundRequests.length === 0) {
1805
+ runtime.lastRequest = Date.now();
1618
1806
  }
1619
- catch (error) {
1620
- // We flag the RPC request as finished early since termination may affect pending requests
1621
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_recordSnapRpcRequestFinish).call(this, snapId, request.id);
1622
- const [jsonRpcError, handled] = unwrapError(error);
1623
- if (!handled) {
1624
- await this.stopSnap(snapId, SnapStatusEvents.Crash);
1625
- }
1626
- throw jsonRpcError;
1807
+ }
1808
+ /**
1809
+ * Retrieves the rollback snapshot of a snap.
1810
+ *
1811
+ * @param snapId - The snap id.
1812
+ * @returns A `RollbackSnapshot` or `undefined` if one doesn't exist.
1813
+ */
1814
+ #getRollbackSnapshot(snapId) {
1815
+ return this.#rollbackSnapshots.get(snapId);
1816
+ }
1817
+ /**
1818
+ * Creates a `RollbackSnapshot` that is used to help ensure
1819
+ * atomicity in multiple snap updates.
1820
+ *
1821
+ * @param snapId - The snap id.
1822
+ * @throws {@link Error}. If the snap exists before creation or if creation fails.
1823
+ * @returns A `RollbackSnapshot`.
1824
+ */
1825
+ #createRollbackSnapshot(snapId) {
1826
+ assert(this.#rollbackSnapshots.get(snapId) === undefined, new Error(`Snap "${snapId}" rollback snapshot already exists.`));
1827
+ this.#rollbackSnapshots.set(snapId, {
1828
+ statePatches: [],
1829
+ permissions: {},
1830
+ newVersion: '',
1831
+ });
1832
+ const newRollbackSnapshot = this.#rollbackSnapshots.get(snapId);
1833
+ assert(newRollbackSnapshot !== undefined, new Error(`Snapshot creation failed for ${snapId}.`));
1834
+ return newRollbackSnapshot;
1835
+ }
1836
+ /**
1837
+ * Rolls back a snap to its previous state, permissions
1838
+ * and source code based on the `RollbackSnapshot` that
1839
+ * is captured during the update process. After rolling back,
1840
+ * the function also emits an event indicating that the
1841
+ * snap has been rolled back and it clears the snapshot
1842
+ * for that snap.
1843
+ *
1844
+ * @param snapId - The snap id.
1845
+ * @throws {@link Error}. If a snapshot does not exist.
1846
+ */
1847
+ async #rollbackSnap(snapId) {
1848
+ const rollbackSnapshot = this.#getRollbackSnapshot(snapId);
1849
+ if (!rollbackSnapshot) {
1850
+ throw new Error('A snapshot does not exist for this snap.');
1627
1851
  }
1628
- };
1629
- runtime.rpcHandler = rpcHandler;
1630
- return rpcHandler;
1631
- }, _SnapController_createInterface =
1632
- /**
1633
- * Create a dynamic interface in the SnapInterfaceController.
1634
- *
1635
- * @param snapId - The snap ID.
1636
- * @param content - The initial interface content.
1637
- * @returns An identifier that can be used to identify the interface.
1638
- */
1639
- async function _SnapController_createInterface(snapId, content) {
1640
- return this.messagingSystem.call('SnapInterfaceController:createInterface', snapId, content);
1641
- }, _SnapController_assertInterfaceExists = function _SnapController_assertInterfaceExists(snapId, id) {
1642
- // This will throw if the interface isn't accessible, but we assert nevertheless.
1643
- assert(this.messagingSystem.call('SnapInterfaceController:getInterface', snapId, id));
1644
- }, _SnapController_transformSnapRpcRequestResult =
1645
- /**
1646
- * Transform a RPC request result if necessary.
1647
- *
1648
- * @param snapId - The snap ID of the snap that produced the result.
1649
- * @param handlerType - The handler type that produced the result.
1650
- * @param result - The result.
1651
- * @returns The transformed result if applicable, otherwise the original result.
1652
- */
1653
- async function _SnapController_transformSnapRpcRequestResult(snapId, handlerType, result) {
1654
- switch (handlerType) {
1655
- case HandlerType.OnTransaction:
1656
- case HandlerType.OnSignature:
1657
- case HandlerType.OnHomePage: {
1658
- // Since this type has been asserted earlier we can cast
1659
- const castResult = result;
1660
- // If a handler returns static content, we turn it into a dynamic UI
1661
- if (castResult && hasProperty(castResult, 'content')) {
1662
- const { content, ...rest } = castResult;
1663
- const id = await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_createInterface).call(this, snapId, content);
1664
- return { ...rest, id };
1665
- }
1666
- return result;
1852
+ await this.stopSnap(snapId, SnapStatusEvents.Stop);
1853
+ // Always set to stopped even if it wasn't running initially
1854
+ if (this.get(snapId)?.status !== SnapStatus.Stopped) {
1855
+ this.#transition(snapId, SnapStatusEvents.Stop);
1667
1856
  }
1668
- default:
1669
- return result;
1670
- }
1671
- }, _SnapController_assertSnapRpcRequestResult =
1672
- /**
1673
- * Assert that the returned result of a Snap RPC call is the expected shape.
1674
- *
1675
- * @param snapId - The snap ID.
1676
- * @param handlerType - The handler type of the RPC Request.
1677
- * @param result - The result of the RPC request.
1678
- */
1679
- async function _SnapController_assertSnapRpcRequestResult(snapId, handlerType, result) {
1680
- switch (handlerType) {
1681
- case HandlerType.OnTransaction: {
1682
- assertStruct(result, OnTransactionResponseStruct);
1683
- if (result && hasProperty(result, 'id')) {
1684
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertInterfaceExists).call(this, snapId, result.id);
1685
- }
1686
- break;
1857
+ const { statePatches, permissions } = rollbackSnapshot;
1858
+ if (statePatches?.length) {
1859
+ this.applyPatches(statePatches);
1687
1860
  }
1688
- case HandlerType.OnSignature: {
1689
- assertStruct(result, OnSignatureResponseStruct);
1690
- if (result && hasProperty(result, 'id')) {
1691
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertInterfaceExists).call(this, snapId, result.id);
1692
- }
1693
- break;
1861
+ // Reset snap status, as we may have been in another state when we stored state patches
1862
+ // But now we are 100% in a stopped state
1863
+ if (this.get(snapId)?.status !== SnapStatus.Stopped) {
1864
+ this.update((state) => {
1865
+ state.snaps[snapId].status = SnapStatus.Stopped;
1866
+ });
1694
1867
  }
1695
- case HandlerType.OnHomePage: {
1696
- assertStruct(result, OnHomePageResponseStruct);
1697
- if (result && hasProperty(result, 'id')) {
1698
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_assertInterfaceExists).call(this, snapId, result.id);
1699
- }
1700
- break;
1701
- }
1702
- case HandlerType.OnNameLookup:
1703
- assertStruct(result, OnNameLookupResponseStruct);
1704
- break;
1705
- default:
1706
- break;
1707
- }
1708
- }, _SnapController_recordSnapRpcRequestStart = function _SnapController_recordSnapRpcRequestStart(snapId, requestId, timer) {
1709
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1710
- runtime.pendingInboundRequests.push({ requestId, timer });
1711
- runtime.lastRequest = null;
1712
- }, _SnapController_recordSnapRpcRequestFinish = function _SnapController_recordSnapRpcRequestFinish(snapId, requestId) {
1713
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntimeExpect).call(this, snapId);
1714
- runtime.pendingInboundRequests = runtime.pendingInboundRequests.filter((request) => request.requestId !== requestId);
1715
- if (runtime.pendingInboundRequests.length === 0) {
1716
- runtime.lastRequest = Date.now();
1717
- }
1718
- }, _SnapController_getRollbackSnapshot = function _SnapController_getRollbackSnapshot(snapId) {
1719
- return __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").get(snapId);
1720
- }, _SnapController_createRollbackSnapshot = function _SnapController_createRollbackSnapshot(snapId) {
1721
- assert(__classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").get(snapId) === undefined, new Error(`Snap "${snapId}" rollback snapshot already exists.`));
1722
- __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").set(snapId, {
1723
- statePatches: [],
1724
- permissions: {},
1725
- newVersion: '',
1726
- });
1727
- const newRollbackSnapshot = __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").get(snapId);
1728
- assert(newRollbackSnapshot !== undefined, new Error(`Snapshot creation failed for ${snapId}.`));
1729
- return newRollbackSnapshot;
1730
- }, _SnapController_rollbackSnap =
1731
- /**
1732
- * Rolls back a snap to its previous state, permissions
1733
- * and source code based on the `RollbackSnapshot` that
1734
- * is captured during the update process. After rolling back,
1735
- * the function also emits an event indicating that the
1736
- * snap has been rolled back and it clears the snapshot
1737
- * for that snap.
1738
- *
1739
- * @param snapId - The snap id.
1740
- * @throws {@link Error}. If a snapshot does not exist.
1741
- */
1742
- async function _SnapController_rollbackSnap(snapId) {
1743
- const rollbackSnapshot = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRollbackSnapshot).call(this, snapId);
1744
- if (!rollbackSnapshot) {
1745
- throw new Error('A snapshot does not exist for this snap.');
1746
- }
1747
- await this.stopSnap(snapId, SnapStatusEvents.Stop);
1748
- // Always set to stopped even if it wasn't running initially
1749
- if (this.get(snapId)?.status !== SnapStatus.Stopped) {
1750
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_transition).call(this, snapId, SnapStatusEvents.Stop);
1751
- }
1752
- const { statePatches, permissions } = rollbackSnapshot;
1753
- if (statePatches?.length) {
1754
- this.applyPatches(statePatches);
1755
- }
1756
- // Reset snap status, as we may have been in another state when we stored state patches
1757
- // But now we are 100% in a stopped state
1758
- if (this.get(snapId)?.status !== SnapStatus.Stopped) {
1759
- this.update((state) => {
1760
- state.snaps[snapId].status = SnapStatus.Stopped;
1868
+ this.#updatePermissions({
1869
+ snapId,
1870
+ unusedPermissions: permissions.granted,
1871
+ newPermissions: permissions.revoked,
1872
+ requestData: permissions.requestData,
1761
1873
  });
1874
+ const truncatedSnap = this.getTruncatedExpect(snapId);
1875
+ this.messagingSystem.publish('SnapController:snapRolledback', truncatedSnap, rollbackSnapshot.newVersion);
1876
+ this.#rollbackSnapshots.delete(snapId);
1762
1877
  }
1763
- __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_updatePermissions).call(this, {
1764
- snapId,
1765
- unusedPermissions: permissions.granted,
1766
- newPermissions: permissions.revoked,
1767
- requestData: permissions.requestData,
1768
- });
1769
- const truncatedSnap = this.getTruncatedExpect(snapId);
1770
- this.messagingSystem.publish('SnapController:snapRolledback', truncatedSnap, rollbackSnapshot.newVersion);
1771
- __classPrivateFieldGet(this, _SnapController_rollbackSnapshots, "f").delete(snapId);
1772
- }, _SnapController_rollbackSnaps =
1773
- /**
1774
- * Iterates through an array of snap ids
1775
- * and calls `rollbackSnap` on them.
1776
- *
1777
- * @param snapIds - An array of snap ids.
1778
- */
1779
- async function _SnapController_rollbackSnaps(snapIds) {
1780
- for (const snapId of snapIds) {
1781
- await __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_rollbackSnap).call(this, snapId);
1782
- }
1783
- }, _SnapController_getRuntime = function _SnapController_getRuntime(snapId) {
1784
- return __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").get(snapId);
1785
- }, _SnapController_getRuntimeExpect = function _SnapController_getRuntimeExpect(snapId) {
1786
- const runtime = __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_getRuntime).call(this, snapId);
1787
- assert(runtime !== undefined, new Error(`Snap "${snapId}" runtime data not found`));
1788
- return runtime;
1789
- }, _SnapController_setupRuntime = function _SnapController_setupRuntime(snapId) {
1790
- if (__classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").has(snapId)) {
1791
- return;
1792
- }
1793
- const snap = this.get(snapId);
1794
- const interpreter = interpret(__classPrivateFieldGet(this, _SnapController_statusMachine, "f"));
1795
- interpreter.start({
1796
- context: { snapId },
1797
- value: snap?.status ??
1798
- __classPrivateFieldGet(this, _SnapController_statusMachine, "f").config.initial,
1799
- });
1800
- forceStrict(interpreter);
1801
- __classPrivateFieldGet(this, _SnapController_snapsRuntimeData, "f").set(snapId, {
1802
- lastRequest: null,
1803
- rpcHandler: null,
1804
- installPromise: null,
1805
- encryptionKey: null,
1806
- encryptionSalt: null,
1807
- activeReferences: 0,
1808
- pendingInboundRequests: [],
1809
- pendingOutboundRequests: 0,
1810
- interpreter,
1811
- stopping: false,
1812
- });
1813
- }, _SnapController_calculatePermissionsChange = function _SnapController_calculatePermissionsChange(snapId, desiredPermissionsSet) {
1814
- const oldPermissions = this.messagingSystem.call('PermissionController:getPermissions', snapId) ?? {};
1815
- const newPermissions = permissionsDiff(desiredPermissionsSet, oldPermissions);
1816
- // TODO(ritave): The assumption that these are unused only holds so long as we do not
1817
- // permit dynamic permission requests.
1818
- const unusedPermissions = permissionsDiff(oldPermissions, desiredPermissionsSet);
1819
- // It's a Set Intersection of oldPermissions and desiredPermissionsSet
1820
- // oldPermissions ∖ (oldPermissions ∖ desiredPermissionsSet) ⟺ oldPermissions ∩ desiredPermissionsSet
1821
- const approvedPermissions = permissionsDiff(oldPermissions, unusedPermissions);
1822
- return { newPermissions, unusedPermissions, approvedPermissions };
1823
- }, _SnapController_isSubjectConnectedToSnap = function _SnapController_isSubjectConnectedToSnap(snapId, origin) {
1824
- const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
1825
- const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
1826
- return Boolean(existingCaveat?.value?.[snapId]);
1827
- }, _SnapController_calculateConnectionsChange = function _SnapController_calculateConnectionsChange(snapId, oldConnectionsSet, desiredConnectionsSet) {
1828
- // Filter out any origins that have been revoked since last install/update.
1829
- // That way they will be represented as new.
1830
- const filteredOldConnections = Object.keys(oldConnectionsSet)
1831
- .filter((origin) => __classPrivateFieldGet(this, _SnapController_instances, "m", _SnapController_isSubjectConnectedToSnap).call(this, snapId, origin))
1832
- .reduce((accumulator, origin) => {
1833
- accumulator[origin] = oldConnectionsSet[origin];
1834
- return accumulator;
1835
- }, {});
1836
- const newConnections = setDiff(desiredConnectionsSet, filteredOldConnections);
1837
- const unusedConnections = setDiff(filteredOldConnections, desiredConnectionsSet);
1838
- // It's a Set Intersection of oldConnections and desiredConnectionsSet
1839
- // oldConnections ∖ (oldConnections ∖ desiredConnectionsSet) ⟺ oldConnections ∩ desiredConnectionsSet
1840
- const approvedConnections = setDiff(filteredOldConnections, unusedConnections);
1841
- return { newConnections, unusedConnections, approvedConnections };
1842
- }, _SnapController_updatePermissions = function _SnapController_updatePermissions({ snapId, unusedPermissions = {}, newPermissions = {}, requestData, }) {
1843
- const unusedPermissionsKeys = Object.keys(unusedPermissions);
1844
- if (isNonEmptyArray(unusedPermissionsKeys)) {
1845
- this.messagingSystem.call('PermissionController:revokePermissions', {
1846
- [snapId]: unusedPermissionsKeys,
1847
- });
1878
+ /**
1879
+ * Iterates through an array of snap ids
1880
+ * and calls `rollbackSnap` on them.
1881
+ *
1882
+ * @param snapIds - An array of snap ids.
1883
+ */
1884
+ async #rollbackSnaps(snapIds) {
1885
+ for (const snapId of snapIds) {
1886
+ await this.#rollbackSnap(snapId);
1887
+ }
1848
1888
  }
1849
- if (isNonEmptyArray(Object.keys(newPermissions))) {
1850
- this.messagingSystem.call('PermissionController:grantPermissions', {
1851
- approvedPermissions: newPermissions,
1852
- subject: { origin: snapId },
1853
- requestData,
1889
+ #getRuntime(snapId) {
1890
+ return this.#snapsRuntimeData.get(snapId);
1891
+ }
1892
+ #getRuntimeExpect(snapId) {
1893
+ const runtime = this.#getRuntime(snapId);
1894
+ assert(runtime !== undefined, new Error(`Snap "${snapId}" runtime data not found`));
1895
+ return runtime;
1896
+ }
1897
+ #setupRuntime(snapId) {
1898
+ if (this.#snapsRuntimeData.has(snapId)) {
1899
+ return;
1900
+ }
1901
+ const snap = this.get(snapId);
1902
+ const interpreter = interpret(this.#statusMachine);
1903
+ interpreter.start({
1904
+ context: { snapId },
1905
+ value: snap?.status ??
1906
+ this.#statusMachine.config.initial,
1907
+ });
1908
+ forceStrict(interpreter);
1909
+ this.#snapsRuntimeData.set(snapId, {
1910
+ lastRequest: null,
1911
+ rpcHandler: null,
1912
+ installPromise: null,
1913
+ encryptionKey: null,
1914
+ encryptionSalt: null,
1915
+ activeReferences: 0,
1916
+ pendingInboundRequests: [],
1917
+ pendingOutboundRequests: 0,
1918
+ interpreter,
1919
+ stopping: false,
1854
1920
  });
1855
1921
  }
1856
- }, _SnapController_isValidUpdate = function _SnapController_isValidUpdate(snapId, newVersionRange) {
1857
- const existingSnap = this.getExpect(snapId);
1858
- if (satisfiesVersionRange(existingSnap.version, newVersionRange)) {
1859
- return false;
1922
+ #calculatePermissionsChange(snapId, desiredPermissionsSet) {
1923
+ const oldPermissions = this.messagingSystem.call('PermissionController:getPermissions', snapId) ?? {};
1924
+ const newPermissions = permissionsDiff(desiredPermissionsSet, oldPermissions);
1925
+ // TODO(ritave): The assumption that these are unused only holds so long as we do not
1926
+ // permit dynamic permission requests.
1927
+ const unusedPermissions = permissionsDiff(oldPermissions, desiredPermissionsSet);
1928
+ // It's a Set Intersection of oldPermissions and desiredPermissionsSet
1929
+ // oldPermissions ∖ (oldPermissions ∖ desiredPermissionsSet) ⟺ oldPermissions ∩ desiredPermissionsSet
1930
+ const approvedPermissions = permissionsDiff(oldPermissions, unusedPermissions);
1931
+ return { newPermissions, unusedPermissions, approvedPermissions };
1932
+ }
1933
+ #isSubjectConnectedToSnap(snapId, origin) {
1934
+ const subjectPermissions = this.messagingSystem.call('PermissionController:getPermissions', origin);
1935
+ const existingCaveat = subjectPermissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds);
1936
+ return Boolean(existingCaveat?.value?.[snapId]);
1937
+ }
1938
+ #calculateConnectionsChange(snapId, oldConnectionsSet, desiredConnectionsSet) {
1939
+ // Filter out any origins that have been revoked since last install/update.
1940
+ // That way they will be represented as new.
1941
+ const filteredOldConnections = Object.keys(oldConnectionsSet)
1942
+ .filter((origin) => this.#isSubjectConnectedToSnap(snapId, origin))
1943
+ .reduce((accumulator, origin) => {
1944
+ accumulator[origin] = oldConnectionsSet[origin];
1945
+ return accumulator;
1946
+ }, {});
1947
+ const newConnections = setDiff(desiredConnectionsSet, filteredOldConnections);
1948
+ const unusedConnections = setDiff(filteredOldConnections, desiredConnectionsSet);
1949
+ // It's a Set Intersection of oldConnections and desiredConnectionsSet
1950
+ // oldConnections ∖ (oldConnections ∖ desiredConnectionsSet) ⟺ oldConnections ∩ desiredConnectionsSet
1951
+ const approvedConnections = setDiff(filteredOldConnections, unusedConnections);
1952
+ return { newConnections, unusedConnections, approvedConnections };
1860
1953
  }
1861
- if (gtRange(existingSnap.version, newVersionRange)) {
1862
- return false;
1954
+ /**
1955
+ * Updates the permissions for a snap following an install, update or rollback.
1956
+ *
1957
+ * Grants newly requested permissions and revokes unused/revoked permissions.
1958
+ *
1959
+ * @param args - An options bag.
1960
+ * @param args.snapId - The snap ID.
1961
+ * @param args.newPermissions - New permissions to be granted.
1962
+ * @param args.unusedPermissions - Unused permissions to be revoked.
1963
+ * @param args.requestData - Optional request data from an approval.
1964
+ */
1965
+ #updatePermissions({ snapId, unusedPermissions = {}, newPermissions = {}, requestData, }) {
1966
+ const unusedPermissionsKeys = Object.keys(unusedPermissions);
1967
+ if (isNonEmptyArray(unusedPermissionsKeys)) {
1968
+ this.messagingSystem.call('PermissionController:revokePermissions', {
1969
+ [snapId]: unusedPermissionsKeys,
1970
+ });
1971
+ }
1972
+ if (isNonEmptyArray(Object.keys(newPermissions))) {
1973
+ this.messagingSystem.call('PermissionController:grantPermissions', {
1974
+ approvedPermissions: newPermissions,
1975
+ subject: { origin: snapId },
1976
+ requestData,
1977
+ });
1978
+ }
1863
1979
  }
1864
- return true;
1865
- }, _SnapController_callLifecycleHook =
1866
- /**
1867
- * Call a lifecycle hook on a snap, if the snap has the
1868
- * `endowment:lifecycle-hooks` permission. If the snap does not have the
1869
- * permission, nothing happens.
1870
- *
1871
- * @param origin - The origin.
1872
- * @param snapId - The snap ID.
1873
- * @param handler - The lifecycle hook to call. This should be one of the
1874
- * supported lifecycle hooks.
1875
- * @private
1876
- */
1877
- async function _SnapController_callLifecycleHook(origin, snapId, handler) {
1878
- const permissionName = handlerEndowments[handler];
1879
- assert(permissionName, 'Lifecycle hook must have an endowment.');
1880
- const hasPermission = this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName);
1881
- if (!hasPermission) {
1882
- return;
1883
- }
1884
- await this.handleRequest({
1885
- snapId,
1886
- handler,
1887
- origin,
1888
- request: {
1889
- jsonrpc: '2.0',
1890
- method: handler,
1891
- },
1892
- });
1893
- };
1980
+ /**
1981
+ * Checks if a snap will pass version validation checks
1982
+ * with the new version range that is requested. The first
1983
+ * check that is done is to check if the existing snap version
1984
+ * falls inside the requested range. If it does, we want to return
1985
+ * false because we do not care to create a rollback snapshot in
1986
+ * that scenario. The second check is to ensure that the current
1987
+ * snap version is not greater than all possible versions in
1988
+ * the requested version range. If it is, then we also want
1989
+ * to return false in that scenario.
1990
+ *
1991
+ * @param snapId - The snap id.
1992
+ * @param newVersionRange - The new version range being requested.
1993
+ * @returns `true` if validation checks pass and `false` if they do not.
1994
+ */
1995
+ #isValidUpdate(snapId, newVersionRange) {
1996
+ const existingSnap = this.getExpect(snapId);
1997
+ if (satisfiesVersionRange(existingSnap.version, newVersionRange)) {
1998
+ return false;
1999
+ }
2000
+ if (gtRange(existingSnap.version, newVersionRange)) {
2001
+ return false;
2002
+ }
2003
+ return true;
2004
+ }
2005
+ /**
2006
+ * Call a lifecycle hook on a snap, if the snap has the
2007
+ * `endowment:lifecycle-hooks` permission. If the snap does not have the
2008
+ * permission, nothing happens.
2009
+ *
2010
+ * @param origin - The origin.
2011
+ * @param snapId - The snap ID.
2012
+ * @param handler - The lifecycle hook to call. This should be one of the
2013
+ * supported lifecycle hooks.
2014
+ * @private
2015
+ */
2016
+ async #callLifecycleHook(origin, snapId, handler) {
2017
+ const permissionName = handlerEndowments[handler];
2018
+ assert(permissionName, 'Lifecycle hook must have an endowment.');
2019
+ const hasPermission = this.messagingSystem.call('PermissionController:hasPermission', snapId, permissionName);
2020
+ if (!hasPermission) {
2021
+ return;
2022
+ }
2023
+ await this.handleRequest({
2024
+ snapId,
2025
+ handler,
2026
+ origin,
2027
+ request: {
2028
+ jsonrpc: '2.0',
2029
+ method: handler,
2030
+ },
2031
+ });
2032
+ }
2033
+ }
1894
2034
  //# sourceMappingURL=SnapController.mjs.map