From 51f7584f31169f2db772970e495283b69474a1d8 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Mon, 22 Jul 2024 17:57:44 +0200 Subject: [PATCH] Feat(NOTIFY-852): Enforce user storage schema usage (#4543) ## Explanation This PR enforces the`UserStorageSchema` usage in every function that gets or sets user storage. This will help keep user storage entries consistent, and improve DX when using user storage. ## References [NOTIFY-852](https://consensyssoftware.atlassian.net/browse/NOTIFY-852) ## Changelog ### `@metamask/profile-sync-controller` - **CHANGED**: User storage schema - **ADDED**: User storage path compile-time and runtime validation ### `@metamask/notification-services-controller` - **CHANGED**: Update argument to follow on `@metamask/profile-sync-controller` changes when calling `UserStorageController:performGetStorage` and `UserStorageController:performSetStorage` ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate [NOTIFY-852]: https://consensyssoftware.atlassian.net/browse/NOTIFY-852?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../NotificationServicesController.ts | 4 +- .../UserStorageController.test.ts | 28 +++++-- .../user-storage/UserStorageController.ts | 17 +++-- .../__fixtures__/mockResponses.ts | 2 +- .../controllers/user-storage/schema.test.ts | 45 +++++++++++ .../src/controllers/user-storage/schema.ts | 76 ++++++++++++++----- .../controllers/user-storage/services.test.ts | 4 +- .../src/controllers/user-storage/services.ts | 20 +++-- .../src/sdk/__fixtures__/mock-userstorage.ts | 5 +- .../src/sdk/user-storage.test.ts | 44 +++++------ .../src/sdk/user-storage.ts | 48 +++++------- 11 files changed, 191 insertions(+), 102 deletions(-) create mode 100644 packages/profile-sync-controller/src/controllers/user-storage/schema.test.ts diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts index f7d6747180..9775b5fcaf 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts @@ -267,13 +267,13 @@ export default class NotificationServicesController extends BaseController< getNotificationStorage: async () => { return await this.messagingSystem.call( 'UserStorageController:performGetStorage', - 'notificationSettings', + 'notifications.notificationSettings', ); }, setNotificationStorage: async (state: string) => { return await this.messagingSystem.call( 'UserStorageController:performSetStorage', - 'notificationSettings', + 'notifications.notificationSettings', state, ); }, diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts index 3343270d13..354e154b95 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts @@ -59,7 +59,9 @@ describe('user-storage/user-storage-controller - performGetStorage() tests', () getMetaMetricsState: () => true, }); - const result = await controller.performGetStorage('notificationSettings'); + const result = await controller.performGetStorage( + 'notifications.notificationSettings', + ); mockAPI.done(); expect(result).toBe(MOCK_STORAGE_DATA); }); @@ -76,7 +78,7 @@ describe('user-storage/user-storage-controller - performGetStorage() tests', () }); await expect( - controller.performGetStorage('notificationSettings'), + controller.performGetStorage('notifications.notificationSettings'), ).rejects.toThrow(expect.any(Error)); }); @@ -111,7 +113,7 @@ describe('user-storage/user-storage-controller - performGetStorage() tests', () }); await expect( - controller.performGetStorage('notificationSettings'), + controller.performGetStorage('notifications.notificationSettings'), ).rejects.toThrow(expect.any(Error)); }, ); @@ -132,7 +134,10 @@ describe('user-storage/user-storage-controller - performSetStorage() tests', () getMetaMetricsState: () => true, }); - await controller.performSetStorage('notificationSettings', 'new data'); + await controller.performSetStorage( + 'notifications.notificationSettings', + 'new data', + ); expect(mockAPI.isDone()).toBe(true); }); @@ -148,7 +153,10 @@ describe('user-storage/user-storage-controller - performSetStorage() tests', () }); await expect( - controller.performSetStorage('notificationSettings', 'new data'), + controller.performSetStorage( + 'notifications.notificationSettings', + 'new data', + ), ).rejects.toThrow(expect.any(Error)); }); @@ -183,7 +191,10 @@ describe('user-storage/user-storage-controller - performSetStorage() tests', () }); await expect( - controller.performSetStorage('notificationSettings', 'new data'), + controller.performSetStorage( + 'notifications.notificationSettings', + 'new data', + ), ).rejects.toThrow(expect.any(Error)); }, ); @@ -197,7 +208,10 @@ describe('user-storage/user-storage-controller - performSetStorage() tests', () getMetaMetricsState: () => true, }); await expect( - controller.performSetStorage('notificationSettings', 'new data'), + controller.performSetStorage( + 'notifications.notificationSettings', + 'new data', + ), ).rejects.toThrow(expect.any(Error)); }); }); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index c4829d89d2..55d9fc5115 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -14,7 +14,7 @@ import type { AuthenticationControllerPerformSignOut, } from '../authentication/AuthenticationController'; import { createSHA256Hash } from './encryption'; -import type { UserStorageEntryKeys } from './schema'; +import type { UserStoragePath } from './schema'; import { getUserStorage, upsertUserStorage } from './services'; // TODO: fix external dependencies @@ -281,17 +281,19 @@ export default class UserStorageController extends BaseController< * Allows retrieval of stored data. Data stored is string formatted. * Developers can extend the entry path and entry name through the `schema.ts` file. * - * @param entryKey - entry key that matches schema + * @param path - string in the form of `${feature}.${key}` that matches schema * @returns the decrypted string contents found from user storage (or null if not found) */ public async performGetStorage( - entryKey: UserStorageEntryKeys, + path: UserStoragePath, ): Promise { this.#assertProfileSyncingEnabled(); + const { bearerToken, storageKey } = await this.#getStorageKeyAndBearerToken(); + const result = await getUserStorage({ - entryKey, + path, bearerToken, storageKey, }); @@ -303,20 +305,21 @@ export default class UserStorageController extends BaseController< * Allows storage of user data. Data stored must be string formatted. * Developers can extend the entry path and entry name through the `schema.ts` file. * - * @param entryKey - entry key that matches schema + * @param path - string in the form of `${feature}.${key}` that matches schema * @param value - The string data you want to store. * @returns nothing. NOTE that an error is thrown if fails to store data. */ public async performSetStorage( - entryKey: UserStorageEntryKeys, + path: UserStoragePath, value: string, ): Promise { this.#assertProfileSyncingEnabled(); + const { bearerToken, storageKey } = await this.#getStorageKeyAndBearerToken(); await upsertUserStorage(value, { - entryKey, + path, bearerToken, storageKey, }); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts index 53412dad96..69db8fde3c 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts @@ -10,7 +10,7 @@ type MockResponse = { }; export const MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT = `${USER_STORAGE_ENDPOINT}${createEntryPath( - 'notificationSettings', + 'notifications.notificationSettings', MOCK_STORAGE_KEY, )}`; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/schema.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/schema.test.ts new file mode 100644 index 0000000000..1bdc350f3b --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/schema.test.ts @@ -0,0 +1,45 @@ +import { getFeatureAndKeyFromPath, USER_STORAGE_SCHEMA } from './schema'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ErroneousUserStoragePath = any; + +describe('user-storage/schema.ts', () => { + describe('getFeatureAndKeyFromPath', () => { + it('should throw error if the feature.key format is incorrect', () => { + const path = 'feature/key'; + expect(() => + getFeatureAndKeyFromPath(path as ErroneousUserStoragePath), + ).toThrow( + "user-storage - path is not in the correct format. Correct format: 'feature.key'", + ); + }); + + it('should throw error if feature is invalid', () => { + const path = 'invalid.feature'; + expect(() => + getFeatureAndKeyFromPath(path as ErroneousUserStoragePath), + ).toThrow('user-storage - invalid feature provided: invalid'); + }); + + it('should throw error if key is invalid', () => { + const feature = 'notifications'; + const path = `${feature}.invalid`; + const validKeys = USER_STORAGE_SCHEMA[feature].join(', '); + + expect(() => + getFeatureAndKeyFromPath(path as ErroneousUserStoragePath), + ).toThrow( + `user-storage - invalid key provided for this feature: invalid. Valid keys: ${validKeys}`, + ); + }); + + it('should return feature and key from path', () => { + const path = 'notifications.notificationSettings'; + const result = getFeatureAndKeyFromPath(path); + expect(result).toStrictEqual({ + feature: 'notifications', + key: 'notificationSettings', + }); + }); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/schema.ts b/packages/profile-sync-controller/src/controllers/user-storage/schema.ts index aff9c98520..39cdf848f2 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/schema.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/schema.ts @@ -1,38 +1,76 @@ import { createSHA256Hash } from './encryption'; -type UserStorageEntry = { path: string; entryName: string }; - /** - * The User Storage Endpoint requires a path and an entry name. - * Developers can provide additional paths by extending this variable below + * The User Storage Endpoint requires a feature name and a namespace key. + * Developers can provide additional features and keys by extending these types below. */ -export const USER_STORAGE_ENTRIES = { - notificationSettings: { - path: 'notifications', - entryName: 'notificationSettings', - }, -} satisfies Record; -export type UserStorageEntryKeys = keyof typeof USER_STORAGE_ENTRIES; +export const USER_STORAGE_SCHEMA = { + notifications: ['notificationSettings'], +} as const; + +type UserStorageSchema = typeof USER_STORAGE_SCHEMA; +type UserStorageFeatures = keyof UserStorageSchema; +type UserStorageFeatureKeys = + UserStorageSchema[Feature][number]; + +type UserStorageFeatureAndKey = { + feature: UserStorageFeatures; + key: UserStorageFeatureKeys; +}; + +export type UserStoragePath = { + [K in keyof UserStorageSchema]: `${K}.${UserStorageSchema[K][number]}`; +}[keyof UserStorageSchema]; + +export const getFeatureAndKeyFromPath = ( + path: UserStoragePath, +): UserStorageFeatureAndKey => { + const pathRegex = /^\w+\.\w+$/u; + + if (!pathRegex.test(path)) { + throw new Error( + `user-storage - path is not in the correct format. Correct format: 'feature.key'`, + ); + } + + const [feature, key] = path.split('.') as [ + UserStorageFeatures, + UserStorageFeatureKeys, + ]; + + if (!(feature in USER_STORAGE_SCHEMA)) { + throw new Error(`user-storage - invalid feature provided: ${feature}`); + } + + const validFeature = USER_STORAGE_SCHEMA[feature] as readonly string[]; + + if (!validFeature.includes(key)) { + const validKeys = USER_STORAGE_SCHEMA[feature].join(', '); + + throw new Error( + `user-storage - invalid key provided for this feature: ${key}. Valid keys: ${validKeys}`, + ); + } + + return { feature, key }; +}; /** * Constructs a unique entry path for a user. * This can be done due to the uniqueness of the storage key (no users will share the same storage key). * The users entry is a unique hash that cannot be reversed. * - * @param entryKey - storage schema entry key + * @param path - string in the form of `${feature}.${key}` that matches schema * @param storageKey - users storage key * @returns path to store entry */ export function createEntryPath( - entryKey: UserStorageEntryKeys, + path: UserStoragePath, storageKey: string, ): string { - const entry = USER_STORAGE_ENTRIES[entryKey]; - if (!entry) { - throw new Error(`user-storage - invalid entry provided: ${entryKey}`); - } + const { feature, key } = getFeatureAndKeyFromPath(path); + const hashedKey = createSHA256Hash(key + storageKey); - const hashedKey = createSHA256Hash(entry.entryName + storageKey); - return `/${entry.path}/${hashedKey}`; + return `/${feature}/${hashedKey}`; } diff --git a/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts index 472319c24c..fe7b96573d 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts @@ -14,7 +14,7 @@ describe('user-storage/services.ts - getUserStorage() tests', () => { const actCallGetUserStorage = () => { return getUserStorage({ bearerToken: 'MOCK_BEARER_TOKEN', - entryKey: 'notificationSettings', + path: 'notifications.notificationSettings', storageKey: MOCK_STORAGE_KEY, }); }; @@ -63,7 +63,7 @@ describe('user-storage/services.ts - upsertUserStorage() tests', () => { const actCallUpsertUserStorage = () => { return upsertUserStorage(MOCK_ENCRYPTED_STORAGE_DATA, { bearerToken: 'MOCK_BEARER_TOKEN', - entryKey: 'notificationSettings', + path: 'notifications.notificationSettings', storageKey: MOCK_STORAGE_KEY, }); }; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/services.ts b/packages/profile-sync-controller/src/controllers/user-storage/services.ts index 69144d1ef6..07289a75cc 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/services.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/services.ts @@ -2,7 +2,7 @@ import log from 'loglevel'; import { Env, getEnvUrls } from '../../sdk'; import encryption from './encryption'; -import type { UserStorageEntryKeys } from './schema'; +import type { UserStoragePath } from './schema'; import { createEntryPath } from './schema'; const ENV_URLS = getEnvUrls(Env.PRD); @@ -21,8 +21,8 @@ export type GetUserStorageResponse = { }; export type UserStorageOptions = { + path: UserStoragePath; bearerToken: string; - entryKey: UserStorageEntryKeys; storageKey: string; }; @@ -36,13 +36,15 @@ export async function getUserStorage( opts: UserStorageOptions, ): Promise { try { - const path = createEntryPath(opts.entryKey, opts.storageKey); - const url = new URL(`${USER_STORAGE_ENDPOINT}${path}`); + const { bearerToken, path, storageKey } = opts; + + const encryptedPath = createEntryPath(path, storageKey); + const url = new URL(`${USER_STORAGE_ENDPOINT}${encryptedPath}`); const userStorageResponse = await fetch(url.toString(), { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${opts.bearerToken}`, + Authorization: `Bearer ${bearerToken}`, }, }); @@ -85,15 +87,17 @@ export async function upsertUserStorage( data: string, opts: UserStorageOptions, ): Promise { + const { bearerToken, path, storageKey } = opts; + const encryptedData = encryption.encryptString(data, opts.storageKey); - const path = createEntryPath(opts.entryKey, opts.storageKey); - const url = new URL(`${USER_STORAGE_ENDPOINT}${path}`); + const encryptedPath = createEntryPath(path, storageKey); + const url = new URL(`${USER_STORAGE_ENDPOINT}${encryptedPath}`); const res = await fetch(url.toString(), { method: 'PUT', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${opts.bearerToken}`, + Authorization: `Bearer ${bearerToken}`, }, body: JSON.stringify({ data: encryptedData }), }); diff --git a/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts b/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts index ff7cddc575..a6204e45a9 100644 --- a/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts +++ b/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts @@ -10,7 +10,10 @@ type MockReply = { }; // Example mock notifications storage entry (wildcard) -const MOCK_STORAGE_URL = STORAGE_URL(Env.DEV, 'notifications', ''); +const MOCK_STORAGE_URL = STORAGE_URL( + Env.DEV, + 'notifications/notificationSettings', +); export const MOCK_STORAGE_KEY = 'MOCK_STORAGE_KEY'; // TODO: Either fix this lint violation or explain why it's necessary to ignore. diff --git a/packages/profile-sync-controller/src/sdk/user-storage.test.ts b/packages/profile-sync-controller/src/sdk/user-storage.test.ts index d79200cb8b..b835253a3b 100644 --- a/packages/profile-sync-controller/src/sdk/user-storage.test.ts +++ b/packages/profile-sync-controller/src/sdk/user-storage.test.ts @@ -8,7 +8,7 @@ import { import { arrangeAuth, typedMockFn } from './__fixtures__/test-utils'; import type { IBaseAuth } from './authentication-jwt-bearer/types'; import { Env } from './env'; -import { NotFoundError, UserStorageError, ValidationError } from './errors'; +import { NotFoundError, UserStorageError } from './errors'; import type { StorageOptions } from './user-storage'; import { STORAGE_URL, UserStorage } from './user-storage'; @@ -17,7 +17,7 @@ const MOCK_ADDRESS = '0x68757d15a4d8d1421c17003512AFce15D3f3FaDa'; describe('User Storage - STORAGE_URL()', () => { it('generates an example url path for User Storage', () => { - const result = STORAGE_URL(Env.DEV, 'my-feature', 'my-hashed-entry'); + const result = STORAGE_URL(Env.DEV, 'my-feature/my-hashed-entry'); expect(result).toBeDefined(); expect(result).toContain('my-feature'); expect(result).toContain('my-hashed-entry'); @@ -34,12 +34,14 @@ describe('User Storage', () => { // Test Set const data = JSON.stringify(MOCK_NOTIFICATIONS_DATA); - await userStorage.setItem('notifications', 'ui_settings', data); + await userStorage.setItem('notifications.notificationSettings', data); expect(mockPut.isDone()).toBe(true); expect(mockGet.isDone()).toBe(false); // Test Get (we expect the mocked encrypted data to be decrypt-able with the given Mock Storage Key) - const response = await userStorage.getItem('notifications', 'ui_settings'); + const response = await userStorage.getItem( + 'notifications.notificationSettings', + ); expect(mockGet.isDone()).toBe(true); expect(response).toBe(data); }); @@ -60,12 +62,14 @@ describe('User Storage', () => { // Test Set const data = JSON.stringify(MOCK_NOTIFICATIONS_DATA); - await userStorage.setItem('notifications', 'ui_settings', data); + await userStorage.setItem('notifications.notificationSettings', data); expect(mockPut.isDone()).toBe(true); expect(mockGet.isDone()).toBe(false); // Test Get (we expect the mocked encrypted data to be decrypt-able with the given Mock Storage Key) - const response = await userStorage.getItem('notifications', 'ui_settings'); + const response = await userStorage.getItem( + 'notifications.notificationSettings', + ); expect(mockGet.isDone()).toBe(true); expect(response).toBe(data); }); @@ -84,7 +88,7 @@ describe('User Storage', () => { const data = JSON.stringify(MOCK_NOTIFICATIONS_DATA); await expect( - userStorage.setItem('notifications', 'ui_settings', data), + userStorage.setItem('notifications.notificationSettings', data), ).rejects.toThrow(UserStorageError); }); @@ -101,7 +105,7 @@ describe('User Storage', () => { }); await expect( - userStorage.getItem('notifications', 'ui_settings'), + userStorage.getItem('notifications.notificationSettings'), ).rejects.toThrow(UserStorageError); }); @@ -118,27 +122,10 @@ describe('User Storage', () => { }); await expect( - userStorage.getItem('notifications', 'ui_settings'), + userStorage.getItem('notifications.notificationSettings'), ).rejects.toThrow(NotFoundError); }); - it('get/set fails when given empty feature or keys', async () => { - const { auth } = arrangeAuth('SRP', MOCK_SRP); - const { userStorage } = arrangeUserStorage(auth); - - handleMockUserStoragePut(); - handleMockUserStorageGet(); - - // Test Set Error - const data = JSON.stringify(MOCK_NOTIFICATIONS_DATA); - await expect(userStorage.setItem('', '', data)).rejects.toThrow( - ValidationError, - ); - - // Test Get Error - await expect(userStorage.getItem('', '')).rejects.toThrow(ValidationError); - }); - it('get/sets using a newly generated storage key (not in storage)', async () => { const { auth } = arrangeAuth('SRP', MOCK_SRP); const { userStorage, mockGetStorageKey } = arrangeUserStorage(auth); @@ -149,7 +136,10 @@ describe('User Storage', () => { handleMockUserStoragePut(); - await userStorage.setItem('notifications', 'ui_settings', 'some fake data'); + await userStorage.setItem( + 'notifications.notificationSettings', + 'some fake data', + ); expect(mockAuthSignMessage).toHaveBeenCalled(); // SignMessage called since generating new key }); }); diff --git a/packages/profile-sync-controller/src/sdk/user-storage.ts b/packages/profile-sync-controller/src/sdk/user-storage.ts index adc03b8f6d..3e439b68db 100644 --- a/packages/profile-sync-controller/src/sdk/user-storage.ts +++ b/packages/profile-sync-controller/src/sdk/user-storage.ts @@ -1,11 +1,13 @@ +import type { UserStoragePath } from '../controllers/user-storage/schema'; +import { createEntryPath } from '../controllers/user-storage/schema'; import type { IBaseAuth } from './authentication-jwt-bearer/types'; import encryption, { createSHA256Hash } from './encryption'; import type { Env } from './env'; import { getEnvUrls } from './env'; -import { NotFoundError, UserStorageError, ValidationError } from './errors'; +import { NotFoundError, UserStorageError } from './errors'; -export const STORAGE_URL = (env: Env, feature: string, entry: string) => - `${getEnvUrls(env).userStorageApiUrl}/api/v1/userstorage/${feature}/${entry}`; +export const STORAGE_URL = (env: Env, encryptedPath: string) => + `${getEnvUrls(env).userStorageApiUrl}/api/v1/userstorage/${encryptedPath}`; export type UserStorageConfig = { env: Env; @@ -39,18 +41,12 @@ export class UserStorage { this.options = options; } - async setItem(feature: string, key: string, value: string): Promise { - if (!feature.trim() || !key.trim()) { - throw new ValidationError('feature or key cannot be empty strings'); - } - await this.#upsertUserStorage(feature, key, value); + async setItem(path: UserStoragePath, value: string): Promise { + await this.#upsertUserStorage(path, value); } - async getItem(feature: string, key: string): Promise { - if (!feature.trim() || !key.trim()) { - throw new ValidationError('feature or key cannot be empty strings'); - } - return this.#getUserStorage(feature, key); + async getItem(path: UserStoragePath): Promise { + return this.#getUserStorage(path); } async getStorageKey(): Promise { @@ -68,18 +64,14 @@ export class UserStorage { return hashedStorageKeySignature; } - async #upsertUserStorage( - feature: string, - key: string, - data: string, - ): Promise { + async #upsertUserStorage(path: UserStoragePath, data: string): Promise { try { const headers = await this.#getAuthorizationHeader(); const storageKey = await this.getStorageKey(); const encryptedData = encryption.encryptString(data, storageKey); - const url = new URL( - STORAGE_URL(this.env, feature, this.#createEntryKey(key, storageKey)), - ); + const encryptedPath = createEntryPath(path, storageKey); + + const url = new URL(STORAGE_URL(this.env, encryptedPath)); const response = await fetch(url.toString(), { method: 'PUT', @@ -104,18 +96,18 @@ export class UserStorage { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e ?? ''); throw new UserStorageError( - `failed to upsert user storage for feature '${feature}' and key '${key}'. ${errorMessage}`, + `failed to upsert user storage for path '${path}'. ${errorMessage}`, ); } } - async #getUserStorage(feature: string, key: string): Promise { + async #getUserStorage(path: UserStoragePath): Promise { try { const headers = await this.#getAuthorizationHeader(); const storageKey = await this.getStorageKey(); - const url = new URL( - STORAGE_URL(this.env, feature, this.#createEntryKey(key, storageKey)), - ); + const encryptedPath = createEntryPath(path, storageKey); + + const url = new URL(STORAGE_URL(this.env, encryptedPath)); const response = await fetch(url.toString(), { headers: { @@ -126,7 +118,7 @@ export class UserStorage { if (response.status === 404) { throw new NotFoundError( - `feature/key set not found for feature '${feature}' and key '${key}'.`, + `feature/key set not found for path '${path}'.`, ); } @@ -149,7 +141,7 @@ export class UserStorage { e instanceof Error ? e.message : JSON.stringify(e ?? ''); throw new UserStorageError( - `failed to get user storage for feature '${feature}' and key '${key}'. ${errorMessage}`, + `failed to get user storage for path '${path}'. ${errorMessage}`, ); } }