@metamask/snaps-rpc-methods 3.2.0 → 3.3.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.3.0]
10
+ ### Added
11
+ - Add support for unencrypted storage using `snap_manageState` ([#1902](https://github.com/MetaMask/snaps/pull/1902))
12
+
13
+ ## [3.2.1]
14
+ ### Fixed
15
+ - Fix `assertLinksAreSafe` import ([#1908](https://github.com/MetaMask/snaps/pull/1908))
16
+
9
17
  ## [3.2.0]
10
18
  ### Added
11
19
  - Add support for links in custom UI and notifications ([#1814](https://github.com/MetaMask/snaps/pull/1814))
@@ -53,7 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
53
61
  - The version of the package no longer needs to match the version of all other
54
62
  MetaMask Snaps packages.
55
63
 
56
- [Unreleased]: https://github.com/MetaMask/snaps/compare/@metamask/snaps-rpc-methods@3.2.0...HEAD
64
+ [Unreleased]: https://github.com/MetaMask/snaps/compare/@metamask/snaps-rpc-methods@3.3.0...HEAD
65
+ [3.3.0]: https://github.com/MetaMask/snaps/compare/@metamask/snaps-rpc-methods@3.2.1...@metamask/snaps-rpc-methods@3.3.0
66
+ [3.2.1]: https://github.com/MetaMask/snaps/compare/@metamask/snaps-rpc-methods@3.2.0...@metamask/snaps-rpc-methods@3.2.1
57
67
  [3.2.0]: https://github.com/MetaMask/snaps/compare/@metamask/snaps-rpc-methods@3.1.0...@metamask/snaps-rpc-methods@3.2.0
58
68
  [3.1.0]: https://github.com/MetaMask/snaps/compare/@metamask/snaps-rpc-methods@3.0.0...@metamask/snaps-rpc-methods@3.1.0
59
69
  [3.0.0]: https://github.com/MetaMask/snaps/compare/@metamask/snaps-rpc-methods@2.0.0...@metamask/snaps-rpc-methods@3.0.0
@@ -131,36 +131,41 @@ const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)
131
131
  function getManageStateImplementation({ getMnemonic, getUnlockPromise, clearSnapState, getSnapState, updateSnapState, encrypt, decrypt }) {
132
132
  return async function manageState(options) {
133
133
  const { params = {}, method, context: { origin } } = options;
134
- const { operation, newState } = getValidatedParams(params, method);
135
- await getUnlockPromise(true);
136
- const mnemonicPhrase = await getMnemonic();
134
+ const { operation, newState, encrypted } = getValidatedParams(params, method);
135
+ // If the encrypted param is undefined or null we default to true.
136
+ const shouldEncrypt = encrypted ?? true;
137
+ // We only need to prompt the user when the mnemonic is needed
138
+ // which it isn't for the clear operation or unencrypted storage.
139
+ if (shouldEncrypt && operation !== ManageStateOperation.ClearState) {
140
+ await getUnlockPromise(true);
141
+ }
137
142
  switch(operation){
138
143
  case ManageStateOperation.ClearState:
139
- await clearSnapState(origin);
144
+ clearSnapState(origin, shouldEncrypt);
140
145
  return null;
141
146
  case ManageStateOperation.GetState:
142
147
  {
143
- const state = await getSnapState(origin);
148
+ const state = getSnapState(origin, shouldEncrypt);
144
149
  if (state === null) {
145
150
  return state;
146
151
  }
147
- return await decryptState({
152
+ return shouldEncrypt ? await decryptState({
148
153
  state,
149
154
  decryptFunction: decrypt,
150
- mnemonicPhrase,
155
+ mnemonicPhrase: await getMnemonic(),
151
156
  snapId: origin
152
- });
157
+ }) : (0, _snapsutils.parseJson)(state);
153
158
  }
154
159
  case ManageStateOperation.UpdateState:
155
160
  {
156
161
  (0, _utils.assert)(newState);
157
- const encryptedState = await encryptState({
162
+ const finalizedState = shouldEncrypt ? await encryptState({
158
163
  state: newState,
159
164
  encryptFunction: encrypt,
160
- mnemonicPhrase,
165
+ mnemonicPhrase: await getMnemonic(),
161
166
  snapId: origin
162
- });
163
- await updateSnapState(origin, encryptedState);
167
+ }) : JSON.stringify(newState);
168
+ updateSnapState(origin, finalizedState, shouldEncrypt);
164
169
  return null;
165
170
  }
166
171
  default:
@@ -174,12 +179,17 @@ function getValidatedParams(params, method, storageSizeLimit = STORAGE_SIZE_LIMI
174
179
  message: 'Expected params to be a single object.'
175
180
  });
176
181
  }
177
- const { operation, newState } = params;
182
+ const { operation, newState, encrypted } = params;
178
183
  if (!operation || typeof operation !== 'string' || !Object.values(ManageStateOperation).includes(operation)) {
179
184
  throw _rpcerrors.rpcErrors.invalidParams({
180
185
  message: 'Must specify a valid manage state "operation".'
181
186
  });
182
187
  }
188
+ if (encrypted !== undefined && typeof encrypted !== 'boolean') {
189
+ throw _rpcerrors.rpcErrors.invalidParams({
190
+ message: '"encrypted" parameter must be a boolean if specified.'
191
+ });
192
+ }
183
193
  if (operation === ManageStateOperation.UpdateState) {
184
194
  if (!(0, _utils.isObject)(newState)) {
185
195
  throw _rpcerrors.rpcErrors.invalidParams({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/restricted/manageState.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport { STATE_ENCRYPTION_MAGIC_VALUE } from '@metamask/snaps-utils';\nimport type { Json, NonEmptyArray, Hex } from '@metamask/utils';\nimport { isObject, getJsonSize, assert, isValidJson } from '@metamask/utils';\n\nimport type { MethodHooksObject } from '../utils';\nimport { deriveEntropy } from '../utils';\n\n// The salt used for SIP-6-based entropy derivation.\nexport const STATE_ENCRYPTION_SALT = 'snap_manageState encryption';\n\nconst methodName = 'snap_manageState';\n\nexport type ManageStateMethodHooks = {\n /**\n * @returns The mnemonic of the user's primary keyring.\n */\n getMnemonic: () => Promise<Uint8Array>;\n\n /**\n * Waits for the extension to be unlocked.\n *\n * @returns A promise that resolves once the extension is unlocked.\n */\n getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise<void>;\n\n /**\n * A function that clears the state of the requesting Snap.\n */\n clearSnapState: (snapId: string) => Promise<void>;\n\n /**\n * A function that gets the encrypted state of the requesting Snap.\n *\n * @returns The current state of the Snap.\n */\n getSnapState: (snapId: string) => Promise<string>;\n\n /**\n * A function that updates the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n */\n updateSnapState: (snapId: string, newState: string) => Promise<void>;\n\n /**\n * Encrypts data with a key. This is assumed to perform symmetric encryption.\n *\n * @param key - The key to use for encryption, in hexadecimal format.\n * @param data - The JSON data to encrypt.\n * @returns The ciphertext as a string. The format for this string is\n * dependent on the implementation, but MUST be a string.\n */\n encrypt: (key: string, data: Json) => Promise<string>;\n\n /**\n * Decrypts data with a key. This is assumed to perform symmetric decryption.\n *\n * @param key - The key to use for decryption, in hexadecimal format.\n * @param cipherText - The ciphertext to decrypt. The format for this string\n * is dependent on the implementation, but MUST be a string.\n * @returns The decrypted data as a JSON object.\n */\n decrypt: (key: Hex, cipherText: string) => Promise<unknown>;\n};\n\ntype ManageStateSpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: ManageStateMethodHooks;\n};\n\ntype ManageStateSpecification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getManageStateImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_manageState` permission.\n * `snap_manageState` lets the Snap store and manage some of its state on\n * your device.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_manageState` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n ManageStateSpecificationBuilderOptions,\n ManageStateSpecification\n> = ({\n allowedCaveats = null,\n methodHooks,\n}: ManageStateSpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getManageStateImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<ManageStateMethodHooks> = {\n getMnemonic: true,\n getUnlockPromise: true,\n clearSnapState: true,\n getSnapState: true,\n updateSnapState: true,\n encrypt: true,\n decrypt: true,\n};\n\nexport const manageStateBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\nexport enum ManageStateOperation {\n ClearState = 'clear',\n GetState = 'get',\n UpdateState = 'update',\n}\n\nexport type ManageStateArgs = {\n operation: EnumToUnion<ManageStateOperation>;\n newState?: Record<string, Json>;\n};\n\nexport const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)\n\ntype GetEncryptionKeyArgs = {\n snapId: string;\n mnemonicPhrase: Uint8Array;\n};\n\n/**\n * Get a deterministic encryption key to use for encrypting and decrypting the\n * state.\n *\n * This key should only be used for state encryption using `snap_manageState`.\n * To get other encryption keys, a different salt can be used.\n *\n * @param args - The encryption key args.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The state encryption key.\n */\nasync function getEncryptionKey({\n mnemonicPhrase,\n snapId,\n}: GetEncryptionKeyArgs) {\n return await deriveEntropy({\n mnemonicPhrase,\n input: snapId,\n salt: STATE_ENCRYPTION_SALT,\n magic: STATE_ENCRYPTION_MAGIC_VALUE,\n });\n}\n\ntype EncryptStateArgs = GetEncryptionKeyArgs & {\n state: Json;\n encryptFunction: ManageStateMethodHooks['encrypt'];\n};\n\n/**\n * Encrypt the state using a deterministic encryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to encrypt.\n * @param args.encryptFunction - The function to use for encrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function encryptState({\n state,\n encryptFunction,\n ...keyArgs\n}: EncryptStateArgs) {\n const encryptionKey = await getEncryptionKey(keyArgs);\n return await encryptFunction(encryptionKey, state);\n}\n\ntype DecryptStateArgs = GetEncryptionKeyArgs & {\n state: string;\n decryptFunction: ManageStateMethodHooks['decrypt'];\n};\n\n/**\n * Decrypt the state using a deterministic decryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to decrypt.\n * @param args.decryptFunction - The function to use for decrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function decryptState({\n state,\n decryptFunction,\n ...keyArgs\n}: DecryptStateArgs) {\n try {\n const encryptionKey = await getEncryptionKey(keyArgs);\n const decryptedState = await decryptFunction(encryptionKey, state);\n\n assert(isValidJson(decryptedState));\n\n return decryptedState as Record<string, Json>;\n } catch {\n throw rpcErrors.internal({\n message: 'Failed to decrypt snap state, the state must be corrupted.',\n });\n }\n}\n\n/**\n * Builds the method implementation for `snap_manageState`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.clearSnapState - A function that clears the state stored for a\n * snap.\n * @param hooks.getSnapState - A function that fetches the persisted decrypted\n * state for a snap.\n * @param hooks.updateSnapState - A function that updates the state stored for a\n * snap.\n * @param hooks.getMnemonic - A function to retrieve the Secret Recovery Phrase\n * of the user.\n * @param hooks.getUnlockPromise - A function that resolves once the MetaMask\n * extension is unlocked and prompts the user to unlock their MetaMask if it is\n * locked.\n * @param hooks.encrypt - A function that encrypts the given state.\n * @param hooks.decrypt - A function that decrypts the given state.\n * @returns The method implementation which either returns `null` for a\n * successful state update/deletion or returns the decrypted state.\n * @throws If the params are invalid.\n */\nexport function getManageStateImplementation({\n getMnemonic,\n getUnlockPromise,\n clearSnapState,\n getSnapState,\n updateSnapState,\n encrypt,\n decrypt,\n}: ManageStateMethodHooks) {\n return async function manageState(\n options: RestrictedMethodOptions<ManageStateArgs>,\n ): Promise<null | Record<string, Json>> {\n const {\n params = {},\n method,\n context: { origin },\n } = options;\n const { operation, newState } = getValidatedParams(params, method);\n\n await getUnlockPromise(true);\n const mnemonicPhrase = await getMnemonic();\n\n switch (operation) {\n case ManageStateOperation.ClearState:\n await clearSnapState(origin);\n return null;\n\n case ManageStateOperation.GetState: {\n const state = await getSnapState(origin);\n if (state === null) {\n return state;\n }\n return await decryptState({\n state,\n decryptFunction: decrypt,\n mnemonicPhrase,\n snapId: origin,\n });\n }\n\n case ManageStateOperation.UpdateState: {\n assert(newState);\n\n const encryptedState = await encryptState({\n state: newState,\n encryptFunction: encrypt,\n mnemonicPhrase,\n snapId: origin,\n });\n\n await updateSnapState(origin, encryptedState);\n return null;\n }\n\n default:\n throw rpcErrors.invalidParams(\n `Invalid ${method} operation: \"${operation as string}\"`,\n );\n }\n };\n}\n\n/**\n * Validates the manageState method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @param method - RPC method name used for debugging errors.\n * @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(\n params: unknown,\n method: string,\n storageSizeLimit = STORAGE_SIZE_LIMIT,\n): ManageStateArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { operation, newState } = params;\n\n if (\n !operation ||\n typeof operation !== 'string' ||\n !(Object.values(ManageStateOperation) as string[]).includes(operation)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid manage state \"operation\".',\n });\n }\n\n if (operation === ManageStateOperation.UpdateState) {\n if (!isObject(newState)) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be a plain object.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n let size;\n try {\n // `getJsonSize` will throw if the state is not JSON serializable.\n size = getJsonSize(newState);\n } catch {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be JSON serializable.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n if (size > storageSizeLimit) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must not exceed ${storageSizeLimit} bytes in size.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n }\n\n return params as ManageStateArgs;\n}\n"],"names":["STATE_ENCRYPTION_SALT","specificationBuilder","manageStateBuilder","STORAGE_SIZE_LIMIT","getManageStateImplementation","getValidatedParams","methodName","allowedCaveats","methodHooks","permissionType","PermissionType","RestrictedMethod","targetName","methodImplementation","subjectTypes","SubjectType","Snap","getMnemonic","getUnlockPromise","clearSnapState","getSnapState","updateSnapState","encrypt","decrypt","Object","freeze","ManageStateOperation","ClearState","GetState","UpdateState","getEncryptionKey","mnemonicPhrase","snapId","deriveEntropy","input","salt","magic","STATE_ENCRYPTION_MAGIC_VALUE","encryptState","state","encryptFunction","keyArgs","encryptionKey","decryptState","decryptFunction","decryptedState","assert","isValidJson","rpcErrors","internal","message","manageState","options","params","method","context","origin","operation","newState","encryptedState","invalidParams","storageSizeLimit","isObject","values","includes","data","receivedNewState","size","getJsonSize"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/restricted/manageState.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport { STATE_ENCRYPTION_MAGIC_VALUE, parseJson } from '@metamask/snaps-utils';\nimport type { Json, NonEmptyArray, Hex } from '@metamask/utils';\nimport { isObject, getJsonSize, assert, isValidJson } from '@metamask/utils';\n\nimport type { MethodHooksObject } from '../utils';\nimport { deriveEntropy } from '../utils';\n\n// The salt used for SIP-6-based entropy derivation.\nexport const STATE_ENCRYPTION_SALT = 'snap_manageState encryption';\n\nconst methodName = 'snap_manageState';\n\nexport type ManageStateMethodHooks = {\n /**\n * @returns The mnemonic of the user's primary keyring.\n */\n getMnemonic: () => Promise<Uint8Array>;\n\n /**\n * Waits for the extension to be unlocked.\n *\n * @returns A promise that resolves once the extension is unlocked.\n */\n getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise<void>;\n\n /**\n * A function that clears the state of the requesting Snap.\n */\n clearSnapState: (snapId: string, encrypted: boolean) => void;\n\n /**\n * A function that gets the encrypted state of the requesting Snap.\n *\n * @returns The current state of the Snap.\n */\n getSnapState: (snapId: string, encrypted: boolean) => string;\n\n /**\n * A function that updates the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n */\n updateSnapState: (\n snapId: string,\n newState: string,\n encrypted: boolean,\n ) => void;\n\n /**\n * Encrypts data with a key. This is assumed to perform symmetric encryption.\n *\n * @param key - The key to use for encryption, in hexadecimal format.\n * @param data - The JSON data to encrypt.\n * @returns The ciphertext as a string. The format for this string is\n * dependent on the implementation, but MUST be a string.\n */\n encrypt: (key: string, data: Json) => Promise<string>;\n\n /**\n * Decrypts data with a key. This is assumed to perform symmetric decryption.\n *\n * @param key - The key to use for decryption, in hexadecimal format.\n * @param cipherText - The ciphertext to decrypt. The format for this string\n * is dependent on the implementation, but MUST be a string.\n * @returns The decrypted data as a JSON object.\n */\n decrypt: (key: Hex, cipherText: string) => Promise<unknown>;\n};\n\ntype ManageStateSpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: ManageStateMethodHooks;\n};\n\ntype ManageStateSpecification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getManageStateImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_manageState` permission.\n * `snap_manageState` lets the Snap store and manage some of its state on\n * your device.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_manageState` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n ManageStateSpecificationBuilderOptions,\n ManageStateSpecification\n> = ({\n allowedCaveats = null,\n methodHooks,\n}: ManageStateSpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getManageStateImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<ManageStateMethodHooks> = {\n getMnemonic: true,\n getUnlockPromise: true,\n clearSnapState: true,\n getSnapState: true,\n updateSnapState: true,\n encrypt: true,\n decrypt: true,\n};\n\nexport const manageStateBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\nexport enum ManageStateOperation {\n ClearState = 'clear',\n GetState = 'get',\n UpdateState = 'update',\n}\n\nexport type ManageStateArgs = {\n operation: EnumToUnion<ManageStateOperation>;\n newState?: Record<string, Json>;\n encrypted?: boolean;\n};\n\nexport const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)\n\ntype GetEncryptionKeyArgs = {\n snapId: string;\n mnemonicPhrase: Uint8Array;\n};\n\n/**\n * Get a deterministic encryption key to use for encrypting and decrypting the\n * state.\n *\n * This key should only be used for state encryption using `snap_manageState`.\n * To get other encryption keys, a different salt can be used.\n *\n * @param args - The encryption key args.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The state encryption key.\n */\nasync function getEncryptionKey({\n mnemonicPhrase,\n snapId,\n}: GetEncryptionKeyArgs) {\n return await deriveEntropy({\n mnemonicPhrase,\n input: snapId,\n salt: STATE_ENCRYPTION_SALT,\n magic: STATE_ENCRYPTION_MAGIC_VALUE,\n });\n}\n\ntype EncryptStateArgs = GetEncryptionKeyArgs & {\n state: Json;\n encryptFunction: ManageStateMethodHooks['encrypt'];\n};\n\n/**\n * Encrypt the state using a deterministic encryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to encrypt.\n * @param args.encryptFunction - The function to use for encrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function encryptState({\n state,\n encryptFunction,\n ...keyArgs\n}: EncryptStateArgs) {\n const encryptionKey = await getEncryptionKey(keyArgs);\n return await encryptFunction(encryptionKey, state);\n}\n\ntype DecryptStateArgs = GetEncryptionKeyArgs & {\n state: string;\n decryptFunction: ManageStateMethodHooks['decrypt'];\n};\n\n/**\n * Decrypt the state using a deterministic decryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to decrypt.\n * @param args.decryptFunction - The function to use for decrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function decryptState({\n state,\n decryptFunction,\n ...keyArgs\n}: DecryptStateArgs) {\n try {\n const encryptionKey = await getEncryptionKey(keyArgs);\n const decryptedState = await decryptFunction(encryptionKey, state);\n\n assert(isValidJson(decryptedState));\n\n return decryptedState as Record<string, Json>;\n } catch {\n throw rpcErrors.internal({\n message: 'Failed to decrypt snap state, the state must be corrupted.',\n });\n }\n}\n\n/**\n * Builds the method implementation for `snap_manageState`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.clearSnapState - A function that clears the state stored for a\n * snap.\n * @param hooks.getSnapState - A function that fetches the persisted decrypted\n * state for a snap.\n * @param hooks.updateSnapState - A function that updates the state stored for a\n * snap.\n * @param hooks.getMnemonic - A function to retrieve the Secret Recovery Phrase\n * of the user.\n * @param hooks.getUnlockPromise - A function that resolves once the MetaMask\n * extension is unlocked and prompts the user to unlock their MetaMask if it is\n * locked.\n * @param hooks.encrypt - A function that encrypts the given state.\n * @param hooks.decrypt - A function that decrypts the given state.\n * @returns The method implementation which either returns `null` for a\n * successful state update/deletion or returns the decrypted state.\n * @throws If the params are invalid.\n */\nexport function getManageStateImplementation({\n getMnemonic,\n getUnlockPromise,\n clearSnapState,\n getSnapState,\n updateSnapState,\n encrypt,\n decrypt,\n}: ManageStateMethodHooks) {\n return async function manageState(\n options: RestrictedMethodOptions<ManageStateArgs>,\n ): Promise<null | Record<string, Json>> {\n const {\n params = {},\n method,\n context: { origin },\n } = options;\n const { operation, newState, encrypted } = getValidatedParams(\n params,\n method,\n );\n\n // If the encrypted param is undefined or null we default to true.\n const shouldEncrypt = encrypted ?? true;\n\n // We only need to prompt the user when the mnemonic is needed\n // which it isn't for the clear operation or unencrypted storage.\n if (shouldEncrypt && operation !== ManageStateOperation.ClearState) {\n await getUnlockPromise(true);\n }\n\n switch (operation) {\n case ManageStateOperation.ClearState:\n clearSnapState(origin, shouldEncrypt);\n return null;\n\n case ManageStateOperation.GetState: {\n const state = getSnapState(origin, shouldEncrypt);\n if (state === null) {\n return state;\n }\n return shouldEncrypt\n ? await decryptState({\n state,\n decryptFunction: decrypt,\n mnemonicPhrase: await getMnemonic(),\n snapId: origin,\n })\n : parseJson<Record<string, Json>>(state);\n }\n\n case ManageStateOperation.UpdateState: {\n assert(newState);\n\n const finalizedState = shouldEncrypt\n ? await encryptState({\n state: newState,\n encryptFunction: encrypt,\n mnemonicPhrase: await getMnemonic(),\n snapId: origin,\n })\n : JSON.stringify(newState);\n\n updateSnapState(origin, finalizedState, shouldEncrypt);\n return null;\n }\n\n default:\n throw rpcErrors.invalidParams(\n `Invalid ${method} operation: \"${operation as string}\"`,\n );\n }\n };\n}\n\n/**\n * Validates the manageState method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @param method - RPC method name used for debugging errors.\n * @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(\n params: unknown,\n method: string,\n storageSizeLimit = STORAGE_SIZE_LIMIT,\n): ManageStateArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { operation, newState, encrypted } = params;\n\n if (\n !operation ||\n typeof operation !== 'string' ||\n !(Object.values(ManageStateOperation) as string[]).includes(operation)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid manage state \"operation\".',\n });\n }\n\n if (encrypted !== undefined && typeof encrypted !== 'boolean') {\n throw rpcErrors.invalidParams({\n message: '\"encrypted\" parameter must be a boolean if specified.',\n });\n }\n\n if (operation === ManageStateOperation.UpdateState) {\n if (!isObject(newState)) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be a plain object.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n let size;\n try {\n // `getJsonSize` will throw if the state is not JSON serializable.\n size = getJsonSize(newState);\n } catch {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be JSON serializable.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n if (size > storageSizeLimit) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must not exceed ${storageSizeLimit} bytes in size.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n }\n\n return params as ManageStateArgs;\n}\n"],"names":["STATE_ENCRYPTION_SALT","specificationBuilder","manageStateBuilder","STORAGE_SIZE_LIMIT","getManageStateImplementation","getValidatedParams","methodName","allowedCaveats","methodHooks","permissionType","PermissionType","RestrictedMethod","targetName","methodImplementation","subjectTypes","SubjectType","Snap","getMnemonic","getUnlockPromise","clearSnapState","getSnapState","updateSnapState","encrypt","decrypt","Object","freeze","ManageStateOperation","ClearState","GetState","UpdateState","getEncryptionKey","mnemonicPhrase","snapId","deriveEntropy","input","salt","magic","STATE_ENCRYPTION_MAGIC_VALUE","encryptState","state","encryptFunction","keyArgs","encryptionKey","decryptState","decryptFunction","decryptedState","assert","isValidJson","rpcErrors","internal","message","manageState","options","params","method","context","origin","operation","newState","encrypted","shouldEncrypt","parseJson","finalizedState","JSON","stringify","invalidParams","storageSizeLimit","isObject","values","includes","undefined","data","receivedNewState","size","getJsonSize"],"mappings":";;;;;;;;;;;;;;IAgBaA,qBAAqB;eAArBA;;IAmFAC,oBAAoB;eAApBA;;IA2BAC,kBAAkB;eAAlBA;;IAkBAC,kBAAkB;eAAlBA;;IAmHGC,4BAA4B;eAA5BA;;IAoFAC,kBAAkB;eAAlBA;;;sCAlV4B;2BAClB;4BAE8B;uBAEG;wBAG7B;AAGvB,MAAML,wBAAwB;AAErC,MAAMM,aAAa;AAiFZ,MAAML,uBAIT,CAAC,EACHM,iBAAiB,IAAI,EACrBC,WAAW,EAC4B;IACvC,OAAO;QACLC,gBAAgBC,oCAAc,CAACC,gBAAgB;QAC/CC,YAAYN;QACZC;QACAM,sBAAsBT,6BAA6BI;QACnDM,cAAc;YAACC,iCAAW,CAACC,IAAI;SAAC;IAClC;AACF;AAEA,MAAMR,cAAyD;IAC7DS,aAAa;IACbC,kBAAkB;IAClBC,gBAAgB;IAChBC,cAAc;IACdC,iBAAiB;IACjBC,SAAS;IACTC,SAAS;AACX;AAEO,MAAMrB,qBAAqBsB,OAAOC,MAAM,CAAC;IAC9Cb,YAAYN;IACZL;IACAO;AACF;IAEO;UAAKkB,oBAAoB;IAApBA,qBACVC,gBAAa;IADHD,qBAEVE,cAAW;IAFDF,qBAGVG,iBAAc;GAHJH,yBAAAA;AAYL,MAAMvB,qBAAqB,WAAW,mBAAmB;AAOhE;;;;;;;;;;;;CAYC,GACD,eAAe2B,iBAAiB,EAC9BC,cAAc,EACdC,MAAM,EACe;IACrB,OAAO,MAAMC,IAAAA,qBAAa,EAAC;QACzBF;QACAG,OAAOF;QACPG,MAAMnC;QACNoC,OAAOC,wCAA4B;IACrC;AACF;AAOA;;;;;;;;;;;CAWC,GACD,eAAeC,aAAa,EAC1BC,KAAK,EACLC,eAAe,EACf,GAAGC,SACc;IACjB,MAAMC,gBAAgB,MAAMZ,iBAAiBW;IAC7C,OAAO,MAAMD,gBAAgBE,eAAeH;AAC9C;AAOA;;;;;;;;;;;CAWC,GACD,eAAeI,aAAa,EAC1BJ,KAAK,EACLK,eAAe,EACf,GAAGH,SACc;IACjB,IAAI;QACF,MAAMC,gBAAgB,MAAMZ,iBAAiBW;QAC7C,MAAMI,iBAAiB,MAAMD,gBAAgBF,eAAeH;QAE5DO,IAAAA,aAAM,EAACC,IAAAA,kBAAW,EAACF;QAEnB,OAAOA;IACT,EAAE,OAAM;QACN,MAAMG,oBAAS,CAACC,QAAQ,CAAC;YACvBC,SAAS;QACX;IACF;AACF;AAuBO,SAAS9C,6BAA6B,EAC3Ca,WAAW,EACXC,gBAAgB,EAChBC,cAAc,EACdC,YAAY,EACZC,eAAe,EACfC,OAAO,EACPC,OAAO,EACgB;IACvB,OAAO,eAAe4B,YACpBC,OAAiD;QAEjD,MAAM,EACJC,SAAS,CAAC,CAAC,EACXC,MAAM,EACNC,SAAS,EAAEC,MAAM,EAAE,EACpB,GAAGJ;QACJ,MAAM,EAAEK,SAAS,EAAEC,QAAQ,EAAEC,SAAS,EAAE,GAAGtD,mBACzCgD,QACAC;QAGF,kEAAkE;QAClE,MAAMM,gBAAgBD,aAAa;QAEnC,8DAA8D;QAC9D,iEAAiE;QACjE,IAAIC,iBAAiBH,cAAc/B,qBAAqBC,UAAU,EAAE;YAClE,MAAMT,iBAAiB;QACzB;QAEA,OAAQuC;YACN,KAAK/B,qBAAqBC,UAAU;gBAClCR,eAAeqC,QAAQI;gBACvB,OAAO;YAET,KAAKlC,qBAAqBE,QAAQ;gBAAE;oBAClC,MAAMW,QAAQnB,aAAaoC,QAAQI;oBACnC,IAAIrB,UAAU,MAAM;wBAClB,OAAOA;oBACT;oBACA,OAAOqB,gBACH,MAAMjB,aAAa;wBACjBJ;wBACAK,iBAAiBrB;wBACjBQ,gBAAgB,MAAMd;wBACtBe,QAAQwB;oBACV,KACAK,IAAAA,qBAAS,EAAuBtB;gBACtC;YAEA,KAAKb,qBAAqBG,WAAW;gBAAE;oBACrCiB,IAAAA,aAAM,EAACY;oBAEP,MAAMI,iBAAiBF,gBACnB,MAAMtB,aAAa;wBACjBC,OAAOmB;wBACPlB,iBAAiBlB;wBACjBS,gBAAgB,MAAMd;wBACtBe,QAAQwB;oBACV,KACAO,KAAKC,SAAS,CAACN;oBAEnBrC,gBAAgBmC,QAAQM,gBAAgBF;oBACxC,OAAO;gBACT;YAEA;gBACE,MAAMZ,oBAAS,CAACiB,aAAa,CAC3B,CAAC,QAAQ,EAAEX,OAAO,aAAa,EAAEG,UAAoB,CAAC,CAAC;QAE7D;IACF;AACF;AAWO,SAASpD,mBACdgD,MAAe,EACfC,MAAc,EACdY,mBAAmB/D,kBAAkB;IAErC,IAAI,CAACgE,IAAAA,eAAQ,EAACd,SAAS;QACrB,MAAML,oBAAS,CAACiB,aAAa,CAAC;YAC5Bf,SAAS;QACX;IACF;IAEA,MAAM,EAAEO,SAAS,EAAEC,QAAQ,EAAEC,SAAS,EAAE,GAAGN;IAE3C,IACE,CAACI,aACD,OAAOA,cAAc,YACrB,CAAC,AAACjC,OAAO4C,MAAM,CAAC1C,sBAAmC2C,QAAQ,CAACZ,YAC5D;QACA,MAAMT,oBAAS,CAACiB,aAAa,CAAC;YAC5Bf,SAAS;QACX;IACF;IAEA,IAAIS,cAAcW,aAAa,OAAOX,cAAc,WAAW;QAC7D,MAAMX,oBAAS,CAACiB,aAAa,CAAC;YAC5Bf,SAAS;QACX;IACF;IAEA,IAAIO,cAAc/B,qBAAqBG,WAAW,EAAE;QAClD,IAAI,CAACsC,IAAAA,eAAQ,EAACT,WAAW;YACvB,MAAMV,oBAAS,CAACiB,aAAa,CAAC;gBAC5Bf,SAAS,CAAC,QAAQ,EAAEI,OAAO,+DAA+D,CAAC;gBAC3FiB,MAAM;oBACJC,kBACE,OAAOd,aAAa,cAAc,cAAcA;gBACpD;YACF;QACF;QAEA,IAAIe;QACJ,IAAI;YACF,kEAAkE;YAClEA,OAAOC,IAAAA,kBAAW,EAAChB;QACrB,EAAE,OAAM;YACN,MAAMV,oBAAS,CAACiB,aAAa,CAAC;gBAC5Bf,SAAS,CAAC,QAAQ,EAAEI,OAAO,kEAAkE,CAAC;gBAC9FiB,MAAM;oBACJC,kBACE,OAAOd,aAAa,cAAc,cAAcA;gBACpD;YACF;QACF;QAEA,IAAIe,OAAOP,kBAAkB;YAC3B,MAAMlB,oBAAS,CAACiB,aAAa,CAAC;gBAC5Bf,SAAS,CAAC,QAAQ,EAAEI,OAAO,wDAAwD,EAAEY,iBAAiB,eAAe,CAAC;gBACtHK,MAAM;oBACJC,kBACE,OAAOd,aAAa,cAAc,cAAcA;gBACpD;YACF;QACF;IACF;IAEA,OAAOL;AACT"}
@@ -27,7 +27,7 @@ _export(exports, {
27
27
  });
28
28
  const _permissioncontroller = require("@metamask/permission-controller");
29
29
  const _rpcerrors = require("@metamask/rpc-errors");
30
- const _snapsutils = require("@metamask/snaps-utils");
30
+ const _snapsui = require("@metamask/snaps-ui");
31
31
  const _utils = require("@metamask/utils");
32
32
  const methodName = 'snap_notify';
33
33
  var NotificationType;
@@ -62,7 +62,7 @@ function getImplementation({ showNativeNotification, showInAppNotification, isOn
62
62
  const { params, context: { origin } } = args;
63
63
  const validatedParams = getValidatedParams(params);
64
64
  await maybeUpdatePhishingList();
65
- (0, _snapsutils.assertLinksAreSafe)(validatedParams.message, isOnPhishingList);
65
+ (0, _snapsui.assertLinksAreSafe)(validatedParams.message, isOnPhishingList);
66
66
  switch(validatedParams.type){
67
67
  case NotificationType.Native:
68
68
  return await showNativeNotification(origin, validatedParams);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/restricted/notify.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport { assertLinksAreSafe } from '@metamask/snaps-utils';\nimport type { NonEmptyArray } from '@metamask/utils';\nimport { isObject } from '@metamask/utils';\n\nimport { type MethodHooksObject } from '../utils';\n\nconst methodName = 'snap_notify';\n\n// TODO: Move all the types to a shared place when implementing more\n// notifications.\nexport enum NotificationType {\n InApp = 'inApp',\n Native = 'native',\n}\n\nexport type NotificationArgs = {\n /**\n * Enum type to determine notification type.\n */\n type: EnumToUnion<NotificationType>;\n\n /**\n * A message to show on the notification.\n */\n message: string;\n};\n\nexport type NotifyMethodHooks = {\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showNativeNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showInAppNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n isOnPhishingList: (url: string) => boolean;\n\n maybeUpdatePhishingList: () => Promise<void>;\n};\n\ntype SpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: NotifyMethodHooks;\n};\n\ntype Specification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_notify` permission.\n * `snap_notify` allows snaps to send multiple types of notifications to its users.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_notify` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n SpecificationBuilderOptions,\n Specification\n> = ({ allowedCaveats = null, methodHooks }: SpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<NotifyMethodHooks> = {\n showNativeNotification: true,\n showInAppNotification: true,\n isOnPhishingList: true,\n maybeUpdatePhishingList: true,\n};\n\nexport const notifyBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\n/**\n * Builds the method implementation for `snap_notify`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.showNativeNotification - A function that shows a native browser notification.\n * @param hooks.showInAppNotification - A function that shows a notification in the MetaMask UI.\n * @param hooks.isOnPhishingList - A function that checks for links against the phishing list.\n * @param hooks.maybeUpdatePhishingList - A function that updates the phishing list if needed.\n * @returns The method implementation which returns `null` on success.\n * @throws If the params are invalid.\n */\nexport function getImplementation({\n showNativeNotification,\n showInAppNotification,\n isOnPhishingList,\n maybeUpdatePhishingList,\n}: NotifyMethodHooks) {\n return async function implementation(\n args: RestrictedMethodOptions<NotificationArgs>,\n ): Promise<null> {\n const {\n params,\n context: { origin },\n } = args;\n\n const validatedParams = getValidatedParams(params);\n\n await maybeUpdatePhishingList();\n\n assertLinksAreSafe(validatedParams.message, isOnPhishingList);\n\n switch (validatedParams.type) {\n case NotificationType.Native:\n return await showNativeNotification(origin, validatedParams);\n case NotificationType.InApp:\n return await showInAppNotification(origin, validatedParams);\n default:\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n };\n}\n\n/**\n * Validates the notify method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(params: unknown): NotificationArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { type, message } = params;\n\n if (\n !type ||\n typeof type !== 'string' ||\n !Object.values(NotificationType).includes(type as NotificationType)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n\n // Set to the max message length on a Mac notification for now.\n if (!message || typeof message !== 'string' || message.length >= 50) {\n throw rpcErrors.invalidParams({\n message:\n 'Must specify a non-empty string \"message\" less than 50 characters long.',\n });\n }\n\n return params as NotificationArgs;\n}\n"],"names":["specificationBuilder","notifyBuilder","getImplementation","getValidatedParams","methodName","NotificationType","InApp","Native","allowedCaveats","methodHooks","permissionType","PermissionType","RestrictedMethod","targetName","methodImplementation","subjectTypes","SubjectType","Snap","showNativeNotification","showInAppNotification","isOnPhishingList","maybeUpdatePhishingList","Object","freeze","implementation","args","params","context","origin","validatedParams","assertLinksAreSafe","message","type","rpcErrors","invalidParams","isObject","values","includes","length"],"mappings":";;;;;;;;;;;;;;IAgFaA,oBAAoB;eAApBA;;IAqBAC,aAAa;eAAbA;;IAiBGC,iBAAiB;eAAjBA;;IAwCAC,kBAAkB;eAAlBA;;;sCAzJ4B;2BAClB;4BAES;uBAEV;AAIzB,MAAMC,aAAa;IAIZ;UAAKC,gBAAgB;IAAhBA,iBACVC,WAAQ;IADED,iBAEVE,YAAS;GAFCF,qBAAAA;AA8DL,MAAML,uBAIT,CAAC,EAAEQ,iBAAiB,IAAI,EAAEC,WAAW,EAA+B;IACtE,OAAO;QACLC,gBAAgBC,oCAAc,CAACC,gBAAgB;QAC/CC,YAAYT;QACZI;QACAM,sBAAsBZ,kBAAkBO;QACxCM,cAAc;YAACC,iCAAW,CAACC,IAAI;SAAC;IAClC;AACF;AAEA,MAAMR,cAAoD;IACxDS,wBAAwB;IACxBC,uBAAuB;IACvBC,kBAAkB;IAClBC,yBAAyB;AAC3B;AAEO,MAAMpB,gBAAgBqB,OAAOC,MAAM,CAAC;IACzCV,YAAYT;IACZJ;IACAS;AACF;AAaO,SAASP,kBAAkB,EAChCgB,sBAAsB,EACtBC,qBAAqB,EACrBC,gBAAgB,EAChBC,uBAAuB,EACL;IAClB,OAAO,eAAeG,eACpBC,IAA+C;QAE/C,MAAM,EACJC,MAAM,EACNC,SAAS,EAAEC,MAAM,EAAE,EACpB,GAAGH;QAEJ,MAAMI,kBAAkB1B,mBAAmBuB;QAE3C,MAAML;QAENS,IAAAA,8BAAkB,EAACD,gBAAgBE,OAAO,EAAEX;QAE5C,OAAQS,gBAAgBG,IAAI;YAC1B,KAAK3B,iBAAiBE,MAAM;gBAC1B,OAAO,MAAMW,uBAAuBU,QAAQC;YAC9C,KAAKxB,iBAAiBC,KAAK;gBACzB,OAAO,MAAMa,sBAAsBS,QAAQC;YAC7C;gBACE,MAAMI,oBAAS,CAACC,aAAa,CAAC;oBAC5BH,SAAS;gBACX;QACJ;IACF;AACF;AASO,SAAS5B,mBAAmBuB,MAAe;IAChD,IAAI,CAACS,IAAAA,eAAQ,EAACT,SAAS;QACrB,MAAMO,oBAAS,CAACC,aAAa,CAAC;YAC5BH,SAAS;QACX;IACF;IAEA,MAAM,EAAEC,IAAI,EAAED,OAAO,EAAE,GAAGL;IAE1B,IACE,CAACM,QACD,OAAOA,SAAS,YAChB,CAACV,OAAOc,MAAM,CAAC/B,kBAAkBgC,QAAQ,CAACL,OAC1C;QACA,MAAMC,oBAAS,CAACC,aAAa,CAAC;YAC5BH,SAAS;QACX;IACF;IAEA,+DAA+D;IAC/D,IAAI,CAACA,WAAW,OAAOA,YAAY,YAAYA,QAAQO,MAAM,IAAI,IAAI;QACnE,MAAML,oBAAS,CAACC,aAAa,CAAC;YAC5BH,SACE;QACJ;IACF;IAEA,OAAOL;AACT"}
1
+ {"version":3,"sources":["../../../src/restricted/notify.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport { assertLinksAreSafe } from '@metamask/snaps-ui';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport type { NonEmptyArray } from '@metamask/utils';\nimport { isObject } from '@metamask/utils';\n\nimport { type MethodHooksObject } from '../utils';\n\nconst methodName = 'snap_notify';\n\n// TODO: Move all the types to a shared place when implementing more\n// notifications.\nexport enum NotificationType {\n InApp = 'inApp',\n Native = 'native',\n}\n\nexport type NotificationArgs = {\n /**\n * Enum type to determine notification type.\n */\n type: EnumToUnion<NotificationType>;\n\n /**\n * A message to show on the notification.\n */\n message: string;\n};\n\nexport type NotifyMethodHooks = {\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showNativeNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showInAppNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n isOnPhishingList: (url: string) => boolean;\n\n maybeUpdatePhishingList: () => Promise<void>;\n};\n\ntype SpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: NotifyMethodHooks;\n};\n\ntype Specification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_notify` permission.\n * `snap_notify` allows snaps to send multiple types of notifications to its users.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_notify` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n SpecificationBuilderOptions,\n Specification\n> = ({ allowedCaveats = null, methodHooks }: SpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<NotifyMethodHooks> = {\n showNativeNotification: true,\n showInAppNotification: true,\n isOnPhishingList: true,\n maybeUpdatePhishingList: true,\n};\n\nexport const notifyBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\n/**\n * Builds the method implementation for `snap_notify`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.showNativeNotification - A function that shows a native browser notification.\n * @param hooks.showInAppNotification - A function that shows a notification in the MetaMask UI.\n * @param hooks.isOnPhishingList - A function that checks for links against the phishing list.\n * @param hooks.maybeUpdatePhishingList - A function that updates the phishing list if needed.\n * @returns The method implementation which returns `null` on success.\n * @throws If the params are invalid.\n */\nexport function getImplementation({\n showNativeNotification,\n showInAppNotification,\n isOnPhishingList,\n maybeUpdatePhishingList,\n}: NotifyMethodHooks) {\n return async function implementation(\n args: RestrictedMethodOptions<NotificationArgs>,\n ): Promise<null> {\n const {\n params,\n context: { origin },\n } = args;\n\n const validatedParams = getValidatedParams(params);\n\n await maybeUpdatePhishingList();\n\n assertLinksAreSafe(validatedParams.message, isOnPhishingList);\n\n switch (validatedParams.type) {\n case NotificationType.Native:\n return await showNativeNotification(origin, validatedParams);\n case NotificationType.InApp:\n return await showInAppNotification(origin, validatedParams);\n default:\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n };\n}\n\n/**\n * Validates the notify method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(params: unknown): NotificationArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { type, message } = params;\n\n if (\n !type ||\n typeof type !== 'string' ||\n !Object.values(NotificationType).includes(type as NotificationType)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n\n // Set to the max message length on a Mac notification for now.\n if (!message || typeof message !== 'string' || message.length >= 50) {\n throw rpcErrors.invalidParams({\n message:\n 'Must specify a non-empty string \"message\" less than 50 characters long.',\n });\n }\n\n return params as NotificationArgs;\n}\n"],"names":["specificationBuilder","notifyBuilder","getImplementation","getValidatedParams","methodName","NotificationType","InApp","Native","allowedCaveats","methodHooks","permissionType","PermissionType","RestrictedMethod","targetName","methodImplementation","subjectTypes","SubjectType","Snap","showNativeNotification","showInAppNotification","isOnPhishingList","maybeUpdatePhishingList","Object","freeze","implementation","args","params","context","origin","validatedParams","assertLinksAreSafe","message","type","rpcErrors","invalidParams","isObject","values","includes","length"],"mappings":";;;;;;;;;;;;;;IAgFaA,oBAAoB;eAApBA;;IAqBAC,aAAa;eAAbA;;IAiBGC,iBAAiB;eAAjBA;;IAwCAC,kBAAkB;eAAlBA;;;sCAzJ4B;2BAClB;yBACS;uBAGV;AAIzB,MAAMC,aAAa;IAIZ;UAAKC,gBAAgB;IAAhBA,iBACVC,WAAQ;IADED,iBAEVE,YAAS;GAFCF,qBAAAA;AA8DL,MAAML,uBAIT,CAAC,EAAEQ,iBAAiB,IAAI,EAAEC,WAAW,EAA+B;IACtE,OAAO;QACLC,gBAAgBC,oCAAc,CAACC,gBAAgB;QAC/CC,YAAYT;QACZI;QACAM,sBAAsBZ,kBAAkBO;QACxCM,cAAc;YAACC,iCAAW,CAACC,IAAI;SAAC;IAClC;AACF;AAEA,MAAMR,cAAoD;IACxDS,wBAAwB;IACxBC,uBAAuB;IACvBC,kBAAkB;IAClBC,yBAAyB;AAC3B;AAEO,MAAMpB,gBAAgBqB,OAAOC,MAAM,CAAC;IACzCV,YAAYT;IACZJ;IACAS;AACF;AAaO,SAASP,kBAAkB,EAChCgB,sBAAsB,EACtBC,qBAAqB,EACrBC,gBAAgB,EAChBC,uBAAuB,EACL;IAClB,OAAO,eAAeG,eACpBC,IAA+C;QAE/C,MAAM,EACJC,MAAM,EACNC,SAAS,EAAEC,MAAM,EAAE,EACpB,GAAGH;QAEJ,MAAMI,kBAAkB1B,mBAAmBuB;QAE3C,MAAML;QAENS,IAAAA,2BAAkB,EAACD,gBAAgBE,OAAO,EAAEX;QAE5C,OAAQS,gBAAgBG,IAAI;YAC1B,KAAK3B,iBAAiBE,MAAM;gBAC1B,OAAO,MAAMW,uBAAuBU,QAAQC;YAC9C,KAAKxB,iBAAiBC,KAAK;gBACzB,OAAO,MAAMa,sBAAsBS,QAAQC;YAC7C;gBACE,MAAMI,oBAAS,CAACC,aAAa,CAAC;oBAC5BH,SAAS;gBACX;QACJ;IACF;AACF;AASO,SAAS5B,mBAAmBuB,MAAe;IAChD,IAAI,CAACS,IAAAA,eAAQ,EAACT,SAAS;QACrB,MAAMO,oBAAS,CAACC,aAAa,CAAC;YAC5BH,SAAS;QACX;IACF;IAEA,MAAM,EAAEC,IAAI,EAAED,OAAO,EAAE,GAAGL;IAE1B,IACE,CAACM,QACD,OAAOA,SAAS,YAChB,CAACV,OAAOc,MAAM,CAAC/B,kBAAkBgC,QAAQ,CAACL,OAC1C;QACA,MAAMC,oBAAS,CAACC,aAAa,CAAC;YAC5BH,SAAS;QACX;IACF;IAEA,+DAA+D;IAC/D,IAAI,CAACA,WAAW,OAAOA,YAAY,YAAYA,QAAQO,MAAM,IAAI,IAAI;QACnE,MAAML,oBAAS,CAACC,aAAa,CAAC;YAC5BH,SACE;QACJ;IACF;IAEA,OAAOL;AACT"}
@@ -1,6 +1,6 @@
1
1
  import { PermissionType, SubjectType } from '@metamask/permission-controller';
2
2
  import { rpcErrors } from '@metamask/rpc-errors';
3
- import { STATE_ENCRYPTION_MAGIC_VALUE } from '@metamask/snaps-utils';
3
+ import { STATE_ENCRYPTION_MAGIC_VALUE, parseJson } from '@metamask/snaps-utils';
4
4
  import { isObject, getJsonSize, assert, isValidJson } from '@metamask/utils';
5
5
  import { deriveEntropy } from '../utils';
6
6
  // The salt used for SIP-6-based entropy derivation.
@@ -128,36 +128,41 @@ export const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)
128
128
  */ export function getManageStateImplementation({ getMnemonic, getUnlockPromise, clearSnapState, getSnapState, updateSnapState, encrypt, decrypt }) {
129
129
  return async function manageState(options) {
130
130
  const { params = {}, method, context: { origin } } = options;
131
- const { operation, newState } = getValidatedParams(params, method);
132
- await getUnlockPromise(true);
133
- const mnemonicPhrase = await getMnemonic();
131
+ const { operation, newState, encrypted } = getValidatedParams(params, method);
132
+ // If the encrypted param is undefined or null we default to true.
133
+ const shouldEncrypt = encrypted ?? true;
134
+ // We only need to prompt the user when the mnemonic is needed
135
+ // which it isn't for the clear operation or unencrypted storage.
136
+ if (shouldEncrypt && operation !== ManageStateOperation.ClearState) {
137
+ await getUnlockPromise(true);
138
+ }
134
139
  switch(operation){
135
140
  case ManageStateOperation.ClearState:
136
- await clearSnapState(origin);
141
+ clearSnapState(origin, shouldEncrypt);
137
142
  return null;
138
143
  case ManageStateOperation.GetState:
139
144
  {
140
- const state = await getSnapState(origin);
145
+ const state = getSnapState(origin, shouldEncrypt);
141
146
  if (state === null) {
142
147
  return state;
143
148
  }
144
- return await decryptState({
149
+ return shouldEncrypt ? await decryptState({
145
150
  state,
146
151
  decryptFunction: decrypt,
147
- mnemonicPhrase,
152
+ mnemonicPhrase: await getMnemonic(),
148
153
  snapId: origin
149
- });
154
+ }) : parseJson(state);
150
155
  }
151
156
  case ManageStateOperation.UpdateState:
152
157
  {
153
158
  assert(newState);
154
- const encryptedState = await encryptState({
159
+ const finalizedState = shouldEncrypt ? await encryptState({
155
160
  state: newState,
156
161
  encryptFunction: encrypt,
157
- mnemonicPhrase,
162
+ mnemonicPhrase: await getMnemonic(),
158
163
  snapId: origin
159
- });
160
- await updateSnapState(origin, encryptedState);
164
+ }) : JSON.stringify(newState);
165
+ updateSnapState(origin, finalizedState, shouldEncrypt);
161
166
  return null;
162
167
  }
163
168
  default:
@@ -179,12 +184,17 @@ export const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)
179
184
  message: 'Expected params to be a single object.'
180
185
  });
181
186
  }
182
- const { operation, newState } = params;
187
+ const { operation, newState, encrypted } = params;
183
188
  if (!operation || typeof operation !== 'string' || !Object.values(ManageStateOperation).includes(operation)) {
184
189
  throw rpcErrors.invalidParams({
185
190
  message: 'Must specify a valid manage state "operation".'
186
191
  });
187
192
  }
193
+ if (encrypted !== undefined && typeof encrypted !== 'boolean') {
194
+ throw rpcErrors.invalidParams({
195
+ message: '"encrypted" parameter must be a boolean if specified.'
196
+ });
197
+ }
188
198
  if (operation === ManageStateOperation.UpdateState) {
189
199
  if (!isObject(newState)) {
190
200
  throw rpcErrors.invalidParams({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/restricted/manageState.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport { STATE_ENCRYPTION_MAGIC_VALUE } from '@metamask/snaps-utils';\nimport type { Json, NonEmptyArray, Hex } from '@metamask/utils';\nimport { isObject, getJsonSize, assert, isValidJson } from '@metamask/utils';\n\nimport type { MethodHooksObject } from '../utils';\nimport { deriveEntropy } from '../utils';\n\n// The salt used for SIP-6-based entropy derivation.\nexport const STATE_ENCRYPTION_SALT = 'snap_manageState encryption';\n\nconst methodName = 'snap_manageState';\n\nexport type ManageStateMethodHooks = {\n /**\n * @returns The mnemonic of the user's primary keyring.\n */\n getMnemonic: () => Promise<Uint8Array>;\n\n /**\n * Waits for the extension to be unlocked.\n *\n * @returns A promise that resolves once the extension is unlocked.\n */\n getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise<void>;\n\n /**\n * A function that clears the state of the requesting Snap.\n */\n clearSnapState: (snapId: string) => Promise<void>;\n\n /**\n * A function that gets the encrypted state of the requesting Snap.\n *\n * @returns The current state of the Snap.\n */\n getSnapState: (snapId: string) => Promise<string>;\n\n /**\n * A function that updates the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n */\n updateSnapState: (snapId: string, newState: string) => Promise<void>;\n\n /**\n * Encrypts data with a key. This is assumed to perform symmetric encryption.\n *\n * @param key - The key to use for encryption, in hexadecimal format.\n * @param data - The JSON data to encrypt.\n * @returns The ciphertext as a string. The format for this string is\n * dependent on the implementation, but MUST be a string.\n */\n encrypt: (key: string, data: Json) => Promise<string>;\n\n /**\n * Decrypts data with a key. This is assumed to perform symmetric decryption.\n *\n * @param key - The key to use for decryption, in hexadecimal format.\n * @param cipherText - The ciphertext to decrypt. The format for this string\n * is dependent on the implementation, but MUST be a string.\n * @returns The decrypted data as a JSON object.\n */\n decrypt: (key: Hex, cipherText: string) => Promise<unknown>;\n};\n\ntype ManageStateSpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: ManageStateMethodHooks;\n};\n\ntype ManageStateSpecification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getManageStateImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_manageState` permission.\n * `snap_manageState` lets the Snap store and manage some of its state on\n * your device.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_manageState` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n ManageStateSpecificationBuilderOptions,\n ManageStateSpecification\n> = ({\n allowedCaveats = null,\n methodHooks,\n}: ManageStateSpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getManageStateImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<ManageStateMethodHooks> = {\n getMnemonic: true,\n getUnlockPromise: true,\n clearSnapState: true,\n getSnapState: true,\n updateSnapState: true,\n encrypt: true,\n decrypt: true,\n};\n\nexport const manageStateBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\nexport enum ManageStateOperation {\n ClearState = 'clear',\n GetState = 'get',\n UpdateState = 'update',\n}\n\nexport type ManageStateArgs = {\n operation: EnumToUnion<ManageStateOperation>;\n newState?: Record<string, Json>;\n};\n\nexport const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)\n\ntype GetEncryptionKeyArgs = {\n snapId: string;\n mnemonicPhrase: Uint8Array;\n};\n\n/**\n * Get a deterministic encryption key to use for encrypting and decrypting the\n * state.\n *\n * This key should only be used for state encryption using `snap_manageState`.\n * To get other encryption keys, a different salt can be used.\n *\n * @param args - The encryption key args.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The state encryption key.\n */\nasync function getEncryptionKey({\n mnemonicPhrase,\n snapId,\n}: GetEncryptionKeyArgs) {\n return await deriveEntropy({\n mnemonicPhrase,\n input: snapId,\n salt: STATE_ENCRYPTION_SALT,\n magic: STATE_ENCRYPTION_MAGIC_VALUE,\n });\n}\n\ntype EncryptStateArgs = GetEncryptionKeyArgs & {\n state: Json;\n encryptFunction: ManageStateMethodHooks['encrypt'];\n};\n\n/**\n * Encrypt the state using a deterministic encryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to encrypt.\n * @param args.encryptFunction - The function to use for encrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function encryptState({\n state,\n encryptFunction,\n ...keyArgs\n}: EncryptStateArgs) {\n const encryptionKey = await getEncryptionKey(keyArgs);\n return await encryptFunction(encryptionKey, state);\n}\n\ntype DecryptStateArgs = GetEncryptionKeyArgs & {\n state: string;\n decryptFunction: ManageStateMethodHooks['decrypt'];\n};\n\n/**\n * Decrypt the state using a deterministic decryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to decrypt.\n * @param args.decryptFunction - The function to use for decrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function decryptState({\n state,\n decryptFunction,\n ...keyArgs\n}: DecryptStateArgs) {\n try {\n const encryptionKey = await getEncryptionKey(keyArgs);\n const decryptedState = await decryptFunction(encryptionKey, state);\n\n assert(isValidJson(decryptedState));\n\n return decryptedState as Record<string, Json>;\n } catch {\n throw rpcErrors.internal({\n message: 'Failed to decrypt snap state, the state must be corrupted.',\n });\n }\n}\n\n/**\n * Builds the method implementation for `snap_manageState`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.clearSnapState - A function that clears the state stored for a\n * snap.\n * @param hooks.getSnapState - A function that fetches the persisted decrypted\n * state for a snap.\n * @param hooks.updateSnapState - A function that updates the state stored for a\n * snap.\n * @param hooks.getMnemonic - A function to retrieve the Secret Recovery Phrase\n * of the user.\n * @param hooks.getUnlockPromise - A function that resolves once the MetaMask\n * extension is unlocked and prompts the user to unlock their MetaMask if it is\n * locked.\n * @param hooks.encrypt - A function that encrypts the given state.\n * @param hooks.decrypt - A function that decrypts the given state.\n * @returns The method implementation which either returns `null` for a\n * successful state update/deletion or returns the decrypted state.\n * @throws If the params are invalid.\n */\nexport function getManageStateImplementation({\n getMnemonic,\n getUnlockPromise,\n clearSnapState,\n getSnapState,\n updateSnapState,\n encrypt,\n decrypt,\n}: ManageStateMethodHooks) {\n return async function manageState(\n options: RestrictedMethodOptions<ManageStateArgs>,\n ): Promise<null | Record<string, Json>> {\n const {\n params = {},\n method,\n context: { origin },\n } = options;\n const { operation, newState } = getValidatedParams(params, method);\n\n await getUnlockPromise(true);\n const mnemonicPhrase = await getMnemonic();\n\n switch (operation) {\n case ManageStateOperation.ClearState:\n await clearSnapState(origin);\n return null;\n\n case ManageStateOperation.GetState: {\n const state = await getSnapState(origin);\n if (state === null) {\n return state;\n }\n return await decryptState({\n state,\n decryptFunction: decrypt,\n mnemonicPhrase,\n snapId: origin,\n });\n }\n\n case ManageStateOperation.UpdateState: {\n assert(newState);\n\n const encryptedState = await encryptState({\n state: newState,\n encryptFunction: encrypt,\n mnemonicPhrase,\n snapId: origin,\n });\n\n await updateSnapState(origin, encryptedState);\n return null;\n }\n\n default:\n throw rpcErrors.invalidParams(\n `Invalid ${method} operation: \"${operation as string}\"`,\n );\n }\n };\n}\n\n/**\n * Validates the manageState method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @param method - RPC method name used for debugging errors.\n * @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(\n params: unknown,\n method: string,\n storageSizeLimit = STORAGE_SIZE_LIMIT,\n): ManageStateArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { operation, newState } = params;\n\n if (\n !operation ||\n typeof operation !== 'string' ||\n !(Object.values(ManageStateOperation) as string[]).includes(operation)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid manage state \"operation\".',\n });\n }\n\n if (operation === ManageStateOperation.UpdateState) {\n if (!isObject(newState)) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be a plain object.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n let size;\n try {\n // `getJsonSize` will throw if the state is not JSON serializable.\n size = getJsonSize(newState);\n } catch {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be JSON serializable.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n if (size > storageSizeLimit) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must not exceed ${storageSizeLimit} bytes in size.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n }\n\n return params as ManageStateArgs;\n}\n"],"names":["PermissionType","SubjectType","rpcErrors","STATE_ENCRYPTION_MAGIC_VALUE","isObject","getJsonSize","assert","isValidJson","deriveEntropy","STATE_ENCRYPTION_SALT","methodName","specificationBuilder","allowedCaveats","methodHooks","permissionType","RestrictedMethod","targetName","methodImplementation","getManageStateImplementation","subjectTypes","Snap","getMnemonic","getUnlockPromise","clearSnapState","getSnapState","updateSnapState","encrypt","decrypt","manageStateBuilder","Object","freeze","ManageStateOperation","ClearState","GetState","UpdateState","STORAGE_SIZE_LIMIT","getEncryptionKey","mnemonicPhrase","snapId","input","salt","magic","encryptState","state","encryptFunction","keyArgs","encryptionKey","decryptState","decryptFunction","decryptedState","internal","message","manageState","options","params","method","context","origin","operation","newState","getValidatedParams","encryptedState","invalidParams","storageSizeLimit","values","includes","data","receivedNewState","size"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/restricted/manageState.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport { STATE_ENCRYPTION_MAGIC_VALUE, parseJson } from '@metamask/snaps-utils';\nimport type { Json, NonEmptyArray, Hex } from '@metamask/utils';\nimport { isObject, getJsonSize, assert, isValidJson } from '@metamask/utils';\n\nimport type { MethodHooksObject } from '../utils';\nimport { deriveEntropy } from '../utils';\n\n// The salt used for SIP-6-based entropy derivation.\nexport const STATE_ENCRYPTION_SALT = 'snap_manageState encryption';\n\nconst methodName = 'snap_manageState';\n\nexport type ManageStateMethodHooks = {\n /**\n * @returns The mnemonic of the user's primary keyring.\n */\n getMnemonic: () => Promise<Uint8Array>;\n\n /**\n * Waits for the extension to be unlocked.\n *\n * @returns A promise that resolves once the extension is unlocked.\n */\n getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise<void>;\n\n /**\n * A function that clears the state of the requesting Snap.\n */\n clearSnapState: (snapId: string, encrypted: boolean) => void;\n\n /**\n * A function that gets the encrypted state of the requesting Snap.\n *\n * @returns The current state of the Snap.\n */\n getSnapState: (snapId: string, encrypted: boolean) => string;\n\n /**\n * A function that updates the state of the requesting Snap.\n *\n * @param newState - The new state of the Snap.\n */\n updateSnapState: (\n snapId: string,\n newState: string,\n encrypted: boolean,\n ) => void;\n\n /**\n * Encrypts data with a key. This is assumed to perform symmetric encryption.\n *\n * @param key - The key to use for encryption, in hexadecimal format.\n * @param data - The JSON data to encrypt.\n * @returns The ciphertext as a string. The format for this string is\n * dependent on the implementation, but MUST be a string.\n */\n encrypt: (key: string, data: Json) => Promise<string>;\n\n /**\n * Decrypts data with a key. This is assumed to perform symmetric decryption.\n *\n * @param key - The key to use for decryption, in hexadecimal format.\n * @param cipherText - The ciphertext to decrypt. The format for this string\n * is dependent on the implementation, but MUST be a string.\n * @returns The decrypted data as a JSON object.\n */\n decrypt: (key: Hex, cipherText: string) => Promise<unknown>;\n};\n\ntype ManageStateSpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: ManageStateMethodHooks;\n};\n\ntype ManageStateSpecification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getManageStateImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_manageState` permission.\n * `snap_manageState` lets the Snap store and manage some of its state on\n * your device.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_manageState` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n ManageStateSpecificationBuilderOptions,\n ManageStateSpecification\n> = ({\n allowedCaveats = null,\n methodHooks,\n}: ManageStateSpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getManageStateImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<ManageStateMethodHooks> = {\n getMnemonic: true,\n getUnlockPromise: true,\n clearSnapState: true,\n getSnapState: true,\n updateSnapState: true,\n encrypt: true,\n decrypt: true,\n};\n\nexport const manageStateBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\nexport enum ManageStateOperation {\n ClearState = 'clear',\n GetState = 'get',\n UpdateState = 'update',\n}\n\nexport type ManageStateArgs = {\n operation: EnumToUnion<ManageStateOperation>;\n newState?: Record<string, Json>;\n encrypted?: boolean;\n};\n\nexport const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)\n\ntype GetEncryptionKeyArgs = {\n snapId: string;\n mnemonicPhrase: Uint8Array;\n};\n\n/**\n * Get a deterministic encryption key to use for encrypting and decrypting the\n * state.\n *\n * This key should only be used for state encryption using `snap_manageState`.\n * To get other encryption keys, a different salt can be used.\n *\n * @param args - The encryption key args.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The state encryption key.\n */\nasync function getEncryptionKey({\n mnemonicPhrase,\n snapId,\n}: GetEncryptionKeyArgs) {\n return await deriveEntropy({\n mnemonicPhrase,\n input: snapId,\n salt: STATE_ENCRYPTION_SALT,\n magic: STATE_ENCRYPTION_MAGIC_VALUE,\n });\n}\n\ntype EncryptStateArgs = GetEncryptionKeyArgs & {\n state: Json;\n encryptFunction: ManageStateMethodHooks['encrypt'];\n};\n\n/**\n * Encrypt the state using a deterministic encryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to encrypt.\n * @param args.encryptFunction - The function to use for encrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function encryptState({\n state,\n encryptFunction,\n ...keyArgs\n}: EncryptStateArgs) {\n const encryptionKey = await getEncryptionKey(keyArgs);\n return await encryptFunction(encryptionKey, state);\n}\n\ntype DecryptStateArgs = GetEncryptionKeyArgs & {\n state: string;\n decryptFunction: ManageStateMethodHooks['decrypt'];\n};\n\n/**\n * Decrypt the state using a deterministic decryption algorithm, based on the\n * snap ID and mnemonic phrase.\n *\n * @param args - The encryption args.\n * @param args.state - The state to decrypt.\n * @param args.decryptFunction - The function to use for decrypting the state.\n * @param args.snapId - The ID of the snap to get the encryption key for.\n * @param args.mnemonicPhrase - The mnemonic phrase to derive the encryption key\n * from.\n * @returns The encrypted state.\n */\nasync function decryptState({\n state,\n decryptFunction,\n ...keyArgs\n}: DecryptStateArgs) {\n try {\n const encryptionKey = await getEncryptionKey(keyArgs);\n const decryptedState = await decryptFunction(encryptionKey, state);\n\n assert(isValidJson(decryptedState));\n\n return decryptedState as Record<string, Json>;\n } catch {\n throw rpcErrors.internal({\n message: 'Failed to decrypt snap state, the state must be corrupted.',\n });\n }\n}\n\n/**\n * Builds the method implementation for `snap_manageState`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.clearSnapState - A function that clears the state stored for a\n * snap.\n * @param hooks.getSnapState - A function that fetches the persisted decrypted\n * state for a snap.\n * @param hooks.updateSnapState - A function that updates the state stored for a\n * snap.\n * @param hooks.getMnemonic - A function to retrieve the Secret Recovery Phrase\n * of the user.\n * @param hooks.getUnlockPromise - A function that resolves once the MetaMask\n * extension is unlocked and prompts the user to unlock their MetaMask if it is\n * locked.\n * @param hooks.encrypt - A function that encrypts the given state.\n * @param hooks.decrypt - A function that decrypts the given state.\n * @returns The method implementation which either returns `null` for a\n * successful state update/deletion or returns the decrypted state.\n * @throws If the params are invalid.\n */\nexport function getManageStateImplementation({\n getMnemonic,\n getUnlockPromise,\n clearSnapState,\n getSnapState,\n updateSnapState,\n encrypt,\n decrypt,\n}: ManageStateMethodHooks) {\n return async function manageState(\n options: RestrictedMethodOptions<ManageStateArgs>,\n ): Promise<null | Record<string, Json>> {\n const {\n params = {},\n method,\n context: { origin },\n } = options;\n const { operation, newState, encrypted } = getValidatedParams(\n params,\n method,\n );\n\n // If the encrypted param is undefined or null we default to true.\n const shouldEncrypt = encrypted ?? true;\n\n // We only need to prompt the user when the mnemonic is needed\n // which it isn't for the clear operation or unencrypted storage.\n if (shouldEncrypt && operation !== ManageStateOperation.ClearState) {\n await getUnlockPromise(true);\n }\n\n switch (operation) {\n case ManageStateOperation.ClearState:\n clearSnapState(origin, shouldEncrypt);\n return null;\n\n case ManageStateOperation.GetState: {\n const state = getSnapState(origin, shouldEncrypt);\n if (state === null) {\n return state;\n }\n return shouldEncrypt\n ? await decryptState({\n state,\n decryptFunction: decrypt,\n mnemonicPhrase: await getMnemonic(),\n snapId: origin,\n })\n : parseJson<Record<string, Json>>(state);\n }\n\n case ManageStateOperation.UpdateState: {\n assert(newState);\n\n const finalizedState = shouldEncrypt\n ? await encryptState({\n state: newState,\n encryptFunction: encrypt,\n mnemonicPhrase: await getMnemonic(),\n snapId: origin,\n })\n : JSON.stringify(newState);\n\n updateSnapState(origin, finalizedState, shouldEncrypt);\n return null;\n }\n\n default:\n throw rpcErrors.invalidParams(\n `Invalid ${method} operation: \"${operation as string}\"`,\n );\n }\n };\n}\n\n/**\n * Validates the manageState method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @param method - RPC method name used for debugging errors.\n * @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(\n params: unknown,\n method: string,\n storageSizeLimit = STORAGE_SIZE_LIMIT,\n): ManageStateArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { operation, newState, encrypted } = params;\n\n if (\n !operation ||\n typeof operation !== 'string' ||\n !(Object.values(ManageStateOperation) as string[]).includes(operation)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid manage state \"operation\".',\n });\n }\n\n if (encrypted !== undefined && typeof encrypted !== 'boolean') {\n throw rpcErrors.invalidParams({\n message: '\"encrypted\" parameter must be a boolean if specified.',\n });\n }\n\n if (operation === ManageStateOperation.UpdateState) {\n if (!isObject(newState)) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be a plain object.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n let size;\n try {\n // `getJsonSize` will throw if the state is not JSON serializable.\n size = getJsonSize(newState);\n } catch {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must be JSON serializable.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n\n if (size > storageSizeLimit) {\n throw rpcErrors.invalidParams({\n message: `Invalid ${method} \"updateState\" parameter: The new state must not exceed ${storageSizeLimit} bytes in size.`,\n data: {\n receivedNewState:\n typeof newState === 'undefined' ? 'undefined' : newState,\n },\n });\n }\n }\n\n return params as ManageStateArgs;\n}\n"],"names":["PermissionType","SubjectType","rpcErrors","STATE_ENCRYPTION_MAGIC_VALUE","parseJson","isObject","getJsonSize","assert","isValidJson","deriveEntropy","STATE_ENCRYPTION_SALT","methodName","specificationBuilder","allowedCaveats","methodHooks","permissionType","RestrictedMethod","targetName","methodImplementation","getManageStateImplementation","subjectTypes","Snap","getMnemonic","getUnlockPromise","clearSnapState","getSnapState","updateSnapState","encrypt","decrypt","manageStateBuilder","Object","freeze","ManageStateOperation","ClearState","GetState","UpdateState","STORAGE_SIZE_LIMIT","getEncryptionKey","mnemonicPhrase","snapId","input","salt","magic","encryptState","state","encryptFunction","keyArgs","encryptionKey","decryptState","decryptFunction","decryptedState","internal","message","manageState","options","params","method","context","origin","operation","newState","encrypted","getValidatedParams","shouldEncrypt","finalizedState","JSON","stringify","invalidParams","storageSizeLimit","values","includes","undefined","data","receivedNewState","size"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { PermissionType, SubjectType } from '@metamask/permission-controller';
2
2
  import { rpcErrors } from '@metamask/rpc-errors';
3
- import { assertLinksAreSafe } from '@metamask/snaps-utils';
3
+ import { assertLinksAreSafe } from '@metamask/snaps-ui';
4
4
  import { isObject } from '@metamask/utils';
5
5
  const methodName = 'snap_notify';
6
6
  export var NotificationType;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/restricted/notify.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport { assertLinksAreSafe } from '@metamask/snaps-utils';\nimport type { NonEmptyArray } from '@metamask/utils';\nimport { isObject } from '@metamask/utils';\n\nimport { type MethodHooksObject } from '../utils';\n\nconst methodName = 'snap_notify';\n\n// TODO: Move all the types to a shared place when implementing more\n// notifications.\nexport enum NotificationType {\n InApp = 'inApp',\n Native = 'native',\n}\n\nexport type NotificationArgs = {\n /**\n * Enum type to determine notification type.\n */\n type: EnumToUnion<NotificationType>;\n\n /**\n * A message to show on the notification.\n */\n message: string;\n};\n\nexport type NotifyMethodHooks = {\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showNativeNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showInAppNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n isOnPhishingList: (url: string) => boolean;\n\n maybeUpdatePhishingList: () => Promise<void>;\n};\n\ntype SpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: NotifyMethodHooks;\n};\n\ntype Specification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_notify` permission.\n * `snap_notify` allows snaps to send multiple types of notifications to its users.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_notify` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n SpecificationBuilderOptions,\n Specification\n> = ({ allowedCaveats = null, methodHooks }: SpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<NotifyMethodHooks> = {\n showNativeNotification: true,\n showInAppNotification: true,\n isOnPhishingList: true,\n maybeUpdatePhishingList: true,\n};\n\nexport const notifyBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\n/**\n * Builds the method implementation for `snap_notify`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.showNativeNotification - A function that shows a native browser notification.\n * @param hooks.showInAppNotification - A function that shows a notification in the MetaMask UI.\n * @param hooks.isOnPhishingList - A function that checks for links against the phishing list.\n * @param hooks.maybeUpdatePhishingList - A function that updates the phishing list if needed.\n * @returns The method implementation which returns `null` on success.\n * @throws If the params are invalid.\n */\nexport function getImplementation({\n showNativeNotification,\n showInAppNotification,\n isOnPhishingList,\n maybeUpdatePhishingList,\n}: NotifyMethodHooks) {\n return async function implementation(\n args: RestrictedMethodOptions<NotificationArgs>,\n ): Promise<null> {\n const {\n params,\n context: { origin },\n } = args;\n\n const validatedParams = getValidatedParams(params);\n\n await maybeUpdatePhishingList();\n\n assertLinksAreSafe(validatedParams.message, isOnPhishingList);\n\n switch (validatedParams.type) {\n case NotificationType.Native:\n return await showNativeNotification(origin, validatedParams);\n case NotificationType.InApp:\n return await showInAppNotification(origin, validatedParams);\n default:\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n };\n}\n\n/**\n * Validates the notify method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(params: unknown): NotificationArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { type, message } = params;\n\n if (\n !type ||\n typeof type !== 'string' ||\n !Object.values(NotificationType).includes(type as NotificationType)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n\n // Set to the max message length on a Mac notification for now.\n if (!message || typeof message !== 'string' || message.length >= 50) {\n throw rpcErrors.invalidParams({\n message:\n 'Must specify a non-empty string \"message\" less than 50 characters long.',\n });\n }\n\n return params as NotificationArgs;\n}\n"],"names":["PermissionType","SubjectType","rpcErrors","assertLinksAreSafe","isObject","methodName","NotificationType","InApp","Native","specificationBuilder","allowedCaveats","methodHooks","permissionType","RestrictedMethod","targetName","methodImplementation","getImplementation","subjectTypes","Snap","showNativeNotification","showInAppNotification","isOnPhishingList","maybeUpdatePhishingList","notifyBuilder","Object","freeze","implementation","args","params","context","origin","validatedParams","getValidatedParams","message","type","invalidParams","values","includes","length"],"mappings":"AAKA,SAASA,cAAc,EAAEC,WAAW,QAAQ,kCAAkC;AAC9E,SAASC,SAAS,QAAQ,uBAAuB;AAEjD,SAASC,kBAAkB,QAAQ,wBAAwB;AAE3D,SAASC,QAAQ,QAAQ,kBAAkB;AAI3C,MAAMC,aAAa;WAIZ;UAAKC,gBAAgB;IAAhBA,iBACVC,WAAQ;IADED,iBAEVE,YAAS;GAFCF,qBAAAA;AAqDZ;;;;;;;;CAQC,GACD,OAAO,MAAMG,uBAIT,CAAC,EAAEC,iBAAiB,IAAI,EAAEC,WAAW,EAA+B;IACtE,OAAO;QACLC,gBAAgBZ,eAAea,gBAAgB;QAC/CC,YAAYT;QACZK;QACAK,sBAAsBC,kBAAkBL;QACxCM,cAAc;YAAChB,YAAYiB,IAAI;SAAC;IAClC;AACF,EAAE;AAEF,MAAMP,cAAoD;IACxDQ,wBAAwB;IACxBC,uBAAuB;IACvBC,kBAAkB;IAClBC,yBAAyB;AAC3B;AAEA,OAAO,MAAMC,gBAAgBC,OAAOC,MAAM,CAAC;IACzCX,YAAYT;IACZI;IACAE;AACF,GAAY;AAEZ;;;;;;;;;;CAUC,GACD,OAAO,SAASK,kBAAkB,EAChCG,sBAAsB,EACtBC,qBAAqB,EACrBC,gBAAgB,EAChBC,uBAAuB,EACL;IAClB,OAAO,eAAeI,eACpBC,IAA+C;QAE/C,MAAM,EACJC,MAAM,EACNC,SAAS,EAAEC,MAAM,EAAE,EACpB,GAAGH;QAEJ,MAAMI,kBAAkBC,mBAAmBJ;QAE3C,MAAMN;QAENnB,mBAAmB4B,gBAAgBE,OAAO,EAAEZ;QAE5C,OAAQU,gBAAgBG,IAAI;YAC1B,KAAK5B,iBAAiBE,MAAM;gBAC1B,OAAO,MAAMW,uBAAuBW,QAAQC;YAC9C,KAAKzB,iBAAiBC,KAAK;gBACzB,OAAO,MAAMa,sBAAsBU,QAAQC;YAC7C;gBACE,MAAM7B,UAAUiC,aAAa,CAAC;oBAC5BF,SAAS;gBACX;QACJ;IACF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASD,mBAAmBJ,MAAe;IAChD,IAAI,CAACxB,SAASwB,SAAS;QACrB,MAAM1B,UAAUiC,aAAa,CAAC;YAC5BF,SAAS;QACX;IACF;IAEA,MAAM,EAAEC,IAAI,EAAED,OAAO,EAAE,GAAGL;IAE1B,IACE,CAACM,QACD,OAAOA,SAAS,YAChB,CAACV,OAAOY,MAAM,CAAC9B,kBAAkB+B,QAAQ,CAACH,OAC1C;QACA,MAAMhC,UAAUiC,aAAa,CAAC;YAC5BF,SAAS;QACX;IACF;IAEA,+DAA+D;IAC/D,IAAI,CAACA,WAAW,OAAOA,YAAY,YAAYA,QAAQK,MAAM,IAAI,IAAI;QACnE,MAAMpC,UAAUiC,aAAa,CAAC;YAC5BF,SACE;QACJ;IACF;IAEA,OAAOL;AACT"}
1
+ {"version":3,"sources":["../../../src/restricted/notify.ts"],"sourcesContent":["import type {\n PermissionSpecificationBuilder,\n RestrictedMethodOptions,\n ValidPermissionSpecification,\n} from '@metamask/permission-controller';\nimport { PermissionType, SubjectType } from '@metamask/permission-controller';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport { assertLinksAreSafe } from '@metamask/snaps-ui';\nimport type { EnumToUnion } from '@metamask/snaps-utils';\nimport type { NonEmptyArray } from '@metamask/utils';\nimport { isObject } from '@metamask/utils';\n\nimport { type MethodHooksObject } from '../utils';\n\nconst methodName = 'snap_notify';\n\n// TODO: Move all the types to a shared place when implementing more\n// notifications.\nexport enum NotificationType {\n InApp = 'inApp',\n Native = 'native',\n}\n\nexport type NotificationArgs = {\n /**\n * Enum type to determine notification type.\n */\n type: EnumToUnion<NotificationType>;\n\n /**\n * A message to show on the notification.\n */\n message: string;\n};\n\nexport type NotifyMethodHooks = {\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showNativeNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n /**\n * @param snapId - The ID of the Snap that created the notification.\n * @param args - The notification arguments.\n */\n showInAppNotification: (\n snapId: string,\n args: NotificationArgs,\n ) => Promise<null>;\n\n isOnPhishingList: (url: string) => boolean;\n\n maybeUpdatePhishingList: () => Promise<void>;\n};\n\ntype SpecificationBuilderOptions = {\n allowedCaveats?: Readonly<NonEmptyArray<string>> | null;\n methodHooks: NotifyMethodHooks;\n};\n\ntype Specification = ValidPermissionSpecification<{\n permissionType: PermissionType.RestrictedMethod;\n targetName: typeof methodName;\n methodImplementation: ReturnType<typeof getImplementation>;\n allowedCaveats: Readonly<NonEmptyArray<string>> | null;\n}>;\n\n/**\n * The specification builder for the `snap_notify` permission.\n * `snap_notify` allows snaps to send multiple types of notifications to its users.\n *\n * @param options - The specification builder options.\n * @param options.allowedCaveats - The optional allowed caveats for the permission.\n * @param options.methodHooks - The RPC method hooks needed by the method implementation.\n * @returns The specification for the `snap_notify` permission.\n */\nexport const specificationBuilder: PermissionSpecificationBuilder<\n PermissionType.RestrictedMethod,\n SpecificationBuilderOptions,\n Specification\n> = ({ allowedCaveats = null, methodHooks }: SpecificationBuilderOptions) => {\n return {\n permissionType: PermissionType.RestrictedMethod,\n targetName: methodName,\n allowedCaveats,\n methodImplementation: getImplementation(methodHooks),\n subjectTypes: [SubjectType.Snap],\n };\n};\n\nconst methodHooks: MethodHooksObject<NotifyMethodHooks> = {\n showNativeNotification: true,\n showInAppNotification: true,\n isOnPhishingList: true,\n maybeUpdatePhishingList: true,\n};\n\nexport const notifyBuilder = Object.freeze({\n targetName: methodName,\n specificationBuilder,\n methodHooks,\n} as const);\n\n/**\n * Builds the method implementation for `snap_notify`.\n *\n * @param hooks - The RPC method hooks.\n * @param hooks.showNativeNotification - A function that shows a native browser notification.\n * @param hooks.showInAppNotification - A function that shows a notification in the MetaMask UI.\n * @param hooks.isOnPhishingList - A function that checks for links against the phishing list.\n * @param hooks.maybeUpdatePhishingList - A function that updates the phishing list if needed.\n * @returns The method implementation which returns `null` on success.\n * @throws If the params are invalid.\n */\nexport function getImplementation({\n showNativeNotification,\n showInAppNotification,\n isOnPhishingList,\n maybeUpdatePhishingList,\n}: NotifyMethodHooks) {\n return async function implementation(\n args: RestrictedMethodOptions<NotificationArgs>,\n ): Promise<null> {\n const {\n params,\n context: { origin },\n } = args;\n\n const validatedParams = getValidatedParams(params);\n\n await maybeUpdatePhishingList();\n\n assertLinksAreSafe(validatedParams.message, isOnPhishingList);\n\n switch (validatedParams.type) {\n case NotificationType.Native:\n return await showNativeNotification(origin, validatedParams);\n case NotificationType.InApp:\n return await showInAppNotification(origin, validatedParams);\n default:\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n };\n}\n\n/**\n * Validates the notify method `params` and returns them cast to the correct\n * type. Throws if validation fails.\n *\n * @param params - The unvalidated params object from the method request.\n * @returns The validated method parameter object.\n */\nexport function getValidatedParams(params: unknown): NotificationArgs {\n if (!isObject(params)) {\n throw rpcErrors.invalidParams({\n message: 'Expected params to be a single object.',\n });\n }\n\n const { type, message } = params;\n\n if (\n !type ||\n typeof type !== 'string' ||\n !Object.values(NotificationType).includes(type as NotificationType)\n ) {\n throw rpcErrors.invalidParams({\n message: 'Must specify a valid notification \"type\".',\n });\n }\n\n // Set to the max message length on a Mac notification for now.\n if (!message || typeof message !== 'string' || message.length >= 50) {\n throw rpcErrors.invalidParams({\n message:\n 'Must specify a non-empty string \"message\" less than 50 characters long.',\n });\n }\n\n return params as NotificationArgs;\n}\n"],"names":["PermissionType","SubjectType","rpcErrors","assertLinksAreSafe","isObject","methodName","NotificationType","InApp","Native","specificationBuilder","allowedCaveats","methodHooks","permissionType","RestrictedMethod","targetName","methodImplementation","getImplementation","subjectTypes","Snap","showNativeNotification","showInAppNotification","isOnPhishingList","maybeUpdatePhishingList","notifyBuilder","Object","freeze","implementation","args","params","context","origin","validatedParams","getValidatedParams","message","type","invalidParams","values","includes","length"],"mappings":"AAKA,SAASA,cAAc,EAAEC,WAAW,QAAQ,kCAAkC;AAC9E,SAASC,SAAS,QAAQ,uBAAuB;AACjD,SAASC,kBAAkB,QAAQ,qBAAqB;AAGxD,SAASC,QAAQ,QAAQ,kBAAkB;AAI3C,MAAMC,aAAa;WAIZ;UAAKC,gBAAgB;IAAhBA,iBACVC,WAAQ;IADED,iBAEVE,YAAS;GAFCF,qBAAAA;AAqDZ;;;;;;;;CAQC,GACD,OAAO,MAAMG,uBAIT,CAAC,EAAEC,iBAAiB,IAAI,EAAEC,WAAW,EAA+B;IACtE,OAAO;QACLC,gBAAgBZ,eAAea,gBAAgB;QAC/CC,YAAYT;QACZK;QACAK,sBAAsBC,kBAAkBL;QACxCM,cAAc;YAAChB,YAAYiB,IAAI;SAAC;IAClC;AACF,EAAE;AAEF,MAAMP,cAAoD;IACxDQ,wBAAwB;IACxBC,uBAAuB;IACvBC,kBAAkB;IAClBC,yBAAyB;AAC3B;AAEA,OAAO,MAAMC,gBAAgBC,OAAOC,MAAM,CAAC;IACzCX,YAAYT;IACZI;IACAE;AACF,GAAY;AAEZ;;;;;;;;;;CAUC,GACD,OAAO,SAASK,kBAAkB,EAChCG,sBAAsB,EACtBC,qBAAqB,EACrBC,gBAAgB,EAChBC,uBAAuB,EACL;IAClB,OAAO,eAAeI,eACpBC,IAA+C;QAE/C,MAAM,EACJC,MAAM,EACNC,SAAS,EAAEC,MAAM,EAAE,EACpB,GAAGH;QAEJ,MAAMI,kBAAkBC,mBAAmBJ;QAE3C,MAAMN;QAENnB,mBAAmB4B,gBAAgBE,OAAO,EAAEZ;QAE5C,OAAQU,gBAAgBG,IAAI;YAC1B,KAAK5B,iBAAiBE,MAAM;gBAC1B,OAAO,MAAMW,uBAAuBW,QAAQC;YAC9C,KAAKzB,iBAAiBC,KAAK;gBACzB,OAAO,MAAMa,sBAAsBU,QAAQC;YAC7C;gBACE,MAAM7B,UAAUiC,aAAa,CAAC;oBAC5BF,SAAS;gBACX;QACJ;IACF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASD,mBAAmBJ,MAAe;IAChD,IAAI,CAACxB,SAASwB,SAAS;QACrB,MAAM1B,UAAUiC,aAAa,CAAC;YAC5BF,SAAS;QACX;IACF;IAEA,MAAM,EAAEC,IAAI,EAAED,OAAO,EAAE,GAAGL;IAE1B,IACE,CAACM,QACD,OAAOA,SAAS,YAChB,CAACV,OAAOY,MAAM,CAAC9B,kBAAkB+B,QAAQ,CAACH,OAC1C;QACA,MAAMhC,UAAUiC,aAAa,CAAC;YAC5BF,SAAS;QACX;IACF;IAEA,+DAA+D;IAC/D,IAAI,CAACA,WAAW,OAAOA,YAAY,YAAYA,QAAQK,MAAM,IAAI,IAAI;QACnE,MAAMpC,UAAUiC,aAAa,CAAC;YAC5BF,SACE;QACJ;IACF;IAEA,OAAOL;AACT"}
@@ -19,19 +19,19 @@ export declare type ManageStateMethodHooks = {
19
19
  /**
20
20
  * A function that clears the state of the requesting Snap.
21
21
  */
22
- clearSnapState: (snapId: string) => Promise<void>;
22
+ clearSnapState: (snapId: string, encrypted: boolean) => void;
23
23
  /**
24
24
  * A function that gets the encrypted state of the requesting Snap.
25
25
  *
26
26
  * @returns The current state of the Snap.
27
27
  */
28
- getSnapState: (snapId: string) => Promise<string>;
28
+ getSnapState: (snapId: string, encrypted: boolean) => string;
29
29
  /**
30
30
  * A function that updates the state of the requesting Snap.
31
31
  *
32
32
  * @param newState - The new state of the Snap.
33
33
  */
34
- updateSnapState: (snapId: string, newState: string) => Promise<void>;
34
+ updateSnapState: (snapId: string, newState: string, encrypted: boolean) => void;
35
35
  /**
36
36
  * Encrypts data with a key. This is assumed to perform symmetric encryption.
37
37
  *
@@ -90,6 +90,7 @@ export declare enum ManageStateOperation {
90
90
  export declare type ManageStateArgs = {
91
91
  operation: EnumToUnion<ManageStateOperation>;
92
92
  newState?: Record<string, Json>;
93
+ encrypted?: boolean;
93
94
  };
94
95
  export declare const STORAGE_SIZE_LIMIT = 104857600;
95
96
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask/snaps-rpc-methods",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "MetaMask Snaps JSON-RPC method implementations.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "@metamask/permission-controller": "^5.0.0",
41
41
  "@metamask/rpc-errors": "^6.1.0",
42
42
  "@metamask/snaps-ui": "^3.1.0",
43
- "@metamask/snaps-utils": "^3.2.0",
43
+ "@metamask/snaps-utils": "^3.3.0",
44
44
  "@metamask/utils": "^8.1.0",
45
45
  "@noble/hashes": "^1.3.1",
46
46
  "superstruct": "^1.0.3"