1use crate::{Error, Result};
3use probly_search::{score::bm25, Index, QueryResult};
4use serde::{Deserialize, Serialize};
5use sos_backend::AccessPoint;
6use sos_core::{crypto::AccessKey, VaultId};
7use sos_vault::{
8 secret::{Secret, SecretId, SecretMeta, SecretRef, SecretType},
9 SecretAccess, Summary, Vault,
10};
11use std::{
12 borrow::Cow,
13 collections::{btree_map::Values, BTreeMap, HashMap, HashSet},
14 sync::Arc,
15};
16use tokio::sync::RwLock;
17use unicode_segmentation::UnicodeSegmentation;
18use url::Url;
19
20#[doc(hidden)]
22pub fn ngram_slice(s: &str, n: usize) -> HashSet<&str> {
23 let mut items: HashSet<&str> = HashSet::new();
24 let graphemes: Vec<usize> =
25 s.grapheme_indices(true).map(|v| v.0).collect();
26 for (index, offset) in graphemes.iter().enumerate() {
27 if let Some(end_offset) = graphemes.get(index + n) {
28 items.insert(&s[*offset..*end_offset]);
29 } else {
30 let mut end_offset = offset;
31 for i in 1..n {
32 if let Some(end) = graphemes.get(index + i) {
33 end_offset = end;
34 }
35 }
36 if end_offset > offset {
37 let val = &s[*offset..*end_offset];
38 items.insert(val);
39 }
40 }
41 }
42 items
43}
44
45#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize)]
47pub struct DocumentKey(String, VaultId, SecretId);
48
49fn tokenizer(s: &str) -> Vec<Cow<'_, str>> {
51 let words = s.split(' ').collect::<HashSet<_>>();
52
53 let ngram2 = ngram_slice(s, 2);
54 let ngram3 = ngram_slice(s, 3);
55 let ngram4 = ngram_slice(s, 4);
56 let ngram5 = ngram_slice(s, 5);
57 let ngram: HashSet<&str> = ngram2.union(&ngram3).map(|s| &**s).collect();
58 let ngram: HashSet<&str> = ngram.union(&ngram4).map(|s| &**s).collect();
59 let ngram: HashSet<&str> = ngram.union(&ngram5).map(|s| &**s).collect();
60
61 let mut tokens: Vec<Cow<str>> = Vec::new();
62 for token in ngram.union(&words) {
63 tokens.push(Cow::Owned(token.to_lowercase()))
64 }
65 tokens
66}
67
68fn query_tokenizer(s: &str) -> Vec<Cow<'_, str>> {
70 s.split(' ')
71 .map(|s| s.to_lowercase())
72 .map(Cow::Owned)
73 .collect::<Vec<_>>()
74}
75
76fn label_extract(d: &Document) -> Vec<&str> {
78 vec![d.meta().label()]
79}
80
81fn tags_extract(d: &Document) -> Vec<&str> {
83 d.meta().tags().iter().map(|s| &s[..]).collect()
84}
85
86fn comment_extract(d: &Document) -> Vec<&str> {
88 if let Some(comment) = d.extra().comment() {
89 vec![comment]
90 } else {
91 vec![""]
92 }
93}
94
95fn website_extract(d: &Document) -> Vec<&str> {
97 if let Some(websites) = d.extra().websites() {
98 websites
99 } else {
101 vec![]
102 }
103}
104
105#[derive(Default, Debug, Clone)]
107pub struct DocumentCount {
108 vaults: HashMap<VaultId, usize>,
110 kinds: HashMap<u8, usize>,
112 tags: HashMap<String, usize>,
114 favorites: usize,
116 archive: Option<VaultId>,
122}
123
124impl DocumentCount {
125 pub fn new(archive: Option<VaultId>) -> Self {
127 Self {
128 vaults: Default::default(),
129 kinds: Default::default(),
130 tags: Default::default(),
131 favorites: Default::default(),
132 archive,
133 }
134 }
135
136 pub fn set_archive_id(&mut self, archive: Option<VaultId>) {
138 self.archive = archive;
139 }
140
141 pub fn vaults(&self) -> &HashMap<VaultId, usize> {
143 &self.vaults
144 }
145
146 pub fn kinds(&self) -> &HashMap<u8, usize> {
148 &self.kinds
149 }
150
151 pub fn tags(&self) -> &HashMap<String, usize> {
153 &self.tags
154 }
155
156 pub fn favorites(&self) -> usize {
158 self.favorites
159 }
160
161 fn is_archived(&self, folder_id: &VaultId) -> bool {
164 if let Some(archive) = &self.archive {
165 return folder_id == archive;
166 }
167 false
168 }
169
170 fn remove(
172 &mut self,
173 folder_id: VaultId,
174 mut options: Option<(u8, HashSet<String>, bool)>,
175 ) {
176 self.vaults
177 .entry(folder_id)
178 .and_modify(|counter| {
179 if *counter > 0 {
180 *counter -= 1;
181 }
182 })
183 .or_insert(0);
184
185 if let Some((kind, tags, favorite)) = options.take() {
186 if !self.is_archived(&folder_id) {
187 self.kinds
188 .entry(kind)
189 .and_modify(|counter| {
190 if *counter > 0 {
191 *counter -= 1;
192 }
193 })
194 .or_insert(0);
195 }
196
197 for tag in &tags {
198 self.tags
199 .entry(tag.to_owned())
200 .and_modify(|counter| {
201 if *counter > 0 {
202 *counter -= 1;
203 }
204 })
205 .or_insert(0);
206
207 let value = self.tags.get(tag).unwrap_or(&0);
209 if *value == 0 {
210 self.tags.remove(tag);
211 }
212 }
213
214 if favorite && self.favorites > 0 {
215 self.favorites -= 1;
216 }
217 }
218 }
219
220 fn add(
222 &mut self,
223 folder_id: VaultId,
224 kind: u8,
225 tags: &HashSet<String>,
226 favorite: bool,
227 ) {
228 self.vaults
229 .entry(folder_id)
230 .and_modify(|counter| *counter += 1)
231 .or_insert(1);
232
233 if !self.is_archived(&folder_id) {
234 self.kinds
235 .entry(kind)
236 .and_modify(|counter| *counter += 1)
237 .or_insert(1);
238 }
239 for tag in tags {
240 self.tags
241 .entry(tag.to_owned())
242 .and_modify(|counter| *counter += 1)
243 .or_insert(1);
244 }
245
246 if favorite {
247 self.favorites += 1;
248 }
249 }
250}
251
252#[derive(Debug)]
254pub struct IndexStatistics {
255 count: DocumentCount,
257}
258
259impl IndexStatistics {
260 pub fn new(archive: Option<VaultId>) -> Self {
262 Self {
263 count: DocumentCount::new(archive),
264 }
265 }
266
267 pub fn set_archive_id(&mut self, archive: Option<VaultId>) {
269 self.count.set_archive_id(archive);
270 }
271
272 pub fn count(&self) -> &DocumentCount {
274 &self.count
275 }
276}
277
278#[typeshare::typeshare]
282#[derive(Default, Debug, Serialize, Deserialize, Clone)]
283#[serde(rename_all = "camelCase")]
284pub struct ExtraFields {
285 pub comment: Option<String>,
287 pub contact_type: Option<vcard4::property::Kind>,
289 pub websites: Option<Vec<String>>,
291}
292
293impl From<&Secret> for ExtraFields {
294 fn from(value: &Secret) -> Self {
295 let mut extra = ExtraFields {
296 comment: value.user_data().comment().map(|c| c.to_owned()),
297 websites: value
298 .websites()
299 .map(|w| w.into_iter().map(|u| u.to_string()).collect()),
300 ..Default::default()
301 };
302 if let Secret::Contact { vcard, .. } = value {
303 extra.contact_type = vcard
304 .kind
305 .as_ref()
306 .map(|p| p.value.clone())
307 .or(Some(vcard4::property::Kind::Individual));
308 }
309 extra
310 }
311}
312
313impl ExtraFields {
314 pub fn comment(&self) -> Option<&str> {
316 self.comment.as_ref().map(|c| &c[..])
317 }
318
319 pub fn websites(&self) -> Option<Vec<&str>> {
321 self.websites
322 .as_ref()
323 .map(|u| u.into_iter().map(|u| &u[..]).collect())
324 }
325}
326
327#[typeshare::typeshare]
329#[derive(Debug, Serialize, Deserialize, Clone)]
330#[serde(rename_all = "camelCase")]
331pub struct Document {
332 pub folder_id: VaultId,
334 pub secret_id: SecretId,
336 pub meta: SecretMeta,
338 pub extra: ExtraFields,
340}
341
342impl Document {
343 pub fn folder_id(&self) -> &VaultId {
345 &self.folder_id
346 }
347
348 pub fn id(&self) -> &SecretId {
350 &self.secret_id
351 }
352
353 pub fn meta(&self) -> &SecretMeta {
355 &self.meta
356 }
357
358 pub fn extra(&self) -> &ExtraFields {
360 &self.extra
361 }
362}
363
364pub struct SearchIndex {
366 index: Index<(VaultId, SecretId)>,
367 documents: BTreeMap<DocumentKey, Document>,
368 statistics: IndexStatistics,
369}
370
371impl Default for SearchIndex {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377impl SearchIndex {
378 pub fn new() -> Self {
380 let index = Index::<(VaultId, SecretId)>::new(4);
382 Self {
383 index,
384 documents: Default::default(),
385 statistics: IndexStatistics::new(None),
386 }
387 }
388
389 pub fn set_archive_id(&mut self, archive: Option<VaultId>) {
391 self.statistics.set_archive_id(archive);
392 }
393
394 pub fn statistics(&self) -> &IndexStatistics {
396 &self.statistics
397 }
398
399 pub fn documents(&self) -> &BTreeMap<DocumentKey, Document> {
401 &self.documents
402 }
403
404 pub fn values(&self) -> Vec<&Document> {
406 self.documents.values().collect::<Vec<_>>()
407 }
408
409 pub fn values_iter(&self) -> Values<'_, DocumentKey, Document> {
411 self.documents.values()
412 }
413
414 pub fn len(&self) -> usize {
416 self.documents.len()
417 }
418
419 pub fn is_empty(&self) -> bool {
421 self.len() == 0
422 }
423
424 pub fn find_by_label<'a>(
428 &'a self,
429 folder_id: &VaultId,
430 label: &str,
431 id: Option<&SecretId>,
432 ) -> Option<&'a Document> {
433 self.documents
434 .values()
435 .filter(|d| {
436 if let Some(id) = id {
437 id != d.id()
438 } else {
439 true
440 }
441 })
442 .find(|d| d.folder_id() == folder_id && d.meta().label() == label)
443 }
444
445 pub fn find_by_label_any<'a>(
447 &'a self,
448 label: &str,
449 id: Option<&SecretId>,
450 case_insensitive: bool,
451 ) -> Option<&'a Document> {
452 self.documents
453 .values()
454 .filter(|d| {
455 if let Some(id) = id {
456 id != d.id()
457 } else {
458 true
459 }
460 })
461 .find(|d| {
462 if case_insensitive {
463 d.meta().label().to_lowercase() == label.to_lowercase()
464 } else {
465 d.meta().label() == label
466 }
467 })
468 }
469
470 pub fn find_all_by_label<'a>(
475 &'a self,
476 label: &str,
477 id: Option<&SecretId>,
478 ) -> Vec<&'a Document> {
479 self.documents
480 .iter()
481 .filter(|(k, v)| {
482 if let Some(id) = id {
483 if id == &k.1 {
484 false
485 } else {
486 v.meta().label() == label
487 }
488 } else {
489 v.meta().label() == label
490 }
491 })
492 .map(|(_, v)| v)
493 .collect::<Vec<_>>()
494 }
495
496 pub fn find_by_id<'a>(
498 &'a self,
499 folder_id: &VaultId,
500 id: &SecretId,
501 ) -> Option<&'a Document> {
502 self.documents
503 .values()
504 .find(|d| d.folder_id() == folder_id && d.id() == id)
505 }
506
507 pub fn find_by_uuid_or_label<'a>(
511 &'a self,
512 folder_id: &VaultId,
513 target: &SecretRef,
514 ) -> Option<&'a Document> {
515 match target {
516 SecretRef::Id(id) => self.find_by_id(folder_id, id),
517 SecretRef::Name(name) => {
518 self.find_by_label(folder_id, name, None)
519 }
520 }
521 }
522
523 pub fn prepare(
528 &self,
529 folder_id: &VaultId,
530 id: &SecretId,
531 meta: &SecretMeta,
532 secret: &Secret,
533 ) -> Option<(DocumentKey, Document)> {
534 if self.find_by_id(folder_id, id).is_none() {
536 let doc = Document {
537 folder_id: *folder_id,
538 secret_id: *id,
539 meta: meta.clone(),
540 extra: secret.into(),
541 };
542
543 let key = DocumentKey(
546 doc.meta().label().to_lowercase(),
547 *folder_id,
548 *id,
549 );
550
551 Some((key, doc))
552 } else {
553 None
554 }
555 }
556
557 pub fn commit(&mut self, doc: Option<(DocumentKey, Document)>) {
559 if let Some((key, doc)) = doc {
561 let exists = self.documents.get(&key).is_some();
562 let doc = self.documents.entry(key).or_insert(doc);
563 if !exists {
564 self.index.add_document(
565 &[
566 label_extract,
567 tags_extract,
568 comment_extract,
569 website_extract,
570 ],
571 tokenizer,
572 (doc.folder_id, doc.secret_id),
573 doc,
574 );
575
576 self.statistics.count.add(
577 doc.folder_id,
578 doc.meta().kind().into(),
579 doc.meta().tags(),
580 doc.meta().favorite(),
581 );
582 }
583 }
584 }
585
586 pub fn add(
588 &mut self,
589 folder_id: &VaultId,
590 id: &SecretId,
591 meta: &SecretMeta,
592 secret: &Secret,
593 ) {
594 self.commit(self.prepare(folder_id, id, meta, secret));
595 }
596
597 pub fn update(
599 &mut self,
600 folder_id: &VaultId,
601 id: &SecretId,
602 meta: &SecretMeta,
603 secret: &Secret,
604 ) {
605 self.remove(folder_id, id);
606 self.add(folder_id, id, meta, secret);
607 }
608
609 pub async fn add_folder(&mut self, folder: &AccessPoint) -> Result<()> {
612 let vault = folder.vault();
613 for id in vault.keys() {
614 let (meta, secret, _) = folder
615 .read_secret(id)
616 .await?
617 .ok_or_else(|| Error::NoSecretId(*folder.id(), *id))?;
618 self.add(folder.id(), id, &meta, &secret);
619 }
620 Ok(())
621 }
622
623 pub async fn remove_folder(
625 &mut self,
626 folder: &AccessPoint,
627 ) -> Result<()> {
628 let vault = folder.vault();
629 for id in vault.keys() {
630 self.remove(folder.id(), id);
631 }
632 Ok(())
633 }
634
635 pub fn remove(&mut self, folder_id: &VaultId, id: &SecretId) {
637 let key = self
638 .documents
639 .keys()
640 .find(|key| &key.1 == folder_id && &key.2 == id)
641 .cloned();
642 let doc_info = if let Some(key) = &key {
643 let doc = self.documents.remove(key);
644 doc.map(|doc| {
645 let kind: u8 = doc.meta().kind().into();
646 (kind, doc.meta().tags().clone(), doc.meta().favorite())
647 })
648 } else {
649 None
650 };
651
652 self.index.remove_document((*folder_id, *id));
653 self.index.vacuum();
655
656 self.statistics.count.remove(*folder_id, doc_info);
657 }
658
659 pub fn remove_vault(&mut self, folder_id: &VaultId) {
661 let keys: Vec<DocumentKey> = self
662 .documents
663 .keys()
664 .filter(|k| &k.1 == folder_id)
665 .cloned()
666 .collect();
667 for key in keys {
668 self.remove(&key.1, &key.2);
669 self.documents.remove(&key);
670 }
671 }
672
673 pub fn remove_all(&mut self) {
678 let keys: Vec<DocumentKey> = self.documents.keys().cloned().collect();
679 for key in keys {
680 self.remove(&key.1, &key.2);
681 self.documents.remove(&key);
682 }
683 }
684
685 pub fn query(
687 &self,
688 needle: &str,
689 ) -> Vec<QueryResult<(VaultId, SecretId)>> {
690 self.index.query(
691 needle,
692 &mut bm25::new(),
693 query_tokenizer,
694 &[1., 1., 1., 1.],
695 )
696 }
697
698 pub fn query_map(
703 &self,
704 needle: &str,
705 predicate: impl Fn(&Document) -> bool,
706 ) -> Vec<&Document> {
707 let results = self.query(needle);
708 results
709 .into_iter()
710 .filter_map(|r| {
711 self.find_by_id(&r.key.0, &r.key.1)
712 .filter(|&doc| predicate(doc))
713 })
714 .collect::<Vec<_>>()
715 }
716}
717
718#[derive(Default, Debug, Serialize, Deserialize)]
720pub struct AccountStatistics {
721 pub documents: usize,
723 pub folders: Vec<(Summary, usize)>,
725 pub tags: HashMap<String, usize>,
727 pub types: HashMap<SecretType, usize>,
729 pub favorites: usize,
731}
732
733pub struct AccountSearch {
735 pub search_index: Arc<RwLock<SearchIndex>>,
737}
738
739impl Default for AccountSearch {
740 fn default() -> Self {
741 Self::new()
742 }
743}
744
745impl AccountSearch {
746 pub fn new() -> Self {
748 Self {
749 search_index: Arc::new(RwLock::new(SearchIndex::new())),
750 }
751 }
752
753 #[doc(hidden)]
755 pub fn search(&self) -> Arc<RwLock<SearchIndex>> {
756 Arc::clone(&self.search_index)
757 }
758
759 pub async fn clear(&mut self) {
761 let mut writer = self.search_index.write().await;
762 writer.remove_all();
763 }
764
765 pub async fn add_folder(&self, folder: &AccessPoint) -> Result<()> {
767 let mut index = self.search_index.write().await;
768 index.add_folder(folder).await
769 }
770
771 pub async fn remove_folder(&self, folder_id: &VaultId) {
773 let mut writer = self.search_index.write().await;
775 writer.remove_vault(folder_id);
776 }
777
778 pub async fn add_vault(
780 &self,
781 vault: Vault,
782 key: &AccessKey,
783 ) -> Result<()> {
784 let mut index = self.search_index.write().await;
785 let mut keeper = AccessPoint::from_vault(vault);
786 keeper.unlock(key).await?;
787 index.add_folder(&keeper).await?;
788 keeper.lock();
789 Ok(())
790 }
791
792 pub async fn document_count(&self) -> DocumentCount {
794 let reader = self.search_index.read().await;
795 reader.statistics().count().clone()
796 }
797
798 pub async fn document_exists(
800 &self,
801 folder_id: &VaultId,
802 label: &str,
803 id: Option<&SecretId>,
804 ) -> bool {
805 let reader = self.search_index.read().await;
806 reader.find_by_label(folder_id, label, id).is_some()
807 }
808
809 pub async fn query_view(
811 &self,
812 views: &[DocumentView],
813 archive: Option<&ArchiveFilter>,
814 ) -> Result<Vec<Document>> {
815 let index_reader = self.search_index.read().await;
816 let mut docs = Vec::with_capacity(index_reader.len());
817 for doc in index_reader.values_iter() {
818 for view in views {
819 if view.test(doc, archive) {
820 docs.push(doc.clone());
821 }
822 }
823 }
824 Ok(docs)
825 }
826
827 pub async fn query_map(
829 &self,
830 query: &str,
831 filter: QueryFilter,
832 ) -> Result<Vec<Document>> {
833 let index_reader = self.search_index.read().await;
834 let mut docs = Vec::new();
835 let tags: HashSet<_> = filter.tags.iter().cloned().collect();
836 let predicate = self.query_predicate(filter, tags);
837 if !query.is_empty() {
838 for doc in index_reader.query_map(query, predicate) {
839 docs.push(doc.clone());
840 }
841 } else {
842 for doc in index_reader.values_iter() {
843 if predicate(doc) {
844 docs.push(doc.clone());
845 }
846 }
847 }
848 Ok(docs)
849 }
850
851 fn query_predicate(
852 &self,
853 filter: QueryFilter,
854 tags: HashSet<String>,
855 ) -> impl Fn(&Document) -> bool {
856 move |doc| {
857 let tag_match = filter.tags.is_empty() || {
858 !tags
859 .intersection(doc.meta().tags())
860 .collect::<HashSet<_>>()
861 .is_empty()
862 };
863
864 let folder_id = doc.folder_id();
865 let folder_match = filter.folders.is_empty()
866 || filter.folders.contains(folder_id);
867
868 let type_match = filter.types.is_empty()
869 || filter.types.contains(doc.meta().kind());
870
871 tag_match && folder_match && type_match
872 }
873 }
874}
875
876#[typeshare::typeshare]
878#[derive(Debug, Clone, Serialize, Deserialize)]
879#[serde(rename_all = "camelCase", tag = "kind", content = "body")]
880pub enum DocumentView {
881 All {
883 #[serde(rename = "ignoredTypes")]
885 ignored_types: Option<Vec<SecretType>>,
886 },
887 Vault(VaultId),
889 TypeId(SecretType),
891 Favorites,
893 Tags(Vec<String>),
895 Contact {
897 include_types: Option<Vec<vcard4::property::Kind>>,
901 },
902 Documents {
904 #[serde(rename = "folderId")]
906 folder_id: VaultId,
907 identifiers: Vec<SecretId>,
909 },
910 Websites {
912 matches: Option<Vec<Url>>,
914 exact: bool,
918 },
919}
920
921impl Default for DocumentView {
922 fn default() -> Self {
923 Self::All {
924 ignored_types: None,
925 }
926 }
927}
928
929impl DocumentView {
930 pub fn test(
932 &self,
933 doc: &Document,
934 archive: Option<&ArchiveFilter>,
935 ) -> bool {
936 if let Some(filter) = archive {
937 if !filter.include_documents && doc.folder_id() == &filter.id {
938 return false;
939 }
940 }
941 match self {
942 DocumentView::All { ignored_types } => {
943 if let Some(ignored_types) = ignored_types {
944 return !ignored_types.contains(doc.meta().kind());
945 }
946 true
947 }
948 DocumentView::Vault(folder_id) => doc.folder_id() == folder_id,
949 DocumentView::TypeId(type_id) => doc.meta().kind() == type_id,
950 DocumentView::Favorites => doc.meta().favorite(),
951 DocumentView::Tags(tags) => {
952 let tags: HashSet<_> = tags.iter().cloned().collect();
953 !tags
954 .intersection(doc.meta().tags())
955 .collect::<HashSet<_>>()
956 .is_empty()
957 }
958 DocumentView::Contact { include_types } => {
959 if doc.meta().kind() == &SecretType::Contact {
960 if let Some(include_types) = include_types {
961 if let Some(contact_type) = &doc.extra().contact_type
962 {
963 let contact_type: vcard4::property::Kind =
964 contact_type.clone();
965 return include_types.contains(&contact_type);
966 } else {
967 return false;
968 }
969 }
970 return true;
971 }
972 false
973 }
974 DocumentView::Documents {
975 folder_id,
976 identifiers,
977 } => {
978 doc.folder_id() == folder_id && identifiers.contains(doc.id())
979 }
980 DocumentView::Websites { matches, exact } => {
981 if let Some(sites) = doc.extra().websites() {
982 if sites.is_empty() {
983 false
984 } else {
985 if let Some(targets) = matches {
986 let mut urls: Vec<Url> =
989 Vec::with_capacity(sites.len());
990 for site in sites {
991 match site.parse() {
992 Ok(url) => urls.push(url),
993 Err(e) => {
994 tracing::warn!(
995 error = %e,
996 "search::url_parse");
997 }
998 }
999 }
1000
1001 if *exact {
1002 for url in targets {
1003 if urls.contains(url) {
1004 return true;
1005 }
1006 }
1007 false
1008 } else {
1009 for url in targets {
1010 for site in &urls {
1011 if url.origin() == site.origin() {
1012 return true;
1013 }
1014 }
1015 }
1016 false
1017 }
1018 } else {
1019 true
1022 }
1023 }
1024 } else {
1025 false
1026 }
1027 }
1028 }
1029 }
1030}
1031
1032#[typeshare::typeshare]
1034#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1035pub struct QueryFilter {
1036 pub tags: Vec<String>,
1038 pub folders: Vec<VaultId>,
1040 pub types: Vec<SecretType>,
1042}
1043
1044#[typeshare::typeshare]
1046#[derive(Debug, Clone, Serialize, Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048pub struct ArchiveFilter {
1049 pub id: VaultId,
1051 pub include_documents: bool,
1053}