mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 01:03:35 +00:00
* Use typescript-strict-plugin to iteratively turn on strict * Add strict testing to pipeline Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files. * turn on strict for scripts directory * Use plugin for all tsconfigs in monorepo vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes. * remove plugin from configs that extend one that already has it * Update workspace settings to honor strict plugin * Apply strict-plugin to native message test runner * Update vscode workspace to use root tsc version * `./node_modules/.bin/update-strict-comments` 🤖 This is a one-time operation. All future files should adhere to strict type checking. * Add fixme to `ts-strict-ignore` comments * `update-strict-comments` 🤖 repeated for new merge files
246 lines
8.2 KiB
TypeScript
246 lines
8.2 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
|
import { Migrator } from "../migrator";
|
|
|
|
// Types to represent data as it is stored in JSON
|
|
type ExpectedAccountType = {
|
|
tokens?: {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
};
|
|
profile?: {
|
|
apiKeyClientId?: string;
|
|
email?: string;
|
|
};
|
|
keys?: {
|
|
apiKeyClientSecret?: string;
|
|
};
|
|
};
|
|
|
|
type ExpectedGlobalType = {
|
|
twoFactorToken?: string;
|
|
};
|
|
|
|
export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL: KeyDefinitionLike = {
|
|
key: "emailTwoFactorTokenRecord",
|
|
stateDefinition: {
|
|
name: "tokenDiskLocal",
|
|
},
|
|
};
|
|
|
|
const TOKEN_STATE_DEF_LIKE: StateDefinitionLike = {
|
|
name: "token",
|
|
};
|
|
|
|
export const ACCESS_TOKEN_DISK: KeyDefinitionLike = {
|
|
key: "accessToken", // matches KeyDefinition.key
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export const REFRESH_TOKEN_DISK: KeyDefinitionLike = {
|
|
key: "refreshToken",
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export const API_KEY_CLIENT_ID_DISK: KeyDefinitionLike = {
|
|
key: "apiKeyClientId",
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export const API_KEY_CLIENT_SECRET_DISK: KeyDefinitionLike = {
|
|
key: "apiKeyClientSecret",
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export class TokenServiceStateProviderMigrator extends Migrator<37, 38> {
|
|
async migrate(helper: MigrationHelper): Promise<void> {
|
|
// Move global data
|
|
const globalData = await helper.get<ExpectedGlobalType>("global");
|
|
|
|
// Create new global record for 2FA token that we can accumulate data in
|
|
const emailTwoFactorTokenRecord = {};
|
|
|
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
|
async function migrateAccount(
|
|
userId: string,
|
|
account: ExpectedAccountType | undefined,
|
|
globalTwoFactorToken: string | undefined,
|
|
emailTwoFactorTokenRecord: Record<string, string>,
|
|
): Promise<void> {
|
|
let updatedAccount = false;
|
|
|
|
// migrate 2FA token from global to user state
|
|
// Due to the existing implmentation, n users on the same device share the same global state value for 2FA token.
|
|
// So, we will just migrate it to all users to keep it valid for whichever was the user that set it previously.
|
|
// Note: don't bother migrating 2FA Token if user account or email is undefined
|
|
const email = account?.profile?.email;
|
|
if (globalTwoFactorToken != undefined && account != undefined && email != undefined) {
|
|
emailTwoFactorTokenRecord[email] = globalTwoFactorToken;
|
|
// Note: don't set updatedAccount to true here as we aren't updating
|
|
// the legacy user state, just migrating a global state to a new user state
|
|
}
|
|
|
|
// Migrate access token
|
|
const existingAccessToken = account?.tokens?.accessToken;
|
|
|
|
if (existingAccessToken != null) {
|
|
// Only migrate data that exists
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate access token to session storage - never local.
|
|
await helper.setToUser(userId, ACCESS_TOKEN_DISK, existingAccessToken);
|
|
}
|
|
delete account.tokens.accessToken;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
// Migrate refresh token
|
|
const existingRefreshToken = account?.tokens?.refreshToken;
|
|
|
|
if (existingRefreshToken != null) {
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate refresh token to session storage - never local.
|
|
await helper.setToUser(userId, REFRESH_TOKEN_DISK, existingRefreshToken);
|
|
}
|
|
delete account.tokens.refreshToken;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
// Migrate API key client id
|
|
const existingApiKeyClientId = account?.profile?.apiKeyClientId;
|
|
|
|
if (existingApiKeyClientId != null) {
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate client id to session storage - never local.
|
|
await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, existingApiKeyClientId);
|
|
}
|
|
delete account.profile.apiKeyClientId;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
// Migrate API key client secret
|
|
const existingApiKeyClientSecret = account?.keys?.apiKeyClientSecret;
|
|
if (existingApiKeyClientSecret != null) {
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate client secret to session storage - never local.
|
|
await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, existingApiKeyClientSecret);
|
|
}
|
|
delete account.keys.apiKeyClientSecret;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
if (updatedAccount) {
|
|
// Save the migrated account only if it was updated
|
|
await helper.set(userId, account);
|
|
}
|
|
}
|
|
|
|
await Promise.all([
|
|
...accounts.map(({ userId, account }) =>
|
|
migrateAccount(userId, account, globalData?.twoFactorToken, emailTwoFactorTokenRecord),
|
|
),
|
|
]);
|
|
|
|
// Save the global 2FA token record
|
|
await helper.setToGlobal(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, emailTwoFactorTokenRecord);
|
|
|
|
// Delete global data
|
|
delete globalData?.twoFactorToken;
|
|
await helper.set("global", globalData);
|
|
}
|
|
|
|
async rollback(helper: MigrationHelper): Promise<void> {
|
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
|
|
|
// Since we migrated the global 2FA token to all users, we need to rollback the 2FA token for all users
|
|
// but we only need to set it to the global state once
|
|
|
|
// Go through accounts and find the first user that has a non-null email and 2FA token
|
|
let migratedTwoFactorToken: string | null = null;
|
|
for (const { account } of accounts) {
|
|
const email = account?.profile?.email;
|
|
if (email == null) {
|
|
continue;
|
|
}
|
|
const emailTwoFactorTokenRecord: Record<string, string> = await helper.getFromGlobal(
|
|
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
|
|
);
|
|
|
|
migratedTwoFactorToken = emailTwoFactorTokenRecord[email];
|
|
|
|
if (migratedTwoFactorToken != null) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (migratedTwoFactorToken != null) {
|
|
let legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
|
if (!legacyGlobal) {
|
|
legacyGlobal = {};
|
|
}
|
|
legacyGlobal.twoFactorToken = migratedTwoFactorToken;
|
|
await helper.set("global", legacyGlobal);
|
|
}
|
|
|
|
// delete global 2FA token record
|
|
await helper.setToGlobal(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, null);
|
|
|
|
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
|
let updatedLegacyAccount = false;
|
|
|
|
// Rollback access token
|
|
const migratedAccessToken = await helper.getFromUser<string>(userId, ACCESS_TOKEN_DISK);
|
|
|
|
if (account?.tokens && migratedAccessToken != null) {
|
|
account.tokens.accessToken = migratedAccessToken;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, ACCESS_TOKEN_DISK, null);
|
|
|
|
// Rollback refresh token
|
|
const migratedRefreshToken = await helper.getFromUser<string>(userId, REFRESH_TOKEN_DISK);
|
|
|
|
if (account?.tokens && migratedRefreshToken != null) {
|
|
account.tokens.refreshToken = migratedRefreshToken;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, REFRESH_TOKEN_DISK, null);
|
|
|
|
// Rollback API key client id
|
|
|
|
const migratedApiKeyClientId = await helper.getFromUser<string>(
|
|
userId,
|
|
API_KEY_CLIENT_ID_DISK,
|
|
);
|
|
|
|
if (account?.profile && migratedApiKeyClientId != null) {
|
|
account.profile.apiKeyClientId = migratedApiKeyClientId;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, null);
|
|
|
|
// Rollback API key client secret
|
|
const migratedApiKeyClientSecret = await helper.getFromUser<string>(
|
|
userId,
|
|
API_KEY_CLIENT_SECRET_DISK,
|
|
);
|
|
|
|
if (account?.keys && migratedApiKeyClientSecret != null) {
|
|
account.keys.apiKeyClientSecret = migratedApiKeyClientSecret;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, null);
|
|
|
|
if (updatedLegacyAccount) {
|
|
await helper.set(userId, account);
|
|
}
|
|
}
|
|
|
|
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
|
}
|
|
}
|