1 import type { IdentityValues, ItemRevision, ItemType } from '@proton/pass/types';
2 import { deobfuscate } from '@proton/pass/utils/obfuscate/xor';
3 import { normalize } from '@proton/shared/lib/helpers/string';
5 import { matchEvery } from './match-every';
6 import type { ItemMatchFunc, ItemMatchFuncMap } from './types';
8 /** Matches a single field from the item using a getter function,
9 * enabling lazy evaluation when used with `combineMatchers`. */
11 <T extends ItemType>(getter: (item: ItemRevision<T>) => string): ItemMatchFunc<T> =>
14 matchEvery(needles)(getter(item));
16 /** Matches any field from an array returned by the getter function. */
18 <T extends ItemType>(getter: (item: ItemRevision<T>) => string[]): ItemMatchFunc<T> =>
21 getter(item).some((field) => matchEvery(needles)(field));
23 /** Matches fields from an `IterableIterator` returned by the getter function.
24 * Uses lazy evaluation and early return for efficiency. */
25 const matchFieldsLazy =
26 <T extends ItemType>(getter: (item: ItemRevision<T>) => IterableIterator<string>): ItemMatchFunc<T> =>
29 for (const field of getter(item)) if (matchEvery(needles)(field)) return true;
33 /** Combines multiple matchers and checks if any of them match the needles.
34 * Uses lazy evaluation and early return via `some` for efficiency. */
35 const combineMatchers =
36 <T extends ItemType>(...matchers: ItemMatchFunc<T>[]): ItemMatchFunc<T> =>
39 matchers.some((matcher) => matcher(item)(needles));
41 const matchesNoteItem: ItemMatchFunc<'note'> = combineMatchers<'note'>(
42 matchField((item) => item.data.metadata.name),
43 matchField((item) => deobfuscate(item.data.metadata.note))
46 const matchesLoginItem: ItemMatchFunc<'login'> = combineMatchers<'login'>(
47 matchField((item) => item.data.metadata.name),
48 matchField((item) => deobfuscate(item.data.metadata.note)),
49 matchField((item) => deobfuscate(item.data.content.itemEmail)),
50 matchField((item) => deobfuscate(item.data.content.itemUsername)),
51 matchFields((item) => item.data.content.urls),
52 matchFieldsLazy(function* matchExtraFields(item): IterableIterator<string> {
53 for (const field of item.data.extraFields) {
54 if (field.type !== 'totp') yield `${field.fieldName} ${deobfuscate(field.data.content)}`;
59 const matchesAliasItem: ItemMatchFunc<'alias'> = combineMatchers<'alias'>(
60 matchField((item) => item.data.metadata.name),
61 matchField((item) => deobfuscate(item.data.metadata.note)),
62 matchField((item) => item.aliasEmail ?? '')
65 const matchesCreditCardItem: ItemMatchFunc<'creditCard'> = combineMatchers<'creditCard'>(
66 matchField((item) => item.data.metadata.name),
67 matchField((item) => deobfuscate(item.data.metadata.note)),
68 matchField((item) => item.data.content.cardholderName),
69 matchField((item) => deobfuscate(item.data.content.number))
72 const matchesIdentityItem: ItemMatchFunc<'identity'> = combineMatchers<'identity'>(
73 matchField((item) => item.data.metadata.name),
74 matchField((item) => deobfuscate(item.data.metadata.note)),
75 matchFieldsLazy(function* matchIdentityFields(item): IterableIterator<string> {
76 for (const key of Object.keys(item.data.content) as (keyof IdentityValues)[]) {
77 const value = item.data.content[key];
78 if (typeof value === 'string') yield value;
81 case 'extraAddressDetails':
82 case 'extraContactDetails':
83 case 'extraPersonalDetails':
84 case 'extraWorkDetails': {
85 for (const field of item.data.content[key]) {
86 if (field.type !== 'totp') yield field.data.content;
90 case 'extraSections': {
91 for (const section of item.data.content[key]) {
92 yield section.sectionName;
93 for (const field of section.sectionFields) {
94 if (field.type !== 'totp') yield field.data.content;
105 /* Each item should expose its own searching mechanism :
106 * we may include/exclude certain fields or add extra criteria
107 * depending on the type of item we're targeting */
108 const itemMatchers: ItemMatchFuncMap = {
109 login: matchesLoginItem,
110 note: matchesNoteItem,
111 alias: matchesAliasItem,
112 creditCard: matchesCreditCardItem,
113 identity: matchesIdentityItem,
116 const matchItem: ItemMatchFunc = <T extends ItemType>(item: ItemRevision<T>) => itemMatchers[item.data.type](item);
118 export const searchItems = <T extends ItemRevision>(items: T[], search?: string) => {
119 if (!search || search.trim() === '') return items;
121 /** split the search term into multiple normalized needles */
122 const needles = normalize(search, true).split(' ');
123 return items.filter((item) => matchItem(item)(needles));