Skip to content

Commit d704973

Browse files
authored
fix: first granted permission doesn't save (#261)
* Fix incorrect setStorageKey() signature * After writing to profile sync, fetch the stored data to ensure that it is retrievable before returning ot the caller. Do not skip permissions that fail to serialize. * Remove unnecessary calls to getAccessToken() Plus minor changes: - Improve a comment relating to maximum permissions size - Remove unnecessary type assertion in test. * Fixes from review: - Remove size limitation - Add performance metrics to profilesync store operations - Decompose validation of stored permission into shared function with improved error messages * Remove auth from profileSyncManager factory ctor * Fix linting
1 parent db37a26 commit d704973

File tree

4 files changed

+179
-184
lines changed

4 files changed

+179
-184
lines changed

packages/gator-permissions-snap/src/profileSync/options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ export const createProfileSyncOptions = (
5050
* Uses snap storage to persist persist storage key
5151
*/
5252
const keyStorageOptions: StorageOptions = {
53-
getStorageKey: async () => {
53+
getStorageKey: async (_message: `metamask:${string}`) => {
5454
const state = await stateManager.getState();
5555
return state.profileSyncUserStorageKey;
5656
},
57-
setStorageKey: async (val: string) => {
57+
setStorageKey: async (_message: `metamask:${string}`, val: string) => {
5858
const state = await stateManager.getState();
5959
await stateManager.setState({
6060
...state,

packages/gator-permissions-snap/src/profileSync/profileSync.ts

Lines changed: 82 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ import { hashDelegation, decodeDelegations } from '@metamask/delegation-core';
1212
import type { Hex } from '@metamask/delegation-core';
1313
import type {
1414
UserStorageGenericPathWithFeatureAndKey,
15-
JwtBearerAuth,
1615
UserStorage,
1716
} from '@metamask/profile-sync-controller/sdk';
1817
import {
18+
InternalError,
1919
InvalidInputError,
20-
LimitExceededError,
2120
ParseError,
2221
UnsupportedMethodError,
2322
} from '@metamask/snaps-sdk';
@@ -30,9 +29,6 @@ export type RevocationMetadata = {
3029
recordedAt: number;
3130
};
3231

33-
// Constants for validation
34-
const MAX_STORAGE_SIZE_BYTES = 400 * 1024; // 400kb limit as documented
35-
3632
// Zod schema for runtime validation of StoredGrantedPermission
3733
const zStoredGrantedPermission = z.object({
3834
permissionResponse: zPermissionResponse,
@@ -83,10 +79,9 @@ function safeDeserializeStoredGrantedPermission(
8379
}
8480

8581
/**
86-
* Safely serializes a StoredGrantedPermission object with size validation.
82+
* Safely serializes a StoredGrantedPermission object.
8783
* @param permission - The permission object to serialize.
8884
* @returns The JSON string.
89-
* @throws LimitExceededError if size limit exceeded.
9085
*/
9186
function safeSerializeStoredGrantedPermission(
9287
permission: StoredGrantedPermission,
@@ -98,19 +93,12 @@ function safeSerializeStoredGrantedPermission(
9893
// Serialize to JSON
9994
const jsonString = JSON.stringify(validated);
10095

101-
// Check size limit
102-
const sizeBytes = new TextEncoder().encode(jsonString).length;
103-
if (sizeBytes > MAX_STORAGE_SIZE_BYTES) {
104-
throw new LimitExceededError(
105-
`Permission data exceeds size limit: ${sizeBytes} bytes > ${MAX_STORAGE_SIZE_BYTES} bytes`,
106-
);
107-
}
108-
10996
return jsonString;
11097
} catch (error) {
11198
if (error instanceof z.ZodError) {
11299
throw new InvalidInputError(extractZodError(error.errors));
113100
}
101+
114102
throw error;
115103
}
116104
}
@@ -153,7 +141,6 @@ export function generateObjectKey(permissionContext: Hex): Hex {
153141
}
154142

155143
export type ProfileSyncManagerConfig = {
156-
auth: JwtBearerAuth;
157144
userStorage: UserStorage;
158145
isFeatureEnabled: boolean;
159146
snapsMetricsService?: SnapsMetricsService;
@@ -168,7 +155,7 @@ export function createProfileSyncManager(
168155
config: ProfileSyncManagerConfig,
169156
): ProfileSyncManager {
170157
const FEATURE = 'gator_7715_permissions';
171-
const { auth, userStorage, isFeatureEnabled, snapsMetricsService } = config;
158+
const { userStorage, isFeatureEnabled, snapsMetricsService } = config;
172159
const unConfiguredProfileSyncManager = {
173160
getAllGrantedPermissions: async (): Promise<StoredGrantedPermission[]> => {
174161
logger.debug('unConfiguredProfileSyncManager.getAllGrantedPermissions()');
@@ -201,19 +188,6 @@ export function createProfileSyncManager(
201188
},
202189
};
203190

204-
/**
205-
* Authenticates the user with profile sync.
206-
*
207-
*/
208-
async function authenticate(): Promise<void> {
209-
try {
210-
await auth.getAccessToken();
211-
} catch (error) {
212-
logger.error('Error fetching access token:', error);
213-
throw error;
214-
}
215-
}
216-
217191
/**
218192
* Retrieve all granted permission items under the "7715_permissions" feature will result in GET /api/v1/userstorage/7715_permissions
219193
* VALUES: decrypted("JSONstringifyPermission1", storage_key), decrypted("JSONstringifyPermission2", storage_key).
@@ -223,8 +197,6 @@ export function createProfileSyncManager(
223197
StoredGrantedPermission[]
224198
> {
225199
try {
226-
await authenticate();
227-
228200
const items = await userStorage.getAllFeatureItems(FEATURE);
229201
if (!items) {
230202
await snapsMetricsService?.trackProfileSync({
@@ -271,8 +243,6 @@ export function createProfileSyncManager(
271243
permissionContext: Hex,
272244
): Promise<StoredGrantedPermission | null> {
273245
try {
274-
await authenticate();
275-
276246
const path: UserStorageGenericPathWithFeatureAndKey = `${FEATURE}.${generateObjectKey(permissionContext)}`;
277247
const permission = await userStorage.getItem(path);
278248

@@ -287,6 +257,36 @@ export function createProfileSyncManager(
287257
}
288258
}
289259

260+
/**
261+
* Validates that the stored permission value for the given key matches the expected value.
262+
*
263+
* Retrieves the item from user storage using the provided key and compares it to the expected value.
264+
* Throws an InternalError if the item does not exist or if the stored value does not match the expected value.
265+
*
266+
* @param key - The key for the item in user storage.
267+
* @param expectedValue - The expected serialized value to compare against the retrieved value.
268+
* @throws {InternalError} If the stored item is missing or does not match the expected value.
269+
*/
270+
async function validatePermissionStored(
271+
key: string,
272+
expectedValue: string,
273+
): Promise<void> {
274+
const path: UserStorageGenericPathWithFeatureAndKey = `${FEATURE}.${key}`;
275+
const retrievedValue = await userStorage.getItem(path);
276+
277+
if (!retrievedValue) {
278+
throw new InternalError(
279+
`Unable to retrieve stored item with key: ${key}`,
280+
);
281+
}
282+
283+
if (expectedValue !== retrievedValue) {
284+
throw new InternalError(
285+
`Retrieved stored item with key: ${key} but does not match expected value`,
286+
);
287+
}
288+
}
289+
290290
/**
291291
* Store the granted permission in profile sync.
292292
*
@@ -300,27 +300,44 @@ export function createProfileSyncManager(
300300
async function storeGrantedPermission(
301301
storedGrantedPermission: StoredGrantedPermission,
302302
): Promise<void> {
303+
const performanceData = {
304+
permissionCount: 1,
305+
permissionsStoredElapsedMs: -1,
306+
storedPermissionsValidatedElapsedMs: -1,
307+
};
303308
try {
304-
await authenticate();
305-
309+
const startTime = Date.now();
306310
// Validate and serialize with size check
307311
const serializedPermission = safeSerializeStoredGrantedPermission(
308312
storedGrantedPermission,
309313
);
310314

311-
const path: UserStorageGenericPathWithFeatureAndKey = `${FEATURE}.${generateObjectKey(storedGrantedPermission.permissionResponse.context)}`;
315+
const key = generateObjectKey(
316+
storedGrantedPermission.permissionResponse.context,
317+
);
318+
319+
const path: UserStorageGenericPathWithFeatureAndKey = `${FEATURE}.${key}`;
312320
await userStorage.setItem(path, serializedPermission);
313321

322+
performanceData.permissionsStoredElapsedMs = Date.now() - startTime;
323+
324+
await validatePermissionStored(key, serializedPermission);
325+
326+
performanceData.storedPermissionsValidatedElapsedMs =
327+
Date.now() - startTime;
328+
314329
await snapsMetricsService?.trackProfileSync({
315330
operation: 'store',
316331
success: true,
332+
performanceData,
317333
});
318334
} catch (error) {
319335
logger.error('Error storing granted permission');
320336
await snapsMetricsService?.trackProfileSync({
321337
operation: 'store',
322338
success: false,
323339
errorMessage: (error as Error).message,
340+
performanceData,
324341
});
325342
throw error;
326343
}
@@ -339,23 +356,25 @@ export function createProfileSyncManager(
339356
async function storeGrantedPermissionBatch(
340357
storedGrantedPermissions: StoredGrantedPermission[],
341358
): Promise<void> {
342-
try {
343-
await authenticate();
359+
const performanceData = {
360+
permissionCount: storedGrantedPermissions.length,
361+
permissionsStoredElapsedMs: -1,
362+
storedPermissionsValidatedElapsedMs: -1,
363+
};
344364

365+
try {
366+
const startTime = Date.now();
345367
// Validate and serialize all permissions with size checks
346368
const validatedItems: [string, string][] = [];
347369

348370
for (const permission of storedGrantedPermissions) {
349-
try {
350-
const serializedPermission =
351-
safeSerializeStoredGrantedPermission(permission);
352-
validatedItems.push([
353-
generateObjectKey(permission.permissionResponse.context), // key
354-
serializedPermission, // value
355-
]);
356-
} catch {
357-
logger.warn('Skipping invalid permission in batch');
358-
}
371+
const serializedPermission =
372+
safeSerializeStoredGrantedPermission(permission);
373+
374+
validatedItems.push([
375+
generateObjectKey(permission.permissionResponse.context), // key
376+
serializedPermission, // value
377+
]);
359378
}
360379

361380
if (validatedItems.length === 0) {
@@ -366,16 +385,30 @@ export function createProfileSyncManager(
366385

367386
await userStorage.batchSetItems(FEATURE, validatedItems);
368387

388+
performanceData.permissionsStoredElapsedMs = Date.now() - startTime;
389+
390+
// ensure that all items were stored correctly
391+
const fetchedItems = validatedItems.map(async ([key, value]) =>
392+
validatePermissionStored(key, value),
393+
);
394+
395+
await Promise.all(fetchedItems);
396+
397+
performanceData.storedPermissionsValidatedElapsedMs =
398+
Date.now() - startTime;
399+
369400
await snapsMetricsService?.trackProfileSync({
370401
operation: 'batch_store',
371402
success: true,
403+
performanceData,
372404
});
373405
} catch (error) {
374406
logger.error('Error storing granted permission batch');
375407
await snapsMetricsService?.trackProfileSync({
376408
operation: 'batch_store',
377409
success: false,
378410
errorMessage: (error as Error).message,
411+
performanceData,
379412
});
380413
throw error;
381414
}
@@ -411,8 +444,6 @@ export function createProfileSyncManager(
411444
revocationMetadata,
412445
});
413446

414-
await authenticate();
415-
416447
const updatedPermission: StoredGrantedPermission = {
417448
...existingPermission,
418449
revocationMetadata,

packages/gator-permissions-snap/src/services/snapsMetricsService.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type TrackProfileSyncParams = {
9595
success: boolean;
9696
/** Optional error message if operation failed */
9797
errorMessage?: string;
98+
/** Optional performance data for the operation */
99+
performanceData?: Record<string, number>;
98100
};
99101

100102
/**
@@ -254,13 +256,18 @@ export class SnapsMetricsService {
254256
* @param params - The tracking parameters.
255257
*/
256258
async trackProfileSync(params: TrackProfileSyncParams): Promise<void> {
259+
const { operation, success, errorMessage, performanceData } = params;
260+
261+
const message = success
262+
? `Profile sync ${operation} successful`
263+
: `Profile sync ${operation} failed`;
264+
257265
await this.#trackEvent('Profile Sync', {
258-
message: params.success
259-
? `Profile sync ${params.operation} successful`
260-
: `Profile sync ${params.operation} failed`,
261-
operation: params.operation,
262-
success: params.success,
263-
...(params.errorMessage ? { error_message: params.errorMessage } : {}),
266+
message,
267+
operation,
268+
success,
269+
...(performanceData ? { performance_data: performanceData } : {}),
270+
...(errorMessage ? { error_message: errorMessage } : {}),
264271
});
265272
}
266273

0 commit comments

Comments
 (0)