11/* eslint-disable @typescript-eslint/naming-convention */
22/* eslint-disable no-restricted-globals */
33import type { PermissionResponse } from '@metamask/7715-permissions-shared/types' ;
4- import { logger } from '@metamask/7715-permissions-shared/utils' ;
4+ import { zPermissionResponse } from '@metamask/7715-permissions-shared/types' ;
5+ import {
6+ logger ,
7+ extractZodError ,
8+ } from '@metamask/7715-permissions-shared/utils' ;
59import {
610 hashDelegation ,
711 decodeDelegations ,
@@ -12,7 +16,78 @@ import type {
1216 JwtBearerAuth ,
1317 UserStorage ,
1418} from '@metamask/profile-sync-controller/sdk' ;
15- import { UnsupportedMethodError } from '@metamask/snaps-sdk' ;
19+ import {
20+ InvalidInputError ,
21+ LimitExceededError ,
22+ ParseError ,
23+ UnsupportedMethodError ,
24+ } from '@metamask/snaps-sdk' ;
25+ import { z } from 'zod' ;
26+
27+ // Constants for validation
28+ const MAX_STORAGE_SIZE_BYTES = 400 * 1024 ; // 400kb limit as documented
29+
30+ // Zod schema for runtime validation of StoredGrantedPermission
31+ const zStoredGrantedPermission = z . object ( {
32+ permissionResponse : zPermissionResponse ,
33+ siteOrigin : z . string ( ) . min ( 1 , 'Site origin cannot be empty' ) ,
34+ } ) ;
35+
36+ /**
37+ * Safely deserializes and validates a JSON string as StoredGrantedPermission.
38+ * @param jsonString - The JSON string to deserialize.
39+ * @returns The validated StoredGrantedPermission object.
40+ * @throws InvalidInputError if validation fails.
41+ * @throws ParseError if parsing fails.
42+ */
43+ function safeDeserializeStoredGrantedPermission (
44+ jsonString : string ,
45+ ) : StoredGrantedPermission {
46+ try {
47+ const parsed = JSON . parse ( jsonString ) ;
48+ const validated = zStoredGrantedPermission . parse ( parsed ) ;
49+ return validated ;
50+ } catch ( error ) {
51+ logger . error ( 'Error deserializing stored granted permission:' , error ) ;
52+ if ( error instanceof z . ZodError ) {
53+ throw new InvalidInputError ( extractZodError ( error . errors ) ) ;
54+ }
55+ throw new ParseError ( `Failed to parse JSON` ) ;
56+ }
57+ }
58+
59+ /**
60+ * Safely serializes a StoredGrantedPermission object with size validation.
61+ * @param permission - The permission object to serialize.
62+ * @returns The JSON string.
63+ * @throws LimitExceededError if size limit exceeded.
64+ */
65+ function safeSerializeStoredGrantedPermission (
66+ permission : StoredGrantedPermission ,
67+ ) : string {
68+ try {
69+ // Validate the object structure first
70+ const validated = zStoredGrantedPermission . parse ( permission ) ;
71+
72+ // Serialize to JSON
73+ const jsonString = JSON . stringify ( validated ) ;
74+
75+ // Check size limit
76+ const sizeBytes = new TextEncoder ( ) . encode ( jsonString ) . length ;
77+ if ( sizeBytes > MAX_STORAGE_SIZE_BYTES ) {
78+ throw new LimitExceededError (
79+ `Permission data exceeds size limit: ${ sizeBytes } bytes > ${ MAX_STORAGE_SIZE_BYTES } bytes` ,
80+ ) ;
81+ }
82+
83+ return jsonString ;
84+ } catch ( error ) {
85+ if ( error instanceof z . ZodError ) {
86+ throw new InvalidInputError ( extractZodError ( error . errors ) ) ;
87+ }
88+ throw error ;
89+ }
90+ }
1691
1792export type ProfileSyncManager = {
1893 getAllGrantedPermissions : ( ) => Promise < StoredGrantedPermission [ ] > ;
@@ -109,9 +184,22 @@ export function createProfileSyncManager(
109184 await authenticate ( ) ;
110185
111186 const items = await userStorage . getAllFeatureItems ( FEATURE ) ;
112- return (
113- items ?. map ( ( item ) => JSON . parse ( item ) as StoredGrantedPermission ) ?? [ ]
114- ) ;
187+ if ( ! items ) {
188+ return [ ] ;
189+ }
190+
191+ const validPermissions : StoredGrantedPermission [ ] = [ ] ;
192+
193+ for ( const item of items ) {
194+ try {
195+ const permission = safeDeserializeStoredGrantedPermission ( item ) ;
196+ validPermissions . push ( permission ) ;
197+ } catch ( error ) {
198+ logger . warn ( 'Skipping invalid permission data:' , error ) ;
199+ }
200+ }
201+
202+ return validPermissions ;
115203 } catch ( error ) {
116204 logger . error ( 'Error fetching all granted permissions:' , error ) ;
117205 throw error ;
@@ -133,9 +221,11 @@ export function createProfileSyncManager(
133221 const path : UserStorageGenericPathWithFeatureAndKey = `${ FEATURE } .${ generateObjectKey ( permissionContext ) } ` ;
134222 const permission = await userStorage . getItem ( path ) ;
135223
136- return permission
137- ? ( JSON . parse ( permission ) as StoredGrantedPermission )
138- : null ;
224+ if ( ! permission ) {
225+ return null ;
226+ }
227+
228+ return safeDeserializeStoredGrantedPermission ( permission ) ;
139229 } catch ( error ) {
140230 logger . error ( 'Error fetching granted permissions:' , error ) ;
141231 throw error ;
@@ -147,7 +237,7 @@ export function createProfileSyncManager(
147237 *
148238 * Persisting "<permissionContext>" key under "gator_7715_permissions" feature
149239 * value has to be serialized to string and does not exceed 400kb
150- * it is up to the SDK consumer to enforce proper schema management
240+ * Runtime schema validation is enforced to prevent corrupted data storage
151241 * will result in PUT /api/v1/userstorage/gator_7715_permissions/Hash(<storage_key+<permissionContext>">)
152242 * VALUE: encrypted("JSONstringifyPermission", storage_key).
153243 * @param storedGrantedPermission - The permission response to store.
@@ -158,8 +248,13 @@ export function createProfileSyncManager(
158248 try {
159249 await authenticate ( ) ;
160250
251+ // Validate and serialize with size check
252+ const serializedPermission = safeSerializeStoredGrantedPermission (
253+ storedGrantedPermission ,
254+ ) ;
255+
161256 const path : UserStorageGenericPathWithFeatureAndKey = `${ FEATURE } .${ generateObjectKey ( storedGrantedPermission . permissionResponse . context ) } ` ;
162- await userStorage . setItem ( path , JSON . stringify ( storedGrantedPermission ) ) ;
257+ await userStorage . setItem ( path , serializedPermission ) ;
163258 } catch ( error ) {
164259 logger . error ( 'Error storing granted permission:' , error ) ;
165260 throw error ;
@@ -171,7 +266,7 @@ export function createProfileSyncManager(
171266 *
172267 * Batch set multiple items under the "gator_7715_permissions" feature
173268 * values have to be serialized to string and does not exceed 400kb
174- * it is up to the SDK consumer to enforce proper schema management
269+ * Runtime schema validation is enforced to prevent corrupted data storage
175270 * will result in PUT /api/v1/userstorage/gator_7715_permissions/
176271 * VALUES: encrypted("JSONstringifyPermission1", storage_key), encrypted("JSONstringifyPermission2", storage_key).
177272 * @param storedGrantedPermissions - The permission responses to store.
@@ -182,15 +277,31 @@ export function createProfileSyncManager(
182277 try {
183278 await authenticate ( ) ;
184279
185- await userStorage . batchSetItems (
186- FEATURE ,
187- storedGrantedPermissions . map ( ( storedGrantedPermission ) => [
188- generateObjectKey ( storedGrantedPermission . permissionResponse . context ) , // key
189- JSON . stringify ( storedGrantedPermission ) , // value
190- ] ) ,
191- ) ;
280+ // Validate and serialize all permissions with size checks
281+ const validatedItems : [ string , string ] [ ] = [ ] ;
282+
283+ for ( const permission of storedGrantedPermissions ) {
284+ try {
285+ const serializedPermission =
286+ safeSerializeStoredGrantedPermission ( permission ) ;
287+ validatedItems . push ( [
288+ generateObjectKey ( permission . permissionResponse . context ) , // key
289+ serializedPermission , // value
290+ ] ) ;
291+ } catch ( error ) {
292+ logger . warn ( 'Skipping invalid permission in batch:' , error ) ;
293+ }
294+ }
295+
296+ if ( validatedItems . length === 0 ) {
297+ throw new InvalidInputError (
298+ 'No valid permissions to store in batch operation' ,
299+ ) ;
300+ }
301+
302+ await userStorage . batchSetItems ( FEATURE , validatedItems ) ;
192303 } catch ( error ) {
193- logger . error ( 'Error storing granted permission:' , error ) ;
304+ logger . error ( 'Error storing granted permission batch :' , error ) ;
194305 throw error ;
195306 }
196307 }
0 commit comments