Skip to content

Commit 3f8fd98

Browse files
authoredFeb 14, 2025··
feat: add optional validateAgainstSchema option when creating user storage entry paths (#5326)
## Explanation This PR adds a new `validateAgainstSchema` option when creating user storage entry paths. This defaults to true, but can be relaxed to false when using the SDK. SDK users should use the feature and key names they decide to use, and Controller users should follow the internal schema. ## References Related to: https://consensyssoftware.atlassian.net/browse/IDENTITY-31 ## Changelog ### `@metamask/profile-sync-controller` - **ADDED**: Optional `validateAgainstSchema` option when creating user storage entry paths ## 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 - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent d931ffa commit 3f8fd98

File tree

3 files changed

+128
-52
lines changed

3 files changed

+128
-52
lines changed
 

‎packages/profile-sync-controller/src/sdk/user-storage.ts

+35-25
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { SHARED_SALT } from '../shared/encryption/constants';
55
import type { Env } from '../shared/env';
66
import { getEnvUrls } from '../shared/env';
77
import type {
8-
UserStorageFeatureKeys,
9-
UserStorageFeatureNames,
10-
UserStoragePathWithFeatureAndKey,
11-
UserStoragePathWithFeatureOnly,
8+
UserStorageGenericFeatureKey,
9+
UserStorageGenericFeatureName,
10+
UserStorageGenericPathWithFeatureAndKey,
11+
UserStorageGenericPathWithFeatureOnly,
1212
} from '../shared/storage-schema';
1313
import { createEntryPath } from '../shared/storage-schema';
1414

@@ -54,42 +54,46 @@ export class UserStorage {
5454
}
5555

5656
async setItem(
57-
path: UserStoragePathWithFeatureAndKey,
57+
path: UserStorageGenericPathWithFeatureAndKey,
5858
value: string,
5959
): Promise<void> {
6060
await this.#upsertUserStorage(path, value);
6161
}
6262

63-
async batchSetItems<FeatureName extends UserStorageFeatureNames>(
64-
path: FeatureName,
65-
values: [UserStorageFeatureKeys<FeatureName>, string][],
63+
async batchSetItems(
64+
path: UserStorageGenericFeatureName,
65+
values: [UserStorageGenericFeatureKey, string][],
6666
) {
6767
await this.#batchUpsertUserStorage(path, values);
6868
}
6969

70-
async getItem(path: UserStoragePathWithFeatureAndKey): Promise<string> {
70+
async getItem(
71+
path: UserStorageGenericPathWithFeatureAndKey,
72+
): Promise<string> {
7173
return this.#getUserStorage(path);
7274
}
7375

7476
async getAllFeatureItems(
75-
path: UserStoragePathWithFeatureOnly,
77+
path: UserStorageGenericFeatureName,
7678
): Promise<string[] | null> {
7779
return this.#getUserStorageAllFeatureEntries(path);
7880
}
7981

80-
async deleteItem(path: UserStoragePathWithFeatureAndKey): Promise<void> {
82+
async deleteItem(
83+
path: UserStorageGenericPathWithFeatureAndKey,
84+
): Promise<void> {
8185
return this.#deleteUserStorage(path);
8286
}
8387

8488
async deleteAllFeatureItems(
85-
path: UserStoragePathWithFeatureOnly,
89+
path: UserStorageGenericFeatureName,
8690
): Promise<void> {
8791
return this.#deleteUserStorageAllFeatureEntries(path);
8892
}
8993

9094
async batchDeleteItems(
91-
path: UserStoragePathWithFeatureOnly,
92-
values: string[],
95+
path: UserStorageGenericFeatureName,
96+
values: UserStorageGenericFeatureKey[],
9397
) {
9498
return this.#batchDeleteUserStorage(path, values);
9599
}
@@ -110,14 +114,16 @@ export class UserStorage {
110114
}
111115

112116
async #upsertUserStorage(
113-
path: UserStoragePathWithFeatureAndKey,
117+
path: UserStorageGenericPathWithFeatureAndKey,
114118
data: string,
115119
): Promise<void> {
116120
try {
117121
const headers = await this.#getAuthorizationHeader();
118122
const storageKey = await this.getStorageKey();
119123
const encryptedData = await encryption.encryptString(data, storageKey);
120-
const encryptedPath = createEntryPath(path, storageKey);
124+
const encryptedPath = createEntryPath(path, storageKey, {
125+
validateAgainstSchema: false,
126+
});
121127

122128
const url = new URL(STORAGE_URL(this.env, encryptedPath));
123129

@@ -150,7 +156,7 @@ export class UserStorage {
150156
}
151157

152158
async #batchUpsertUserStorage(
153-
path: UserStoragePathWithFeatureOnly,
159+
path: UserStorageGenericPathWithFeatureOnly,
154160
data: [string, string][],
155161
): Promise<void> {
156162
try {
@@ -201,7 +207,7 @@ export class UserStorage {
201207
}
202208

203209
async #batchUpsertUserStorageWithAlreadyHashedAndEncryptedEntries(
204-
path: UserStoragePathWithFeatureOnly,
210+
path: UserStorageGenericPathWithFeatureOnly,
205211
encryptedData: [string, string][],
206212
): Promise<void> {
207213
try {
@@ -242,12 +248,14 @@ export class UserStorage {
242248
}
243249

244250
async #getUserStorage(
245-
path: UserStoragePathWithFeatureAndKey,
251+
path: UserStorageGenericPathWithFeatureAndKey,
246252
): Promise<string> {
247253
try {
248254
const headers = await this.#getAuthorizationHeader();
249255
const storageKey = await this.getStorageKey();
250-
const encryptedPath = createEntryPath(path, storageKey);
256+
const encryptedPath = createEntryPath(path, storageKey, {
257+
validateAgainstSchema: false,
258+
});
251259

252260
const url = new URL(STORAGE_URL(this.env, encryptedPath));
253261

@@ -300,7 +308,7 @@ export class UserStorage {
300308
}
301309

302310
async #getUserStorageAllFeatureEntries(
303-
path: UserStoragePathWithFeatureOnly,
311+
path: UserStorageGenericPathWithFeatureOnly,
304312
): Promise<string[] | null> {
305313
try {
306314
const headers = await this.#getAuthorizationHeader();
@@ -383,12 +391,14 @@ export class UserStorage {
383391
}
384392

385393
async #deleteUserStorage(
386-
path: UserStoragePathWithFeatureAndKey,
394+
path: UserStorageGenericPathWithFeatureAndKey,
387395
): Promise<void> {
388396
try {
389397
const headers = await this.#getAuthorizationHeader();
390398
const storageKey = await this.getStorageKey();
391-
const encryptedPath = createEntryPath(path, storageKey);
399+
const encryptedPath = createEntryPath(path, storageKey, {
400+
validateAgainstSchema: false,
401+
});
392402

393403
const url = new URL(STORAGE_URL(this.env, encryptedPath));
394404

@@ -428,7 +438,7 @@ export class UserStorage {
428438
}
429439

430440
async #deleteUserStorageAllFeatureEntries(
431-
path: UserStoragePathWithFeatureOnly,
441+
path: UserStorageGenericPathWithFeatureOnly,
432442
): Promise<void> {
433443
try {
434444
const headers = await this.#getAuthorizationHeader();
@@ -469,7 +479,7 @@ export class UserStorage {
469479
}
470480

471481
async #batchDeleteUserStorage(
472-
path: UserStoragePathWithFeatureOnly,
482+
path: UserStorageGenericPathWithFeatureOnly,
473483
data: string[],
474484
): Promise<void> {
475485
try {

‎packages/profile-sync-controller/src/shared/storage-schema.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ describe('user-storage/schema.ts', () => {
4949
);
5050
});
5151

52+
it('should not throw errors if validateAgainstSchema is false', () => {
53+
const path = 'invalid.feature';
54+
expect(() =>
55+
getFeatureAndKeyFromPath(path, {
56+
validateAgainstSchema: false,
57+
}),
58+
).not.toThrow();
59+
});
60+
5261
it('should return feature and key from path', () => {
5362
const result = getFeatureAndKeyFromPath(
5463
`${USER_STORAGE_FEATURE_NAMES.notifications}.notification_settings`,
@@ -68,5 +77,15 @@ describe('user-storage/schema.ts', () => {
6877
key: '0x123',
6978
});
7079
});
80+
81+
it('should return feature and key from path with arbitrary feature and key when validateAgainstSchema is false', () => {
82+
const result = getFeatureAndKeyFromPath('feature.key', {
83+
validateAgainstSchema: false,
84+
});
85+
expect(result).toStrictEqual({
86+
feature: 'feature',
87+
key: 'key',
88+
});
89+
});
7190
});
7291
});

‎packages/profile-sync-controller/src/shared/storage-schema.ts

+74-27
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,36 @@ export type UserStoragePathWithFeatureAndKey = {
4141
[K in UserStorageFeatureNames]: `${K}.${UserStorageFeatureKeys<K>}`;
4242
}[UserStoragePathWithFeatureOnly];
4343

44-
export const getFeatureAndKeyFromPath = (
45-
path: UserStoragePathWithFeatureAndKey,
46-
): UserStorageFeatureAndKey => {
44+
/**
45+
* The below types are mainly used for the SDK.
46+
* These exist so that the SDK can be used with arbitrary feature names and keys.
47+
*
48+
* We only type enforce feature names and keys when using UserStorageController.
49+
* This is done so we don't end up with magic strings within the applications.
50+
*/
51+
52+
export type UserStorageGenericFeatureName = string;
53+
export type UserStorageGenericFeatureKey = string;
54+
export type UserStorageGenericPathWithFeatureAndKey =
55+
`${UserStorageGenericFeatureName}.${UserStorageGenericFeatureKey}`;
56+
export type UserStorageGenericPathWithFeatureOnly =
57+
UserStorageGenericFeatureName;
58+
59+
type UserStorageGenericFeatureAndKey = {
60+
feature: UserStorageGenericFeatureName;
61+
key: UserStorageGenericFeatureKey;
62+
};
63+
64+
export const getFeatureAndKeyFromPath = <T extends boolean>(
65+
path: T extends true
66+
? UserStoragePathWithFeatureAndKey
67+
: UserStorageGenericPathWithFeatureAndKey,
68+
options: {
69+
validateAgainstSchema: T;
70+
} = { validateAgainstSchema: true as T },
71+
): T extends true
72+
? UserStorageFeatureAndKey
73+
: UserStorageGenericFeatureAndKey => {
4774
const pathRegex = /^\w+\.\w+$/u;
4875

4976
if (!pathRegex.test(path)) {
@@ -52,29 +79,41 @@ export const getFeatureAndKeyFromPath = (
5279
);
5380
}
5481

55-
const [feature, key] = path.split('.') as [
56-
UserStorageFeatureNames,
57-
UserStorageFeatureKeys<UserStorageFeatureNames>,
58-
];
59-
60-
if (!(feature in USER_STORAGE_SCHEMA)) {
61-
throw new Error(`user-storage - invalid feature provided: ${feature}`);
62-
}
63-
64-
const validFeature = USER_STORAGE_SCHEMA[feature] as readonly string[];
65-
66-
if (
67-
!validFeature.includes(key) &&
68-
!validFeature.includes(ALLOW_ARBITRARY_KEYS)
69-
) {
70-
const validKeys = USER_STORAGE_SCHEMA[feature].join(', ');
71-
72-
throw new Error(
73-
`user-storage - invalid key provided for this feature: ${key}. Valid keys: ${validKeys}`,
74-
);
82+
const [feature, key] = path.split('.');
83+
84+
if (options.validateAgainstSchema) {
85+
const featureToValidate = feature as UserStorageFeatureNames;
86+
const keyToValidate = key as UserStorageFeatureKeys<
87+
typeof featureToValidate
88+
>;
89+
90+
if (!(featureToValidate in USER_STORAGE_SCHEMA)) {
91+
throw new Error(
92+
`user-storage - invalid feature provided: ${featureToValidate}. Valid features: ${Object.keys(
93+
USER_STORAGE_SCHEMA,
94+
).join(', ')}`,
95+
);
96+
}
97+
98+
const validFeature = USER_STORAGE_SCHEMA[
99+
featureToValidate
100+
] as readonly string[];
101+
102+
if (
103+
!validFeature.includes(keyToValidate) &&
104+
!validFeature.includes(ALLOW_ARBITRARY_KEYS)
105+
) {
106+
const validKeys = USER_STORAGE_SCHEMA[featureToValidate].join(', ');
107+
108+
throw new Error(
109+
`user-storage - invalid key provided for this feature: ${keyToValidate}. Valid keys: ${validKeys}`,
110+
);
111+
}
75112
}
76113

77-
return { feature, key };
114+
return { feature, key } as T extends true
115+
? UserStorageFeatureAndKey
116+
: UserStorageGenericFeatureAndKey;
78117
};
79118

80119
export const isPathWithFeatureAndKey = (
@@ -92,13 +131,21 @@ export const isPathWithFeatureAndKey = (
92131
*
93132
* @param path - string in the form of `${feature}.${key}` that matches schema
94133
* @param storageKey - users storage key
134+
* @param options - options object
135+
* @param options.validateAgainstSchema - whether to validate the path against the schema.
136+
* This defaults to true, and should only be set to false when using the SDK with arbitrary feature names and keys.
95137
* @returns path to store entry
96138
*/
97-
export function createEntryPath(
98-
path: UserStoragePathWithFeatureAndKey,
139+
export function createEntryPath<T extends boolean>(
140+
path: T extends true
141+
? UserStoragePathWithFeatureAndKey
142+
: UserStorageGenericPathWithFeatureAndKey,
99143
storageKey: string,
144+
options: {
145+
validateAgainstSchema: T;
146+
} = { validateAgainstSchema: true as T },
100147
): string {
101-
const { feature, key } = getFeatureAndKeyFromPath(path);
148+
const { feature, key } = getFeatureAndKeyFromPath(path, options);
102149
const hashedKey = createSHA256Hash(key + storageKey);
103150

104151
return `${feature}/${hashedKey}`;

0 commit comments

Comments
 (0)
Please sign in to comment.