diff --git a/libs/admin-console/src/common/collections/services/vnext-collection.state.ts b/libs/admin-console/src/common/collections/services/vnext-collection.state.ts index 331c80436f7..9d0ded718b5 100644 --- a/libs/admin-console/src/common/collections/services/vnext-collection.state.ts +++ b/libs/admin-console/src/common/collections/services/vnext-collection.state.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { COLLECTION_DATA, DeriveDefinition, @@ -15,7 +13,7 @@ export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record) => CollectionData.fromJSON(jsonData), + deserializer: (jsonData) => CollectionData.fromJSON(jsonData!), clearOn: ["logout"], }, ); diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts index 6ca817772b0..340b3cbce3b 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -16,11 +16,11 @@ import { import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service"; export const LOGIN_EMAIL = new KeyDefinition(LOGIN_EMAIL_MEMORY, "loginEmail", { - deserializer: (value: string) => value, + deserializer: (value: string | null) => value, }); export const STORED_EMAIL = new KeyDefinition(LOGIN_EMAIL_DISK, "storedEmail", { - deserializer: (value: string) => value, + deserializer: (value: string | null) => value, }); export class LoginEmailService implements LoginEmailServiceAbstraction { diff --git a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts index e257b691638..ba5f810778e 100644 --- a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts +++ b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts @@ -1,5 +1,4 @@ import { map } from "rxjs"; -import { Jsonify } from "type-fest"; import { ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, @@ -20,7 +19,7 @@ import { */ function buildKeyDefinition(key: string): UserKeyDefinition { return new UserKeyDefinition(ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, key, { - deserializer: (obj: Jsonify) => obj as T, + deserializer: (obj) => obj! as T, clearOn: ["logout"], }); } diff --git a/libs/common/src/admin-console/services/organization/organization.state.ts b/libs/common/src/admin-console/services/organization/organization.state.ts index fea0423f389..523e0d21a31 100644 --- a/libs/common/src/admin-console/services/organization/organization.state.ts +++ b/libs/common/src/admin-console/services/organization/organization.state.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { ORGANIZATIONS_DISK, UserKeyDefinition } from "../../../platform/state"; import { OrganizationData } from "../../models/data/organization.data"; @@ -13,7 +11,7 @@ export const ORGANIZATIONS = UserKeyDefinition.record( ORGANIZATIONS_DISK, "organizations", { - deserializer: (obj: Jsonify) => OrganizationData.fromJSON(obj), + deserializer: (obj) => OrganizationData.fromJSON(obj!), clearOn: ["logout"], }, ); diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index 1fdca04aceb..b41903100f5 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -122,7 +122,7 @@ export class SymmetricCryptoKey { return new SymmetricCryptoKey(arrayBuffer); } - static fromJSON(obj: Jsonify): SymmetricCryptoKey { + static fromJSON(obj: Jsonify | null): SymmetricCryptoKey { return SymmetricCryptoKey.fromString(obj?.keyB64); } } diff --git a/libs/common/src/platform/state/deserialization-helpers.ts b/libs/common/src/platform/state/deserialization-helpers.ts index cb7d393a83a..f25edc2361c 100644 --- a/libs/common/src/platform/state/deserialization-helpers.ts +++ b/libs/common/src/platform/state/deserialization-helpers.ts @@ -8,8 +8,8 @@ import { Jsonify } from "type-fest"; * @returns */ export function array( - elementDeserializer: (element: Jsonify) => T, -): (array: Jsonify) => T[] { + elementDeserializer: (element: Jsonify | null) => T | null, +): (array: Jsonify | null) => T[] | null { return (array) => { if (array == null) { return null; @@ -24,9 +24,9 @@ export function array( * @param valueDeserializer */ export function record( - valueDeserializer: (value: Jsonify) => T, -): (record: Jsonify>) => Record { - return (jsonValue: Jsonify | null>) => { + valueDeserializer: (value: Jsonify | null) => T | null, +): (record: Jsonify> | null) => Record | null { + return (jsonValue: Jsonify> | null) => { if (jsonValue == null) { return null; } diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index 519e98ef52d..9888b111b61 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -1,7 +1,6 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; +import { mergeOptions, OptionsWithDefaultsDeep } from "../../types/options"; import { StorageKey } from "../../types/state"; import { array, record } from "./deserialization-helpers"; @@ -42,7 +41,7 @@ export type KeyDefinitionOptions = { * @param jsonValue The JSON object representation of your state. * @returns The fully typed version of your state. */ - readonly deserializer: (jsonValue: Jsonify) => T | null; + readonly deserializer: (jsonValue: Jsonify | null) => T | null; /** * The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed. * Defaults to 1000ms. @@ -55,13 +54,27 @@ export type KeyDefinitionOptions = { readonly debug?: DebugOptions; }; +const DEFAULT_KEY_DEFINITION_OPTIONS = Object.freeze({ + cleanupDelayMs: 1000 as number, + debug: { + enableUpdateLogging: false, + enableRetrievalLogging: false, + }, +}); + /** * KeyDefinitions describe the precise location to store data for a given piece of state. * The StateDefinition is used to describe the domain of the state, and the KeyDefinition * sub-divides that domain into specific keys. */ export class KeyDefinition { - readonly debug: Required; + private readonly options: OptionsWithDefaultsDeep< + KeyDefinitionOptions, + typeof DEFAULT_KEY_DEFINITION_OPTIONS + >; + get debug() { + return Object.freeze({ ...this.options.debug }); + } /** * Creates a new instance of a KeyDefinition @@ -75,24 +88,19 @@ export class KeyDefinition { constructor( readonly stateDefinition: StateDefinition, readonly key: string, - private readonly options: KeyDefinitionOptions, + options: KeyDefinitionOptions, ) { if (options.deserializer == null) { throw new Error(`'deserializer' is a required property on key ${this.errorKeyName}`); } - if (options.cleanupDelayMs < 0) { + this.options = mergeOptions(options, DEFAULT_KEY_DEFINITION_OPTIONS); + + if (this.options.cleanupDelayMs < 0) { throw new Error( - `'cleanupDelayMs' must be greater than or equal to 0. Value of ${options.cleanupDelayMs} passed to key ${this.errorKeyName} `, + `'cleanupDelayMs' must be greater than or equal to 0. Value of ${this.options.cleanupDelayMs} passed to key ${this.errorKeyName} `, ); } - - // Normalize optional debug options - const { enableUpdateLogging = false, enableRetrievalLogging = false } = options.debug ?? {}; - this.debug = { - enableUpdateLogging, - enableRetrievalLogging, - }; } /** @@ -106,7 +114,7 @@ export class KeyDefinition { * Gets the number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed. */ get cleanupDelayMs() { - return this.options.cleanupDelayMs < 0 ? 0 : (this.options.cleanupDelayMs ?? 1000); + return this.options.cleanupDelayMs; } /** diff --git a/libs/common/src/services/event/key-definitions.ts b/libs/common/src/services/event/key-definitions.ts index 56820996886..9119d773656 100644 --- a/libs/common/src/services/event/key-definitions.ts +++ b/libs/common/src/services/event/key-definitions.ts @@ -5,7 +5,7 @@ export const EVENT_COLLECTION = UserKeyDefinition.array( EVENT_COLLECTION_DISK, "events", { - deserializer: (s) => EventData.fromJSON(s), + deserializer: (s) => EventData.fromJSON(s!), clearOn: ["logout"], }, ); diff --git a/libs/common/src/tools/send/services/key-definitions.ts b/libs/common/src/tools/send/services/key-definitions.ts index f1a6b3d6c6a..a5d1249c3ec 100644 --- a/libs/common/src/tools/send/services/key-definitions.ts +++ b/libs/common/src/tools/send/services/key-definitions.ts @@ -7,7 +7,7 @@ export const SEND_USER_ENCRYPTED = UserKeyDefinition.record( SEND_DISK, "sendUserEncrypted", { - deserializer: (obj: SendData) => obj, + deserializer: (obj) => obj, clearOn: ["logout"], }, ); diff --git a/libs/common/src/types/merge-deep.ts b/libs/common/src/types/merge-deep.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/common/src/types/options.spec.ts b/libs/common/src/types/options.spec.ts new file mode 100644 index 00000000000..412673683f3 --- /dev/null +++ b/libs/common/src/types/options.spec.ts @@ -0,0 +1,100 @@ +import { mergeOptions } from "./options"; + +type ExampleOptions = { + readonly a?: number; + readonly required_func: () => void; + readonly optional_func?: () => void; + readonly b: string; + readonly c?: { + readonly d?: number; + readonly e: string; + }; +}; + +const EXAMPLE_DEFAULTS = Object.freeze({ + a: 0, + optional_func: () => {}, + c: { + d: 1, + }, + f: { + h: 1, + }, +}); + +describe("mergeOptions", () => { + it("merges options with defaults", () => { + const options: ExampleOptions = { + required_func: () => {}, + b: "test", + }; + + const merged = mergeOptions(options, EXAMPLE_DEFAULTS); + + // can access properties + expect(merged.a).toBe(42); + + expect(merged).toEqual({ + a: 0, + b: "test", + }); + }); + + it("overrides defaults with options", () => { + const options: ExampleOptions = { + a: 42, + b: "test", + f: { + i: "example", + }, + required_func: () => {}, + }; + + const merged = mergeOptions(options, EXAMPLE_DEFAULTS); + + // can access properties + expect(merged.a).toBe(42); + + expect(merged).toEqual({ + a: 42, + b: "test", + }); + }); + + //Defaults has a required property 'a', but options does not provide it. Not an error + mergeOptions({ b: "test" } as { b: string }, { a: 0 }); + //Defaults provides a required function, but options does not provide it. Not an error + mergeOptions({ b: "test" } as { b: string }, { required_func: () => {} }); + //Defaults provides a required property of the wrong type. Not an error because default will never be used + mergeOptions({ a: "test" } as { a: string }, { a: 0 }); + //Defaults provides a required function of the wrong type. Not an error because default will never be used + mergeOptions({ required_func: () => "" } as { required_func: () => string }, { + required_func: () => {}, + }); + + //@ts-expect-error -- Defaults provides an optional property of the wrong type + mergeOptions({} as { a?: string }, { a: 0 }); + //@ts-expect-error -- Defaults provides an optional function of the wrong type + mergeOptions({} as { optional_func?: () => string }, { optional_func: () => {} }); + + //@ts-expect-error -- defaults missing an property optional in options + mergeOptions({ a: 42 } as { a?: number }, {}); + //@ts-expect-error -- defaults missing an method optional in options + mergeOptions({ f: () => {} } as { f?: () => void }, {}); + + //@ts-expect-error -- defaults missing a deep optional property defined in options + mergeOptions({ a: { required_func: () => {} } } as { a?: { required_func: () => void } }, {}); + //@ts-expect-error -- defaults missing a deep optional property defined in options + mergeOptions({ a: { required_func: () => {} } } as { a: { required_func?: () => void } }, {}); + + //@ts-expect-error -- defaults missing a optional object defined in options + mergeOptions({} as { a?: { b: number } }, {}); + + //@ts-expect-error -- defaults missing a deep optional property defined in required option + mergeOptions({ a: {} } as { a: { b?: number } }, {}); + + //@ts-expect-error -- defaults missing a deep optional property defined in optional option + mergeOptions({ a: {} } as { a?: { b?: number } }, {}); + //@ts-expect-error -- defaults missing a deep optional property defined in optional option + mergeOptions({ a: {} } as { a?: { b?: number } }, { a: {} }); +}); diff --git a/libs/common/src/types/options.ts b/libs/common/src/types/options.ts new file mode 100644 index 00000000000..4cdccf8adf0 --- /dev/null +++ b/libs/common/src/types/options.ts @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type -- used in type-fest's code*/ +import { RequiredKeysOf, Simplify, Primitive } from "type-fest"; + +type Function = (...args: any[]) => any; + +/** FIXME: this is pulled from type-fest-v4. remove when we update package */ +export type ConditionalSimplifyDeep< + Type, + ExcludeType = never, + IncludeType = unknown, +> = Type extends ExcludeType + ? Type + : Type extends IncludeType + ? { [TypeKey in keyof Type]: ConditionalSimplifyDeep } + : Type; +export type BuiltIns = Primitive | void | Date | RegExp; +export type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown); +export type SimplifyDeep = ConditionalSimplifyDeep< + Type, + ExcludeType | NonRecursiveType | Set | Map, + object +>; + +type SimplifyDeepExcludeArray = SimplifyDeep; +export type UnknownArray = readonly unknown[]; +export type UnknownRecord = Record; +export type UnknownArrayOrTuple = readonly [...unknown[]]; +export type OmitIndexSignature = { + [KeyType in keyof ObjectType as {} extends Record + ? never + : KeyType]: ObjectType[KeyType]; +}; +export type PickIndexSignature = { + [KeyType in keyof ObjectType as {} extends Record + ? KeyType + : never]: ObjectType[KeyType]; +}; + +type MergeDeepRecordProperty = undefined extends Source + ? + | MergeDeepOrReturn, Exclude> + | undefined + : MergeDeepOrReturn; +type RequiredFilter = undefined extends Type[Key] + ? Type[Key] extends undefined + ? Key + : never + : Key; + +// Returns `never` if the key is required otherwise return the key type. +type OptionalFilter = undefined extends Type[Key] + ? Type[Key] extends undefined + ? never + : Key + : never; + +export type EnforceOptional = Simplify< + { + [Key in keyof ObjectType as RequiredFilter]: ObjectType[Key]; + } & { + [Key in keyof ObjectType as OptionalFilter]?: Exclude< + ObjectType[Key], + undefined + >; + } +>; + +type MergeDeepOrReturn = SimplifyDeepExcludeArray< + [undefined] extends [Destination | Source] + ? DefaultType + : Destination extends UnknownRecord + ? Source extends UnknownRecord + ? MergeDeepRecord + : DefaultType + : Destination extends UnknownArrayOrTuple + ? Source extends UnknownArrayOrTuple + ? MergeDeepArrayOrTuple + : DefaultType + : DefaultType +>; + +type MergeDeepArrayOrTuple< + Destination extends UnknownArrayOrTuple, + Source extends UnknownArrayOrTuple, +> = Array[number] | Exclude[number]>; + +type MergeDeepRecord< + Destination extends UnknownRecord, + Source extends UnknownRecord, +> = DoMergeDeepRecord, OmitIndexSignature> & + Merge, PickIndexSignature>; + +type DoMergeDeepRecord = + // Case in rule 1: The destination contains the key but the source doesn't. + { + [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key]; + } & { + // Case in rule 2: The source contains the key but the destination doesn't. + [Key in keyof Source as Key extends keyof Destination ? never : Key]: Source[Key]; + } & { + // Case in rule 3: Both the source and the destination contain the key. + [Key in keyof Source as Key extends keyof Destination ? Key : never]: MergeDeepRecordProperty< + Destination[Key], + Source[Key] + >; + }; +type SimpleMerge = { + [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key]; +} & Source; + +export type Merge = Simplify< + SimpleMerge, PickIndexSignature> & + SimpleMerge, OmitIndexSignature> +>; +export type IfNever = + IsNever extends true ? TypeIfNever : TypeIfNotNever; +export type IsNever = [T] extends [never] ? true : false; + +type MergeDeep = SimplifyDeepExcludeArray< + [undefined] extends [Destination | Source] + ? never + : Destination extends UnknownRecord + ? Source extends UnknownRecord + ? MergeDeepRecord + : never + : Destination extends UnknownArrayOrTuple + ? Source extends UnknownArrayOrTuple + ? MergeDeepArrayOrTuple + : never + : never +>; +export type ConditionalKeys = { + // Map through all the keys of the given base type. + [Key in keyof Base]-?: Base[Key] extends Condition // Pick only keys with types extending the given `Condition` type. + ? // Retain this key + // If the value for the key extends never, only include it if `Condition` also extends never + IfNever, Key> + : // Discard this key since the condition fails. + never; + // Convert the produced object into a union type of the keys which passed the conditional test. +}[keyof Base]; + +/** END FIXME: this is pulled from type-fest-v4. remove when we update package */ + +export type OptionsWithDefaultsDeep = + // FIXME: replace with MergeDeep when type-fest is updated to v4+ + MergeDeep; + +type DefaultsForDeep = Simplify< + Omit< + Required<{ + [K in keyof Options]: Required[K] extends Primitive | Function + ? Options[K] + : Required[K] extends object + ? DefaultsForDeep[K]> + : never; // should only ever be primitive, function, or object + }>, + RequiredPrimitiveKeysOf | RequiredMethodKeysOf + > +>; + +// Returns only the keys of `Obj` that are required and not records +type RequiredPrimitiveKeysOf = RequiredKeysOf< + Omit> +>; + +// Returns only the keys of `Obj` that are required and are functions +type RequiredMethodKeysOf = RequiredKeysOf< + Pick> +>; + +export function mergeOptions< + Options extends object, + const Defaults extends DefaultsForDeep, +>( + options: Options, + defaults: Defaults, +): OptionsWithDefaultsDeep> { + const result = { ...options } as any; + for (const key in defaults) { + if (result[key] == null) { + result[key] = (defaults as any)[key]; + } + } + return result as OptionsWithDefaultsDeep>; +} diff --git a/libs/common/src/vault/notifications/state/end-user-notification.state.ts b/libs/common/src/vault/notifications/state/end-user-notification.state.ts index 644c8e42429..1bc24f413d0 100644 --- a/libs/common/src/vault/notifications/state/end-user-notification.state.ts +++ b/libs/common/src/vault/notifications/state/end-user-notification.state.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { NOTIFICATION_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; import { NotificationViewData } from "../models"; @@ -8,8 +6,7 @@ export const NOTIFICATIONS = UserKeyDefinition.array( NOTIFICATION_DISK, "notifications", { - deserializer: (notification: Jsonify) => - NotificationViewData.fromJSON(notification), + deserializer: (notification) => NotificationViewData.fromJSON(notification!), clearOn: ["logout", "lock"], }, ); diff --git a/libs/common/src/vault/services/key-state/folder.state.ts b/libs/common/src/vault/services/key-state/folder.state.ts index b3e61f5bf31..6e3a2f7d614 100644 --- a/libs/common/src/vault/services/key-state/folder.state.ts +++ b/libs/common/src/vault/services/key-state/folder.state.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { FOLDER_DISK, FOLDER_MEMORY, UserKeyDefinition } from "../../../platform/state"; import { FolderData } from "../../models/data/folder.data"; import { FolderView } from "../../models/view/folder.view"; @@ -8,7 +6,7 @@ export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record( FOLDER_DISK, "folders", { - deserializer: (obj: Jsonify) => FolderData.fromJSON(obj), + deserializer: (obj) => FolderData.fromJSON(obj!), clearOn: ["logout"], }, ); @@ -17,7 +15,7 @@ export const FOLDER_DECRYPTED_FOLDERS = new UserKeyDefinition( FOLDER_MEMORY, "decryptedFolders", { - deserializer: (obj: Jsonify) => obj?.map((f) => FolderView.fromJSON(f)) ?? [], + deserializer: (obj) => obj?.map((f) => FolderView.fromJSON(f)) ?? [], clearOn: ["logout", "lock"], }, ); diff --git a/libs/common/src/vault/tasks/state/security-task.state.ts b/libs/common/src/vault/tasks/state/security-task.state.ts index b86a891f008..e4c42a5ea39 100644 --- a/libs/common/src/vault/tasks/state/security-task.state.ts +++ b/libs/common/src/vault/tasks/state/security-task.state.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { SECURITY_TASKS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; import { SecurityTaskData } from "../models/security-task.data"; @@ -8,7 +6,7 @@ export const SECURITY_TASKS = UserKeyDefinition.array( SECURITY_TASKS_DISK, "securityTasks", { - deserializer: (task: Jsonify) => SecurityTaskData.fromJSON(task), + deserializer: (task) => SecurityTaskData.fromJSON(task!), clearOn: ["logout", "lock"], }, ); diff --git a/libs/key-management/src/biometrics/biometric.state.ts b/libs/key-management/src/biometrics/biometric.state.ts index 277d35c176e..a3132707d58 100644 --- a/libs/key-management/src/biometrics/biometric.state.ts +++ b/libs/key-management/src/biometrics/biometric.state.ts @@ -103,6 +103,6 @@ export const LAST_PROCESS_RELOAD = new KeyDefinition( BIOMETRIC_SETTINGS_DISK, "lastProcessReload", { - deserializer: (obj) => new Date(obj), + deserializer: (obj) => new Date(obj!), }, ); diff --git a/libs/key-management/src/kdf-config.service.ts b/libs/key-management/src/kdf-config.service.ts index 24635e87580..f01c77da851 100644 --- a/libs/key-management/src/kdf-config.service.ts +++ b/libs/key-management/src/kdf-config.service.ts @@ -13,7 +13,7 @@ import { KdfType } from "./enums/kdf-type.enum"; import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "./models/kdf-config"; export const KDF_CONFIG = new UserKeyDefinition(KDF_CONFIG_DISK, "kdfConfig", { - deserializer: (kdfConfig: Jsonify) => { + deserializer: (kdfConfig: Jsonify | null) => { if (kdfConfig == null) { return null; } diff --git a/libs/tools/generator/extensions/history/src/key-definitions.ts b/libs/tools/generator/extensions/history/src/key-definitions.ts index 187a6c8fc94..d75461bc328 100644 --- a/libs/tools/generator/extensions/history/src/key-definitions.ts +++ b/libs/tools/generator/extensions/history/src/key-definitions.ts @@ -15,7 +15,7 @@ export const GENERATOR_HISTORY = SecretKeyDefinition.array( "localGeneratorHistory", SecretClassifier.allSecret(), { - deserializer: GeneratedCredential.fromJSON, + deserializer: (obj) => GeneratedCredential.fromJSON(obj!), clearOn: ["logout"], }, ); diff --git a/package-lock.json b/package-lock.json index 115f7cbf32d..ec9b069f2b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -179,6 +179,7 @@ "ts-loader": "9.5.2", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", + "type-fest-v4": "npm:type-fest@4.41.0", "typescript": "5.5.4", "typescript-eslint": "8.31.0", "typescript-strict-plugin": "2.4.4", @@ -37241,6 +37242,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-fest-v4": { + "name": "type-fest", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index be0c53914b8..ae84fb08157 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "ts-loader": "9.5.2", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", + "type-fest-v4": "npm:type-fest@4.41.0", "typescript": "5.5.4", "typescript-eslint": "8.31.0", "typescript-strict-plugin": "2.4.4",