1pub mod catalog;
7pub mod denylist;
8pub mod links;
9pub mod parallel;
10pub mod reputation;
11pub mod resilience;
12pub mod wot;
13pub use catalog::{Catalog, CatalogMetadata, CatalogRecord, CatalogService};
14pub use denylist::{Denied, DenyReason, Denylist, DenylistRule, DenylistService, FilterContext, FilterList};
15pub use links::{
16 CapabilityLink, ContentLink, ImmutableLink, Link, LinkResolution, LinkResolver, Mirror, MutableLink,
17 MutablePointer, NameLink, PrivateLink, ProviderFetch, REVOCATION_GOSSIP_TOPIC, ResolveOptions, ResolvedLink,
18 SignedMutablePointer, SyncwebLink, current_epoch_seconds, fetch_from_mirrors, revocation_topic_id,
19};
20pub use parallel::{ParallelDownloadConfig, TryParallelResult};
21pub use reputation::{
22 ProviderReputation, ProviderReputationStore, ProviderTrustSignal, ReputationConfig, TrustSignalKind,
23 trust_stream_topic,
24};
25pub use resilience::{
26 AvailabilityHealth, BanRecord, BanSource, FailureRecord, FetchFailure, FetchFailureKind, FetchWait, LeaseUpdate,
27 ProviderLease, ProviderLeaseTracker, ReplicationBudget, ReplicationResult, ResilienceConfig, ResilienceService,
28 consistent_hashing_selection, jitter_delay, resilience_topic, validate_bounded_fetch, validate_bounded_stream,
29 validate_fetch_stream, xor_distance,
30};
31pub use wot::{
32 Attestation, AttestationKind, MetadataEntry, ModerationAction, ModerationContext, ModerationDecision,
33 ModerationRecord, ModerationScope, ProviderTrustAction, ProviderTrustDecision, ProviderTrustRecord,
34 RevocationRecord, TrustDecision, TrustDelegation, TrustPolicy, WotMetadata, WotService,
35};
36
37use std::{
38 collections::HashMap,
39 path::{Path, PathBuf},
40 str::FromStr,
41 sync::{Arc, Mutex},
42 time::{SystemTime, UNIX_EPOCH},
43};
44
45use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
46use iroh::PublicKey;
47use iroh_blobs::Hash;
48use iroh_docs::{Entry, NamespaceId, engine::LiveEvent};
49use n0_future::StreamExt;
50use rusqlite::{Connection, OptionalExtension, params};
51use serde::{Deserialize, Serialize};
52use tokio::{sync::broadcast, task::JoinHandle};
53
54use crate::{
55 error::{Result, SyncwebError},
56 folder::SyncwebFolder,
57 gossip::SignedGossipMessage,
58};
59
60pub const SCHEMA_VERSION: &str = "1";
62const EVENT_CAPACITY: usize = 256;
63
64#[derive(Clone, Debug, Eq, PartialEq)]
66#[non_exhaustive]
67pub struct IndexedEntry {
68 pub namespace_id: NamespaceId,
69 pub key: Vec<u8>,
70 pub hash: Hash,
71 pub size: u64,
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76#[non_exhaustive]
77pub enum CoreIndexingEvent {
78 ContentReady { hash: Hash },
79 PendingContentReady,
80 NeighborUp,
81 NeighborDown,
82 SyncFinished,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
87#[non_exhaustive]
88pub enum IndexingEvent {
89 FolderEnabled {
90 namespace_id: NamespaceId,
91 },
92 FolderDisabled {
93 namespace_id: NamespaceId,
94 },
95 EntryIndexed(IndexedEntry),
96 Core {
97 namespace_id: NamespaceId,
98 event: CoreIndexingEvent,
99 },
100 Error {
101 namespace_id: NamespaceId,
102 message: String,
103 },
104}
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108#[non_exhaustive]
109pub struct IndexingHandle {
110 namespace_id: NamespaceId,
111}
112
113impl IndexingHandle {
114 #[must_use]
115 pub const fn namespace_id(self) -> NamespaceId {
116 self.namespace_id
117 }
118}
119
120#[derive(Clone)]
122pub struct IndexingDatabase {
123 connection: Arc<Mutex<Connection>>,
124 path: Arc<PathBuf>,
125}
126
127impl std::fmt::Debug for IndexingDatabase {
128 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 formatter
130 .debug_struct("IndexingDatabase")
131 .field("path", &self.path)
132 .finish_non_exhaustive()
133 }
134}
135
136impl IndexingDatabase {
137 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
143 let database_path = path.as_ref().to_path_buf();
144 if database_path.as_os_str() != ":memory:"
145 && let Some(parent) = database_path.parent()
146 && !parent.as_os_str().is_empty()
147 {
148 std::fs::create_dir_all(parent)
149 .map_err(|error| SyncwebError::operation("failed to create indexing database directory", error))?;
150 }
151 let connection = Connection::open(&database_path)
152 .map_err(|error| SyncwebError::operation("failed to open indexing database", error))?;
153 initialize_connection(&connection)?;
154 Ok(Self {
155 connection: Arc::new(Mutex::new(connection)),
156 path: Arc::new(database_path),
157 })
158 }
159
160 pub fn in_memory() -> Result<Self> {
166 Self::open(":memory:")
167 }
168
169 #[must_use]
170 pub fn path(&self) -> &Path {
171 self.path.as_path()
172 }
173
174 pub fn schema_version(&self) -> Result<String> {
180 self.with_connection(|connection| {
181 connection
182 .query_row(
183 "SELECT value FROM index_metadata WHERE key = 'schema_version'",
184 [],
185 |row| row.get(0),
186 )
187 .map_err(|error| database_error("failed to read indexing schema version", error))
188 })
189 }
190
191 pub fn has_fts5(&self) -> Result<bool> {
197 self.has_table("indexed_entries_fts")
198 }
199
200 pub fn schema_objects(&self) -> Result<Vec<String>> {
206 self.with_connection(|connection| {
207 let mut statement = connection
208 .prepare("SELECT name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' ORDER BY name")
209 .map_err(|error| database_error("failed to prepare schema query", error))?;
210 let names = statement
211 .query_map([], |row| row.get::<_, String>(0))
212 .map_err(|error| database_error("failed to query schema objects", error))?
213 .collect::<std::result::Result<Vec<_>, _>>()
214 .map_err(|error| database_error("failed to read schema objects", error))?;
215 Ok(names)
216 })
217 }
218
219 pub fn has_table(&self, name: &str) -> Result<bool> {
225 self.with_connection(|connection| {
226 let exists = connection
227 .query_row(
228 "SELECT EXISTS(
229 SELECT 1 FROM sqlite_master
230 WHERE name = ?1 AND type IN ('table', 'view')
231 )",
232 [name],
233 |row| row.get(0),
234 )
235 .map_err(|error| database_error("failed to query schema object", error))?;
236 Ok(exists)
237 })
238 }
239
240 pub fn folder_count(&self) -> Result<usize> {
246 self.with_connection(|connection| {
247 let count = connection
248 .query_row("SELECT COUNT(*) FROM indexed_folders", [], |row| row.get::<_, i64>(0))
249 .map_err(|error| database_error("failed to count indexed folders", error))?;
250 usize::try_from(count).map_err(|error| database_error("indexed folder count is invalid", error))
251 })
252 }
253
254 pub fn entry_count(&self) -> Result<usize> {
260 self.with_connection(|connection| {
261 let count = connection
262 .query_row("SELECT COUNT(*) FROM indexed_entries", [], |row| row.get::<_, i64>(0))
263 .map_err(|error| database_error("failed to count indexed entries", error))?;
264 usize::try_from(count).map_err(|error| database_error("indexed entry count is invalid", error))
265 })
266 }
267
268 pub fn is_folder_enabled(&self, namespace_id: NamespaceId) -> Result<bool> {
274 self.with_connection(|connection| {
275 let enabled = connection
276 .query_row(
277 "SELECT EXISTS(
278 SELECT 1 FROM indexed_folders WHERE namespace_id = ?1
279 )",
280 [namespace_id.to_string()],
281 |row| row.get(0),
282 )
283 .map_err(|error| database_error("failed to query indexed folder", error))?;
284 Ok(enabled)
285 })
286 }
287
288 pub fn upsert_entry(
294 &self,
295 namespace_id: NamespaceId,
296 key: impl AsRef<[u8]>,
297 hash: Hash,
298 size: u64,
299 ) -> Result<IndexedEntry> {
300 let key_bytes = key.as_ref().to_vec();
301 let namespace = namespace_id.to_string();
302 let hash_bytes = hash.as_bytes().to_vec();
303 let size_value =
304 i64::try_from(size).map_err(|error| database_error("indexed entry size is too large", error))?;
305 let key_text = String::from_utf8_lossy(&key_bytes).into_owned();
306 let title = Path::new(&key_text)
307 .file_name()
308 .and_then(|name| name.to_str())
309 .unwrap_or(&key_text)
310 .to_owned();
311 self.with_connection(|connection| {
312 let transaction = connection
313 .unchecked_transaction()
314 .map_err(|error| database_error("failed to begin indexing transaction", error))?;
315 let existing_id = transaction
316 .query_row(
317 "SELECT id FROM indexed_entries WHERE namespace_id = ?1 AND entry_key = ?2",
318 params![namespace, key_bytes],
319 |row| row.get::<_, i64>(0),
320 )
321 .optional()
322 .map_err(|error| database_error("failed to find indexed entry", error))?;
323 let id = if let Some(id) = existing_id {
324 transaction
325 .execute(
326 "UPDATE indexed_entries
327 SET content_hash = ?1, content_len = ?2, updated_at = ?3
328 WHERE id = ?4",
329 params![hash_bytes, size_value, now_seconds(), id],
330 )
331 .map_err(|error| database_error("failed to update indexed entry", error))?;
332 transaction
333 .execute("DELETE FROM indexed_entries_fts WHERE rowid = ?1", [id])
334 .map_err(|error| database_error("failed to refresh indexed search entry", error))?;
335 id
336 } else {
337 transaction
338 .execute(
339 "INSERT INTO indexed_entries
340 (namespace_id, entry_key, content_hash, content_len, updated_at)
341 VALUES (?1, ?2, ?3, ?4, ?5)",
342 params![namespace, key_bytes, hash_bytes, size_value, now_seconds()],
343 )
344 .map_err(|error| database_error("failed to insert indexed entry", error))?;
345 transaction.last_insert_rowid()
346 };
347 transaction
348 .execute(
349 "INSERT INTO indexed_entries_fts(rowid, namespace_id, entry_key, title, tags)
350 VALUES (?1, ?2, ?3, ?4, ?5)",
351 params![id, namespace, key_text, title, ""],
352 )
353 .map_err(|error| database_error("failed to insert indexed search entry", error))?;
354 transaction
355 .commit()
356 .map_err(|error| database_error("failed to commit indexed entry", error))?;
357 Ok(IndexedEntry {
358 namespace_id,
359 key: key_bytes,
360 hash,
361 size,
362 })
363 })
364 }
365
366 pub fn remove_entry(&self, namespace_id: NamespaceId, key: impl AsRef<[u8]>) -> Result<bool> {
372 self.with_connection(|connection| {
373 let transaction = connection
374 .unchecked_transaction()
375 .map_err(|error| database_error("failed to begin indexing transaction", error))?;
376 let id = transaction
377 .query_row(
378 "SELECT id FROM indexed_entries WHERE namespace_id = ?1 AND entry_key = ?2",
379 params![namespace_id.to_string(), key.as_ref()],
380 |row| row.get::<_, i64>(0),
381 )
382 .optional()
383 .map_err(|error| database_error("failed to find indexed entry", error))?;
384 let Some(entry_id) = id else {
385 transaction
386 .commit()
387 .map_err(|error| database_error("failed to commit indexing transaction", error))?;
388 return Ok(false);
389 };
390 transaction
391 .execute("DELETE FROM indexed_entries_fts WHERE rowid = ?1", [entry_id])
392 .map_err(|error| database_error("failed to remove indexed search entry", error))?;
393 transaction
394 .execute("DELETE FROM indexed_entries WHERE id = ?1", [entry_id])
395 .map_err(|error| database_error("failed to remove indexed entry", error))?;
396 transaction
397 .commit()
398 .map_err(|error| database_error("failed to commit indexing transaction", error))?;
399 Ok(true)
400 })
401 }
402
403 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<IndexedEntry>> {
412 if limit == 0 {
413 return Ok(Vec::new());
414 }
415 let limit_value = i64::try_from(limit).map_err(|error| database_error("search limit is invalid", error))?;
416 self.with_connection(|connection| {
417 let mut output = Vec::new();
418 if query.trim().is_empty() {
419 let mut statement = connection
420 .prepare(
421 "SELECT namespace_id, entry_key, content_hash, content_len
422 FROM indexed_entries ORDER BY updated_at DESC, id DESC LIMIT ?1",
423 )
424 .map_err(|error| database_error("failed to prepare index query", error))?;
425 let rows = statement
426 .query_map([limit_value], indexed_entry_from_row)
427 .map_err(|error| database_error("failed to query indexed entries", error))?;
428 for row in rows {
429 output.push(row.map_err(|error| database_error("failed to read indexed entry", error))?);
430 }
431 } else {
432 let mut statement = connection
433 .prepare(
434 "SELECT e.namespace_id, e.entry_key, e.content_hash, e.content_len
435 FROM indexed_entries_fts f
436 JOIN indexed_entries e ON e.id = f.rowid
437 WHERE indexed_entries_fts MATCH ?1
438 ORDER BY bm25(indexed_entries_fts), e.id DESC
439 LIMIT ?2",
440 )
441 .map_err(|error| database_error("failed to prepare full-text query", error))?;
442 let rows = statement
443 .query_map(params![query, limit_value], indexed_entry_from_row)
444 .map_err(|error| database_error("failed to query full-text index", error))?;
445 for row in rows {
446 output.push(row.map_err(|error| database_error("failed to read full-text result", error))?);
447 }
448 }
449 Ok(output)
450 })
451 }
452
453 pub fn enable_catalog(&self, namespace_id: NamespaceId, label: &str) -> Result<()> {
459 self.with_connection(|connection| {
460 connection
461 .execute(
462 "INSERT INTO indexed_catalogs(namespace_id, label, subscribed_at)
463 VALUES (?1, ?2, ?3)
464 ON CONFLICT(namespace_id) DO UPDATE SET label = excluded.label",
465 params![namespace_id.to_string(), label, now_seconds()],
466 )
467 .map_err(|error| database_error("failed to enable indexed catalog", error))?;
468 Ok(())
469 })
470 }
471
472 pub fn catalog_count(&self) -> Result<usize> {
478 self.with_connection(|connection| {
479 let count = connection
480 .query_row("SELECT COUNT(*) FROM indexed_catalogs", [], |row| row.get::<_, i64>(0))
481 .map_err(|error| database_error("failed to count indexed catalogs", error))?;
482 usize::try_from(count).map_err(|error| database_error("indexed catalog count is invalid", error))
483 })
484 }
485
486 pub fn load_catalogs(&self) -> Result<Vec<(NamespaceId, String)>> {
492 use std::str::FromStr;
493 self.with_connection(|connection| {
494 let mut statement = connection
495 .prepare("SELECT namespace_id, label FROM indexed_catalogs ORDER BY subscribed_at DESC")
496 .map_err(|error| database_error("failed to prepare catalog listing", error))?;
497 let rows = statement
498 .query_map([], |row| {
499 let namespace: String = row.get(0)?;
500 let label: String = row.get(1)?;
501 Ok((namespace, label))
502 })
503 .map_err(|error| database_error("failed to read indexed catalogs", error))?;
504 let mut catalogs = Vec::new();
505 for row in rows {
506 let (namespace, label) =
507 row.map_err(|error| database_error("failed to read indexed catalog record", error))?;
508 let namespace_id = NamespaceId::from_str(&namespace)
509 .map_err(|error| database_error("indexed catalog namespace is invalid", error))?;
510 catalogs.push((namespace_id, label));
511 }
512 Ok(catalogs)
513 })
514 }
515
516 pub fn catalog_entry_count(&self) -> Result<usize> {
522 self.with_connection(|connection| {
523 let count = connection
524 .query_row("SELECT COUNT(*) FROM indexed_catalog_entries", [], |row| {
525 row.get::<_, i64>(0)
526 })
527 .map_err(|error| database_error("failed to count indexed catalog entries", error))?;
528 usize::try_from(count).map_err(|error| database_error("indexed catalog entry count is invalid", error))
529 })
530 }
531
532 pub fn upsert_catalog_record(&self, record: &CatalogRecord) -> Result<CatalogRecord> {
539 record.validate()?;
540 let catalog_namespace = record.catalog_namespace_id.to_string();
541 let folder_namespace = record.folder_namespace_id.to_string();
542 let hash_bytes = record.hash.as_bytes().to_vec();
543 let size =
544 i64::try_from(record.size).map_err(|error| database_error("catalog record size is too large", error))?;
545 let tags = serde_json::to_string(&record.tags)
546 .map_err(|error| database_error("failed to serialize catalog record tags", error))?;
547 let key = record.key.clone();
548 let publisher = record.publisher.clone();
549 self.with_connection(|connection| {
550 let transaction = connection
551 .unchecked_transaction()
552 .map_err(|error| database_error("failed to begin catalog transaction", error))?;
553 let catalog_exists = transaction
554 .query_row(
555 "SELECT EXISTS(
556 SELECT 1 FROM indexed_catalogs WHERE namespace_id = ?1
557 )",
558 [&catalog_namespace],
559 |row| row.get::<_, bool>(0),
560 )
561 .map_err(|error| database_error("failed to find indexed catalog", error))?;
562 if !catalog_exists {
563 return Err(SyncwebError::InvalidConfig(format!(
564 "catalog is not enabled: {catalog_namespace}"
565 )));
566 }
567 let existing_id = transaction
568 .query_row(
569 "SELECT id FROM indexed_catalog_entries
570 WHERE catalog_namespace_id = ?1
571 AND folder_namespace_id = ?2
572 AND entry_key = ?3",
573 params![catalog_namespace, folder_namespace, key],
574 |row| row.get::<_, i64>(0),
575 )
576 .optional()
577 .map_err(|error| database_error("failed to find indexed catalog record", error))?;
578 let id = if let Some(id) = existing_id {
579 transaction
580 .execute(
581 "UPDATE indexed_catalog_entries
582 SET content_hash = ?1, content_len = ?2, folder_name = ?3,
583 title = ?4, tags = ?5, publisher = ?6, updated_at = ?7
584 WHERE id = ?8",
585 params![
586 hash_bytes,
587 size,
588 record.folder_name,
589 record.title,
590 tags,
591 publisher,
592 now_seconds(),
593 id
594 ],
595 )
596 .map_err(|error| database_error("failed to update indexed catalog record", error))?;
597 transaction
598 .execute("DELETE FROM indexed_catalog_entries_fts WHERE rowid = ?1", [id])
599 .map_err(|error| database_error("failed to refresh catalog search record", error))?;
600 id
601 } else {
602 transaction
603 .execute(
604 "INSERT INTO indexed_catalog_entries
605 (catalog_namespace_id, folder_namespace_id, entry_key, content_hash,
606 content_len, folder_name, title, tags, publisher, updated_at)
607 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
608 params![
609 catalog_namespace,
610 folder_namespace,
611 key,
612 hash_bytes,
613 size,
614 record.folder_name,
615 record.title,
616 tags,
617 publisher,
618 now_seconds()
619 ],
620 )
621 .map_err(|error| database_error("failed to insert indexed catalog record", error))?;
622 transaction.last_insert_rowid()
623 };
624 transaction
625 .execute(
626 "INSERT INTO indexed_catalog_entries_fts(
627 rowid, catalog_namespace_id, folder_namespace_id, entry_key,
628 folder_name, title, tags, publisher, content_hash
629 )
630 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
631 params![
632 id,
633 record.catalog_namespace_id.to_string(),
634 record.folder_namespace_id.to_string(),
635 String::from_utf8_lossy(&record.key),
636 record.folder_name,
637 record.title,
638 tags,
639 publisher,
640 record.hash.to_string()
641 ],
642 )
643 .map_err(|error| database_error("failed to insert catalog search record", error))?;
644 transaction
645 .commit()
646 .map_err(|error| database_error("failed to commit catalog record", error))?;
647 Ok(record.clone())
648 })
649 }
650
651 pub fn search_catalogs(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
659 if limit == 0 {
660 return Ok(Vec::new());
661 }
662 let limit_value =
663 i64::try_from(limit).map_err(|error| database_error("catalog search limit is invalid", error))?;
664 self.with_connection(|connection| {
665 let sql = if query.trim().is_empty() {
666 "SELECT catalog_namespace_id, folder_namespace_id, entry_key, content_hash,
667 content_len, folder_name, title, tags, publisher
668 FROM indexed_catalog_entries
669 ORDER BY updated_at DESC, id DESC LIMIT ?1"
670 } else {
671 "SELECT e.catalog_namespace_id, e.folder_namespace_id, e.entry_key, e.content_hash,
672 e.content_len, e.folder_name, e.title, e.tags, e.publisher
673 FROM indexed_catalog_entries_fts f
674 JOIN indexed_catalog_entries e ON e.id = f.rowid
675 WHERE indexed_catalog_entries_fts MATCH ?1
676 ORDER BY bm25(indexed_catalog_entries_fts), e.id DESC
677 LIMIT ?2"
678 };
679 let mut statement = connection
680 .prepare(sql)
681 .map_err(|error| database_error("failed to prepare catalog search", error))?;
682 let rows = if query.trim().is_empty() {
683 statement
684 .query_map([rusqlite::types::Value::Integer(limit_value)], catalog_record_from_row)
685 .map_err(|error| database_error("failed to query catalog records", error))?
686 } else {
687 statement
688 .query_map(params![query, limit_value], catalog_record_from_row)
689 .map_err(|error| database_error("failed to query catalog records", error))?
690 };
691 rows.collect::<std::result::Result<Vec<_>, _>>()
692 .map_err(|error| database_error("failed to read catalog records", error))
693 })
694 }
695
696 pub fn global_search(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
702 self.search_catalogs(query, limit)
703 }
704
705 pub fn catalog_search(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
711 self.search_catalogs(query, limit)
712 }
713
714 pub fn append_wot_metadata(&self, entry: &wot::MetadataEntry) -> Result<bool> {
720 entry.validate()?;
721 let hash_bytes = entry.content.as_bytes().to_vec();
722 let sequence =
723 i64::try_from(entry.sequence).map_err(|error| database_error("metadata sequence is too large", error))?;
724 let created_at = i64::try_from(entry.created_at)
725 .map_err(|error| database_error("metadata timestamp is too large", error))?;
726 let key = entry.key.clone();
727 let value = entry.value.clone();
728 let author = entry.author.clone();
729 let signature = entry
730 .signature
731 .clone()
732 .ok_or_else(|| SyncwebError::InvalidConfig("Web-of-Trust metadata must be signed".to_owned()))?;
733 self.with_connection(|connection| {
734 let transaction = connection
735 .unchecked_transaction()
736 .map_err(|error| database_error("failed to begin Web-of-Trust metadata transaction", error))?;
737 let inserted = transaction
738 .execute(
739 "INSERT INTO wot_metadata
740 (content_hash, metadata_key, metadata_value, author, sequence, created_at, signature)
741 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
742 ON CONFLICT(content_hash, metadata_key, author, sequence) DO NOTHING",
743 params![hash_bytes, key, value, author, sequence, created_at, signature],
744 )
745 .map_err(|error| database_error("failed to append Web-of-Trust metadata", error))?;
746 if inserted == 1 {
747 let rowid = transaction.last_insert_rowid();
748 transaction
749 .execute(
750 "INSERT INTO wot_metadata_fts(rowid, content_hash, metadata_key, metadata_value, author)
751 VALUES (?1, ?2, ?3, ?4, ?5)",
752 params![rowid, entry.content.to_string(), entry.key, entry.value, entry.author],
753 )
754 .map_err(|error| database_error("failed to index Web-of-Trust metadata", error))?;
755 }
756 transaction
757 .commit()
758 .map_err(|error| database_error("failed to commit Web-of-Trust metadata", error))?;
759 Ok(inserted == 1)
760 })
761 }
762
763 pub fn search_wot_metadata(&self, query: &str, limit: usize) -> Result<Vec<wot::MetadataEntry>> {
769 if limit == 0 {
770 return Ok(Vec::new());
771 }
772 let limit_value =
773 i64::try_from(limit).map_err(|error| database_error("metadata search limit is invalid", error))?;
774 self.with_connection(|connection| {
775 let mut statement = if query.trim().is_empty() {
776 connection
777 .prepare(
778 "SELECT content_hash, metadata_key, metadata_value, author, sequence, created_at, signature
779 FROM wot_metadata ORDER BY created_at DESC, id DESC LIMIT ?1",
780 )
781 .map_err(|error| database_error("failed to prepare metadata query", error))?
782 } else {
783 connection
784 .prepare(
785 "SELECT m.content_hash, m.metadata_key, m.metadata_value, m.author,
786 m.sequence, m.created_at, m.signature
787 FROM wot_metadata_fts f
788 JOIN wot_metadata m ON m.id = f.rowid
789 WHERE wot_metadata_fts MATCH ?1
790 ORDER BY bm25(wot_metadata_fts), m.id DESC
791 LIMIT ?2",
792 )
793 .map_err(|error| database_error("failed to prepare metadata full-text query", error))?
794 };
795 let rows = if query.trim().is_empty() {
796 statement
797 .query_map([limit_value], wot_metadata_from_row)
798 .map_err(|error| database_error("failed to query metadata", error))?
799 } else {
800 statement
801 .query_map(params![query, limit_value], wot_metadata_from_row)
802 .map_err(|error| database_error("failed to query metadata full-text index", error))?
803 };
804 rows.collect::<std::result::Result<Vec<_>, _>>()
805 .map_err(|error| database_error("failed to read metadata search results", error))
806 })
807 }
808
809 pub fn disable_catalog(&self, namespace_id: NamespaceId) -> Result<()> {
815 self.with_connection(|connection| {
816 let transaction = connection
817 .unchecked_transaction()
818 .map_err(|error| database_error("failed to begin catalog removal", error))?;
819 let namespace = namespace_id.to_string();
820 let mut statement = transaction
821 .prepare("SELECT id FROM indexed_catalog_entries WHERE catalog_namespace_id = ?1")
822 .map_err(|error| database_error("failed to find catalog records", error))?;
823 let ids = statement
824 .query_map([namespace.as_str()], |row| row.get::<_, i64>(0))
825 .map_err(|error| database_error("failed to list catalog records", error))?
826 .collect::<std::result::Result<Vec<_>, _>>()
827 .map_err(|error| database_error("failed to read catalog record IDs", error))?;
828 drop(statement);
829 for id in ids {
830 transaction
831 .execute("DELETE FROM indexed_catalog_entries_fts WHERE rowid = ?1", [id])
832 .map_err(|error| database_error("failed to remove catalog search record", error))?;
833 }
834 transaction
835 .execute(
836 "DELETE FROM indexed_catalog_entries WHERE catalog_namespace_id = ?1",
837 [namespace.as_str()],
838 )
839 .map_err(|error| database_error("failed to remove catalog records", error))?;
840 transaction
841 .execute(
842 "DELETE FROM indexed_catalogs WHERE namespace_id = ?1",
843 [namespace.as_str()],
844 )
845 .map_err(|error| database_error("failed to disable indexed catalog", error))?;
846 transaction
847 .commit()
848 .map_err(|error| database_error("failed to commit catalog removal", error))?;
849 Ok(())
850 })
851 }
852
853 pub fn enable_folder(&self, namespace_id: NamespaceId, label: &str) -> Result<()> {
859 self.with_connection(|connection| {
860 connection
861 .execute(
862 "INSERT INTO indexed_folders(namespace_id, label, enabled_at)
863 VALUES (?1, ?2, ?3)
864 ON CONFLICT(namespace_id) DO UPDATE SET label = excluded.label",
865 params![namespace_id.to_string(), label, now_seconds()],
866 )
867 .map_err(|error| database_error("failed to enable indexed folder", error))?;
868 Ok(())
869 })
870 }
871
872 pub fn disable_folder(&self, namespace_id: NamespaceId) -> Result<()> {
878 self.with_connection(|connection| {
879 let transaction = connection
880 .unchecked_transaction()
881 .map_err(|error| database_error("failed to begin folder removal", error))?;
882 let namespace = namespace_id.to_string();
883 let mut statement = transaction
884 .prepare("SELECT id FROM indexed_entries WHERE namespace_id = ?1")
885 .map_err(|error| database_error("failed to find folder entries", error))?;
886 let ids = statement
887 .query_map([namespace.as_str()], |row| row.get::<_, i64>(0))
888 .map_err(|error| database_error("failed to list folder entries", error))?
889 .collect::<std::result::Result<Vec<_>, _>>()
890 .map_err(|error| database_error("failed to read folder entries", error))?;
891 drop(statement);
892 for id in ids {
893 transaction
894 .execute("DELETE FROM indexed_entries_fts WHERE rowid = ?1", [id])
895 .map_err(|error| database_error("failed to remove folder search entry", error))?;
896 }
897 transaction
898 .execute(
899 "DELETE FROM indexed_entries WHERE namespace_id = ?1",
900 [namespace.as_str()],
901 )
902 .map_err(|error| database_error("failed to remove folder entries", error))?;
903 transaction
904 .execute(
905 "DELETE FROM indexed_folders WHERE namespace_id = ?1",
906 [namespace.as_str()],
907 )
908 .map_err(|error| database_error("failed to disable indexed folder", error))?;
909 transaction
910 .commit()
911 .map_err(|error| database_error("failed to commit folder removal", error))?;
912 Ok(())
913 })
914 }
915
916 fn with_connection<T>(&self, operation: impl FnOnce(&mut Connection) -> Result<T>) -> Result<T> {
917 let mut connection = self
918 .connection
919 .lock()
920 .map_err(|error| SyncwebError::operation("indexing database lock poisoned", error))?;
921 operation(&mut connection)
922 }
923
924 pub fn save_denylist_rules(&self, rules: &[DenylistRule]) -> Result<()> {
930 self.with_connection(|connection| {
931 connection
932 .execute("DELETE FROM denylist_rules", [])
933 .map_err(|error| database_error("failed to clear denylist rules", error))?;
934 let now = now_seconds();
935 for rule in rules {
936 let (rule_type, rule_value, namespace_id) = match rule {
937 DenylistRule::Device(d) => ("device", d.as_bytes().to_vec(), None::<String>),
938 DenylistRule::Hash(h) => ("hash", h.as_bytes().to_vec(), None),
939 DenylistRule::File { namespace_id: ns, key } => ("file", key.clone(), ns.map(|n| n.to_string())),
940 };
941 connection
942 .execute(
943 "INSERT OR REPLACE INTO denylist_rules(rule_type, rule_value, namespace_id, updated_at)
944 VALUES (?1, ?2, ?3, ?4)",
945 params![rule_type, rule_value, namespace_id, now],
946 )
947 .map_err(|error| database_error("failed to save denylist rule", error))?;
948 }
949 Ok(())
950 })
951 }
952
953 pub fn load_denylist_rules(&self) -> Result<Vec<DenylistRule>> {
959 self.with_connection(|connection| {
960 let mut stmt = connection
961 .prepare("SELECT rule_type, rule_value, namespace_id FROM denylist_rules ORDER BY updated_at")
962 .map_err(|error| database_error("failed to prepare denylist query", error))?;
963 let rules = stmt
964 .query_map([], |row| {
965 let rule_type: String = row.get(0)?;
966 let rule_value: Vec<u8> = row.get(1)?;
967 let namespace_id: Option<String> = row.get(2)?;
968 match rule_type.as_str() {
969 "device" => Ok(DenylistRule::Device(String::from_utf8_lossy(&rule_value).to_string())),
970 "hash" => {
971 let arr: [u8; 32] = rule_value.try_into().map_err(|error: Vec<u8>| {
972 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
973 "invalid hash length",
974 format!("expected 32 bytes, got {}", error.len()),
975 )))
976 })?;
977 Ok(DenylistRule::Hash(Hash::from(arr)))
978 }
979 "file" => {
980 let ns = namespace_id.and_then(|s| s.parse::<NamespaceId>().ok());
981 Ok(DenylistRule::File {
982 namespace_id: ns,
983 key: rule_value,
984 })
985 }
986 _ => Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
987 SyncwebError::operation("unknown rule type", rule_type),
988 ))),
989 }
990 })
991 .map_err(|error| database_error("failed to query denylist rules", error))?
992 .collect::<std::result::Result<Vec<_>, _>>()
993 .map_err(|error| database_error("failed to read denylist rules", error))?;
994 Ok(rules)
995 })
996 }
997
998 pub fn load_filter_lists(&self) -> Result<Vec<denylist::FilterList>> {
1004 self.with_connection(|connection| {
1005 let mut stmt = connection
1006 .prepare("SELECT namespace_id, sequence, publisher, payload, updated_at FROM filter_lists ORDER BY updated_at")
1007 .map_err(|error| database_error("failed to prepare filter lists query", error))?;
1008 let lists = stmt
1009 .query_map([], |row| {
1010 let payload: Vec<u8> = row.get(3)?;
1011 let list: denylist::FilterList = serde_json::from_slice(&payload).map_err(|error| {
1012 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1013 "failed to deserialize filter list",
1014 error,
1015 )))
1016 })?;
1017 Ok(list)
1018 })
1019 .map_err(|error| database_error("failed to query filter lists", error))?
1020 .collect::<std::result::Result<Vec<_>, _>>()
1021 .map_err(|error| database_error("failed to read filter list rows", error))?;
1022 Ok(lists)
1023 })
1024 }
1025
1026 pub fn upsert_filter_list(&self, list: &denylist::FilterList) -> Result<()> {
1032 let payload = list.to_bytes()?;
1033 self.with_connection(|connection| {
1034 connection
1035 .execute(
1036 "INSERT OR REPLACE INTO filter_lists(namespace_id, sequence, publisher, payload, updated_at)
1037 VALUES (?1, ?2, ?3, ?4, ?5)",
1038 params![
1039 list.namespace_id.to_string(),
1040 i64::try_from(list.sequence).unwrap_or(i64::MAX),
1041 list.publisher,
1042 payload,
1043 now_seconds(),
1044 ],
1045 )
1046 .map_err(|error| database_error("failed to upsert filter list", error))?;
1047 Ok(())
1048 })
1049 }
1050
1051 pub fn load_links(&self) -> Result<(Vec<links::MutablePointer>, Vec<String>, Vec<links::PrivateLink>)> {
1057 self.with_connection(|connection| {
1058 let mut stmt = connection
1059 .prepare("SELECT link, kind, payload, updated_at FROM stable_links ORDER BY updated_at")
1060 .map_err(|error| database_error("failed to prepare links query", error))?;
1061 let mut pointers = Vec::new();
1062 let rows = stmt
1063 .query_map([], |row| {
1064 let kind: String = row.get(1)?;
1065 let payload: Vec<u8> = row.get(2)?;
1066 Ok((kind, payload))
1067 })
1068 .map_err(|error| database_error("failed to query stable links", error))?
1069 .collect::<std::result::Result<Vec<_>, _>>()
1070 .map_err(|error| database_error("failed to read link rows", error))?;
1071 for (kind, payload) in rows {
1072 if kind.as_str() == "mutable"
1073 && let Ok(p) = serde_json::from_slice::<links::MutablePointer>(&payload)
1074 {
1075 pointers.push(p);
1076 }
1077 }
1078
1079 let mut mirror_stmt = connection
1080 .prepare("SELECT ticket FROM link_mirrors ORDER BY priority")
1081 .map_err(|error| database_error("failed to prepare mirror query", error))?;
1082 let mirrors = mirror_stmt
1083 .query_map([], |row| row.get::<_, String>(0))
1084 .map_err(|error| database_error("failed to query mirrors", error))?
1085 .collect::<std::result::Result<Vec<_>, _>>()
1086 .map_err(|error| database_error("failed to read mirror rows", error))?;
1087
1088 let mut revoked = Vec::new();
1089 if let Ok(mut rev_stmt) = connection.prepare("SELECT manifest, capability, revoked_at FROM revoked_links")
1090 && let Ok(rev_rows) = rev_stmt.query_map([], |row| {
1091 let manifest_bytes: Vec<u8> = row.get(0)?;
1092 let capability_blob: Vec<u8> = row.get(1)?;
1093 let manifests: [u8; 32] = manifest_bytes.try_into().unwrap_or([0_u8; 32]);
1094 let hash = Hash::from_bytes(manifests);
1095 Ok((hash, capability_blob))
1096 })
1097 {
1098 for row in rev_rows.flatten() {
1099 let cap: [u8; 32] = row.1.try_into().unwrap_or([0_u8; 32]);
1100 if let Ok(link) = PrivateLink::new(row.0, cap, u64::MAX) {
1101 revoked.push(link);
1102 }
1103 }
1104 }
1105 Ok((pointers, mirrors, revoked))
1106 })
1107 }
1108
1109 pub fn save_links(
1115 &self,
1116 pointers: &[links::MutablePointer],
1117 mirrors: &[String],
1118 revoked: &[links::PrivateLink],
1119 ) -> Result<()> {
1120 self.with_connection(|connection| {
1121 connection.execute("DELETE FROM stable_links", [])
1122 .map_err(|error| database_error("failed to clear stable links", error))?;
1123 connection.execute("DELETE FROM link_mirrors", [])
1124 .map_err(|error| database_error("failed to clear link mirrors", error))?;
1125 connection.execute("DELETE FROM revoked_links", [])
1126 .map_err(|error| database_error("failed to clear revoked links", error))?;
1127 let now = now_seconds();
1128 for pointer in pointers {
1129 let payload = serde_json::to_vec(pointer)
1130 .map_err(|error| database_error("failed to serialize pointer", error))?;
1131 connection.execute(
1132 "INSERT OR REPLACE INTO stable_links(link, kind, publisher, alias, content_hash, sequence, version, payload, updated_at)
1133 VALUES (?1, 'mutable', ?2, ?3, NULL, ?4, ?5, ?6, ?7)",
1134 params![
1135 format!("mutable:{}:{}", pointer.publisher, pointer.alias),
1136 pointer.publisher.to_string(),
1137 pointer.alias,
1138 i64::try_from(pointer.sequence).unwrap_or(i64::MAX),
1139 pointer.version.as_deref(),
1140 payload,
1141 now,
1142 ],
1143 ).map_err(|error| database_error("failed to save pointer", error))?;
1144 }
1145 for uri in mirrors {
1146 connection.execute(
1147 "INSERT INTO link_mirrors(link, provider, ticket, priority, updated_at)
1148 VALUES ('', 'local', ?1, 0, ?2)",
1149 params![uri, now],
1150 ).map_err(|error| database_error("failed to save mirror", error))?;
1151 }
1152 for link in revoked {
1153 connection.execute(
1154 "INSERT OR REPLACE INTO revoked_links(manifest, capability, revoked_at)
1155 VALUES (?1, ?2, ?3)",
1156 params![link.manifest.as_bytes(), link.capability.as_slice(), now],
1157 ).map_err(|error| database_error("failed to save revoked link", error))?;
1158 }
1159 Ok(())
1160 })
1161 }
1162
1163 pub fn upsert_link_pointer(&self, pointer: &links::MutablePointer) -> Result<()> {
1169 let payload =
1170 serde_json::to_vec(pointer).map_err(|error| database_error("failed to serialize pointer", error))?;
1171 self.with_connection(|connection| {
1172 connection.execute(
1173 "INSERT OR REPLACE INTO stable_links(link, kind, publisher, alias, content_hash, sequence, version, payload, updated_at)
1174 VALUES (?1, 'mutable', ?2, ?3, NULL, ?4, ?5, ?6, ?7)",
1175 params![
1176 format!("mutable:{}:{}", pointer.publisher, pointer.alias),
1177 pointer.publisher.to_string(),
1178 pointer.alias,
1179 i64::try_from(pointer.sequence).unwrap_or(i64::MAX),
1180 pointer.version.as_deref(),
1181 payload,
1182 now_seconds(),
1183 ],
1184 )
1185 .map_err(|error| database_error("failed to upsert link pointer", error))?;
1186 Ok(())
1187 })
1188 }
1189
1190 pub fn upsert_link_mirror(&self, ticket: &str) -> Result<()> {
1196 self.with_connection(|connection| {
1197 connection
1198 .execute(
1199 "INSERT OR REPLACE INTO link_mirrors(link, provider, ticket, priority, updated_at)
1200 VALUES ('', 'local', ?1, 0, ?2)",
1201 params![ticket, now_seconds()],
1202 )
1203 .map_err(|error| database_error("failed to upsert link mirror", error))?;
1204 Ok(())
1205 })
1206 }
1207
1208 pub fn save_leases(&self, leases: &[resilience::ProviderLease]) -> Result<()> {
1214 self.with_connection(|connection| {
1215 connection.execute("DELETE FROM provider_leases", [])
1216 .map_err(|error| database_error("failed to clear provider leases", error))?;
1217 for lease in leases {
1218 connection.execute(
1219 "INSERT INTO provider_leases(provider, content_hash, ticket, sequence, issued_at, expires_at, signature)
1220 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1221 params![
1222 lease.provider.to_string(),
1223 lease.hash.as_bytes().to_vec(),
1224 lease.ticket,
1225 i64::try_from(lease.sequence).unwrap_or(i64::MAX),
1226 i64::try_from(lease.issued_at).unwrap_or(i64::MAX),
1227 i64::try_from(lease.expires_at).unwrap_or(i64::MAX),
1228 lease.signature,
1229 ],
1230 ).map_err(|error| database_error("failed to save lease", error))?;
1231 }
1232 Ok(())
1233 })
1234 }
1235
1236 pub fn load_leases(&self) -> Result<Vec<resilience::ProviderLease>> {
1242 self.with_connection(|connection| {
1243 let mut stmt = connection
1244 .prepare(
1245 "SELECT provider, content_hash, ticket, sequence, issued_at, expires_at, signature
1246 FROM provider_leases ORDER BY issued_at",
1247 )
1248 .map_err(|error| database_error("failed to prepare leases query", error))?;
1249 let leases = stmt
1250 .query_map([], |row| {
1251 let provider_str: String = row.get(0)?;
1252 let hash_bytes: Vec<u8> = row.get(1)?;
1253 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1254 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1255 "invalid hash length",
1256 format!("expected 32 bytes, got {}", error.len()),
1257 )))
1258 })?;
1259 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1260 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1261 "invalid provider",
1262 error,
1263 )))
1264 })?;
1265 Ok(resilience::ProviderLease {
1266 hash: Hash::from(arr),
1267 provider,
1268 ticket: row.get(2)?,
1269 sequence: u64::try_from(row.get::<_, i64>(3)?).unwrap_or_default(),
1270 issued_at: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1271 expires_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1272 signature: row.get(6)?,
1273 })
1274 })
1275 .map_err(|error| database_error("failed to query leases", error))?
1276 .collect::<std::result::Result<Vec<_>, _>>()
1277 .map_err(|error| database_error("failed to read lease rows", error))?;
1278 Ok(leases)
1279 })
1280 }
1281
1282 pub fn save_trust_delegations(&self, delegations: &[wot::TrustDelegation]) -> Result<()> {
1288 self.with_connection(|connection| {
1289 connection.execute("DELETE FROM trust_delegations", [])
1290 .map_err(|error| database_error("failed to clear trust delegations", error))?;
1291 for d in delegations {
1292 connection.execute(
1293 "INSERT INTO trust_delegations(delegator, delegate, scope, max_depth, sequence, issued_at, expires_at, revoked_at, signature)
1294 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
1295 params![
1296 d.delegator, d.delegate,
1297 d.scope.map(|h| h.as_bytes().to_vec()),
1298 d.max_depth.map(i64::from),
1299 i64::try_from(d.sequence).unwrap_or(i64::MAX),
1300 i64::try_from(d.issued_at).unwrap_or(i64::MAX),
1301 i64::try_from(d.expires_at).unwrap_or(i64::MAX),
1302 d.revoked_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1303 d.signature,
1304 ],
1305 ).map_err(|error| database_error("failed to save delegation", error))?;
1306 }
1307 Ok(())
1308 })
1309 }
1310
1311 pub fn load_trust_delegations(&self) -> Result<Vec<wot::TrustDelegation>> {
1317 self.with_connection(|connection| {
1318 let mut stmt = connection
1319 .prepare(
1320 "SELECT delegator, delegate, scope, max_depth, sequence, issued_at, expires_at, revoked_at, signature
1321 FROM trust_delegations ORDER BY issued_at",
1322 )
1323 .map_err(|error| database_error("failed to prepare delegations query", error))?;
1324 let delegations = stmt
1325 .query_map([], |row| {
1326 let scope: Option<Vec<u8>> = row.get(2)?;
1327 let hash = scope.and_then(|b| {
1328 let arr: [u8; 32] = b.try_into().ok()?;
1329 Some(Hash::from(arr))
1330 });
1331 let max_depth: Option<i64> = row.get(3)?;
1332 let revoked_at: Option<i64> = row.get(7)?;
1333 Ok(wot::TrustDelegation {
1334 delegator: row.get::<_, String>(0)?,
1335 delegate: row.get::<_, String>(1)?,
1336 scope: hash,
1337 max_depth: max_depth.map(|v| u32::try_from(v).unwrap_or(u32::MAX)),
1338 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1339 issued_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1340 expires_at: u64::try_from(row.get::<_, i64>(6)?).unwrap_or_default(),
1341 revoked_at: revoked_at.map(|v| u64::try_from(v).unwrap_or(u64::MAX)),
1342 signature: row.get(8)?,
1343 })
1344 })
1345 .map_err(|error| database_error("failed to query delegations", error))?
1346 .collect::<std::result::Result<Vec<_>, _>>()
1347 .map_err(|error| database_error("failed to read delegation rows", error))?;
1348 Ok(delegations)
1349 })
1350 }
1351
1352 pub fn save_moderation_records(&self, records: &[wot::ModerationRecord]) -> Result<()> {
1358 self.with_connection(|connection| {
1359 connection.execute("DELETE FROM moderation_records_v2", [])
1360 .map_err(|error| database_error("failed to clear moderation records", error))?;
1361 for r in records {
1362 let scope_json = serde_json::to_string(&r.scope)
1363 .map_err(|error| database_error("failed to serialize scope", error))?;
1364 connection.execute(
1365 "INSERT INTO moderation_records_v2(content_hash, moderator, action, scope_json, sequence, created_at, reason, signature)
1366 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
1367 params![
1368 r.content.as_bytes().to_vec(), r.moderator,
1369 format!("{:?}", r.action), scope_json,
1370 i64::try_from(r.sequence).unwrap_or(i64::MAX),
1371 i64::try_from(r.created_at).unwrap_or(i64::MAX),
1372 r.reason,
1373 r.signature,
1374 ],
1375 ).map_err(|error| database_error("failed to save moderation record", error))?;
1376 }
1377 Ok(())
1378 })
1379 }
1380
1381 pub fn load_moderation_records(&self) -> Result<Vec<wot::ModerationRecord>> {
1387 self.with_connection(|connection| {
1388 let mut stmt = connection
1389 .prepare(
1390 "SELECT content_hash, moderator, action, scope_json, sequence, created_at, reason, signature
1391 FROM moderation_records_v2 ORDER BY created_at",
1392 )
1393 .map_err(|error| database_error("failed to prepare moderation query", error))?;
1394 let records = stmt
1395 .query_map([], |row| {
1396 let hash_bytes: Vec<u8> = row.get(0)?;
1397 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1398 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1399 "invalid hash length",
1400 format!("expected 32 bytes, got {}", error.len()),
1401 )))
1402 })?;
1403 let scope_json: String = row.get(3)?;
1404 let scope: wot::ModerationScope = serde_json::from_str(&scope_json)
1405 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
1406 let action_str: String = row.get(2)?;
1407 let action = match action_str.as_str() {
1408 "Show" => wot::ModerationAction::Show,
1409 "Hide" => wot::ModerationAction::Hide,
1410 "Warn" => wot::ModerationAction::Warn,
1411 "Quarantine" => wot::ModerationAction::Quarantine,
1412 "Restore" => wot::ModerationAction::Restore,
1413 _ => {
1414 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1415 SyncwebError::operation("unknown moderation action", action_str),
1416 )));
1417 }
1418 };
1419 Ok(wot::ModerationRecord {
1420 content: Hash::from(arr),
1421 moderator: row.get::<_, String>(1)?,
1422 action,
1423 scope,
1424 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1425 created_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1426 reason: row.get::<_, String>(6)?,
1427 signature: row.get(7)?,
1428 })
1429 })
1430 .map_err(|error| database_error("failed to query moderation records", error))?
1431 .collect::<std::result::Result<Vec<_>, _>>()
1432 .map_err(|error| database_error("failed to read moderation rows", error))?;
1433 Ok(records)
1434 })
1435 }
1436
1437 pub fn save_attestations(&self, attestations: &[wot::Attestation]) -> Result<()> {
1443 self.with_connection(|connection| {
1444 connection.execute("DELETE FROM attestations_v2", [])
1445 .map_err(|error| database_error("failed to clear attestations", error))?;
1446 for a in attestations {
1447 let (kind, kind_other) = match &a.kind {
1448 wot::AttestationKind::License => ("license", None),
1449 wot::AttestationKind::Provenance => ("provenance", None),
1450 wot::AttestationKind::Derivative => ("derivative", None),
1451 wot::AttestationKind::Other(v) => ("other", Some(v.as_str())),
1452 };
1453 connection.execute(
1454 "INSERT INTO attestations_v2(content_hash, issuer, kind, kind_other, value, sequence, issued_at, signature)
1455 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
1456 params![
1457 a.content.as_bytes().to_vec(), a.issuer,
1458 kind, kind_other, a.value,
1459 i64::try_from(a.sequence).unwrap_or(i64::MAX),
1460 i64::try_from(a.issued_at).unwrap_or(i64::MAX),
1461 a.signature,
1462 ],
1463 ).map_err(|error| database_error("failed to save attestation", error))?;
1464 }
1465 Ok(())
1466 })
1467 }
1468
1469 pub fn load_attestations(&self) -> Result<Vec<wot::Attestation>> {
1475 self.with_connection(|connection| {
1476 let mut stmt = connection
1477 .prepare(
1478 "SELECT content_hash, issuer, kind, kind_other, value, sequence, issued_at, signature
1479 FROM attestations_v2 ORDER BY issued_at",
1480 )
1481 .map_err(|error| database_error("failed to prepare attestations query", error))?;
1482 let attestations = stmt
1483 .query_map([], |row| {
1484 let hash_bytes: Vec<u8> = row.get(0)?;
1485 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1486 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1487 "invalid hash length",
1488 format!("expected 32 bytes, got {}", error.len()),
1489 )))
1490 })?;
1491 let kind_str: String = row.get(2)?;
1492 let kind_other: Option<String> = row.get(3)?;
1493 let kind = match kind_str.as_str() {
1494 "license" => wot::AttestationKind::License,
1495 "provenance" => wot::AttestationKind::Provenance,
1496 "derivative" => wot::AttestationKind::Derivative,
1497 "other" => wot::AttestationKind::Other(kind_other.unwrap_or_default()),
1498 _ => {
1499 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1500 SyncwebError::operation("unknown attestation kind", kind_str),
1501 )));
1502 }
1503 };
1504 Ok(wot::Attestation {
1505 content: Hash::from(arr),
1506 issuer: row.get::<_, String>(1)?,
1507 kind,
1508 value: row.get::<_, String>(4)?,
1509 sequence: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1510 issued_at: u64::try_from(row.get::<_, i64>(6)?).unwrap_or_default(),
1511 signature: row.get(7)?,
1512 })
1513 })
1514 .map_err(|error| database_error("failed to query attestations", error))?
1515 .collect::<std::result::Result<Vec<_>, _>>()
1516 .map_err(|error| database_error("failed to read attestation rows", error))?;
1517 Ok(attestations)
1518 })
1519 }
1520
1521 pub fn save_content_reports(&self, reports: &[ReportRecord]) -> Result<()> {
1527 self.with_connection(|connection| {
1528 connection
1529 .execute("DELETE FROM content_reports_v2", [])
1530 .map_err(|error| database_error("failed to clear content reports", error))?;
1531 for r in reports {
1532 let reporter = r.reporter.as_deref().unwrap_or("cli");
1533 connection
1534 .execute(
1535 "INSERT INTO content_reports_v2(content_hash, reporter, reason, scope, created_at, signature)
1536 VALUES (?1, ?2, ?3, 'global', ?4, ?5)",
1537 params![
1538 r.content.as_bytes().to_vec(),
1539 reporter,
1540 r.reason,
1541 i64::try_from(r.created_at).unwrap_or(i64::MAX),
1542 r.signature,
1543 ],
1544 )
1545 .map_err(|error| database_error("failed to save content report", error))?;
1546 }
1547 Ok(())
1548 })
1549 }
1550
1551 pub fn load_content_reports(&self) -> Result<Vec<ReportRecord>> {
1557 self.with_connection(|connection| {
1558 let mut stmt = connection
1559 .prepare(
1560 "SELECT content_hash, reason, created_at, reporter, signature
1561 FROM content_reports_v2 ORDER BY created_at",
1562 )
1563 .map_err(|error| database_error("failed to prepare reports query", error))?;
1564 let reports = stmt
1565 .query_map([], |row| {
1566 let hash_bytes: Vec<u8> = row.get(0)?;
1567 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1568 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1569 "invalid hash length",
1570 format!("expected 32 bytes, got {}", error.len()),
1571 )))
1572 })?;
1573 Ok(ReportRecord {
1574 content: Hash::from(arr),
1575 reason: row.get::<_, String>(1)?,
1576 created_at: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1577 reporter: row.get::<_, Option<String>>(3)?,
1578 signature: row.get::<_, Option<String>>(4)?,
1579 })
1580 })
1581 .map_err(|error| database_error("failed to query content reports", error))?
1582 .collect::<std::result::Result<Vec<_>, _>>()
1583 .map_err(|error| database_error("failed to read report rows", error))?;
1584 Ok(reports)
1585 })
1586 }
1587
1588 pub fn save_provider_bans(&self, bans: &[resilience::BanRecord]) -> Result<()> {
1594 self.with_connection(|connection| {
1595 connection
1596 .execute("DELETE FROM provider_bans_v2", [])
1597 .map_err(|error| database_error("failed to clear provider bans", error))?;
1598 for b in bans {
1599 connection
1600 .execute(
1601 "INSERT INTO provider_bans_v2(provider, content_hash, banned_at, expires_at, reason, source)
1602 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1603 params![
1604 b.provider.to_string(),
1605 b.hash.map(|h| h.as_bytes().to_vec()),
1606 i64::try_from(b.banned_at).unwrap_or(i64::MAX),
1607 b.expires_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1608 b.reason,
1609 format!("{:?}", b.source),
1610 ],
1611 )
1612 .map_err(|error| database_error("failed to save provider ban", error))?;
1613 }
1614 Ok(())
1615 })
1616 }
1617
1618 pub fn load_provider_bans(&self) -> Result<Vec<resilience::BanRecord>> {
1624 self.with_connection(|connection| {
1625 let mut stmt = connection
1626 .prepare(
1627 "SELECT provider, content_hash, banned_at, expires_at, reason, source
1628 FROM provider_bans_v2 ORDER BY banned_at",
1629 )
1630 .map_err(|error| database_error("failed to prepare bans query", error))?;
1631 let bans = stmt
1632 .query_map([], |row| {
1633 let provider_str: String = row.get(0)?;
1634 let content_hash: Option<Vec<u8>> = row.get(1)?;
1635 let source_str: String = row.get(5)?;
1636 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1637 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1638 "invalid provider",
1639 error,
1640 )))
1641 })?;
1642 let hash = content_hash.and_then(|b| {
1643 let arr: [u8; 32] = b.try_into().ok()?;
1644 Some(Hash::from(arr))
1645 });
1646 let source = match source_str.as_str() {
1647 "Automated" => resilience::BanSource::Automated,
1648 "WoT" => resilience::BanSource::WoT,
1649 _ => resilience::BanSource::Manual,
1650 };
1651 Ok(resilience::BanRecord {
1652 provider,
1653 hash,
1654 banned_at: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1655 expires_at: row
1656 .get::<_, Option<i64>>(3)?
1657 .map(|v| u64::try_from(v).unwrap_or_default()),
1658 reason: row.get::<_, String>(4)?,
1659 source,
1660 })
1661 })
1662 .map_err(|error| database_error("failed to query provider bans", error))?
1663 .collect::<std::result::Result<Vec<_>, _>>()
1664 .map_err(|error| database_error("failed to read ban rows", error))?;
1665 Ok(bans)
1666 })
1667 }
1668
1669 pub fn insert_provider_lease(&self, lease: &resilience::ProviderLease) -> Result<()> {
1675 self.with_connection(|connection| {
1676 connection.execute(
1677 "INSERT OR REPLACE INTO provider_leases(provider, content_hash, ticket, sequence, issued_at, expires_at, signature)
1678 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1679 params![
1680 lease.provider.to_string(),
1681 lease.hash.as_bytes().to_vec(),
1682 lease.ticket,
1683 i64::try_from(lease.sequence).unwrap_or(i64::MAX),
1684 i64::try_from(lease.issued_at).unwrap_or(i64::MAX),
1685 i64::try_from(lease.expires_at).unwrap_or(i64::MAX),
1686 lease.signature,
1687 ],
1688 )
1689 .map_err(|error| database_error("failed to insert provider lease", error))?;
1690 Ok(())
1691 })
1692 }
1693
1694 pub fn load_active_leases(&self, now: u64) -> Result<Vec<resilience::ProviderLease>> {
1700 self.with_connection(|connection| {
1701 let mut stmt = connection
1702 .prepare(
1703 "SELECT provider, content_hash, ticket, sequence, issued_at, expires_at, signature
1704 FROM provider_leases WHERE expires_at > ?1 ORDER BY issued_at",
1705 )
1706 .map_err(|error| database_error("failed to prepare active leases query", error))?;
1707 let leases = stmt
1708 .query_map(params![i64::try_from(now).unwrap_or(i64::MAX)], |row| {
1709 let provider_str: String = row.get(0)?;
1710 let hash_bytes: Vec<u8> = row.get(1)?;
1711 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1712 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1713 "invalid hash length",
1714 format!("expected 32 bytes, got {}", error.len()),
1715 )))
1716 })?;
1717 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1718 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1719 "invalid provider",
1720 error,
1721 )))
1722 })?;
1723 Ok(resilience::ProviderLease {
1724 hash: Hash::from(arr),
1725 provider,
1726 ticket: row.get(2)?,
1727 sequence: u64::try_from(row.get::<_, i64>(3)?).unwrap_or_default(),
1728 issued_at: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1729 expires_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1730 signature: row.get(6)?,
1731 })
1732 })
1733 .map_err(|error| database_error("failed to query active leases", error))?
1734 .collect::<std::result::Result<Vec<_>, _>>()
1735 .map_err(|error| database_error("failed to read lease rows", error))?;
1736 Ok(leases)
1737 })
1738 }
1739
1740 pub fn insert_provider_ban(&self, ban: &resilience::BanRecord) -> Result<()> {
1746 self.with_connection(|connection| {
1747 connection
1748 .execute(
1749 "INSERT OR REPLACE INTO provider_bans_v2(provider, content_hash, banned_at, expires_at, reason, source)
1750 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1751 params![
1752 ban.provider.to_string(),
1753 ban.hash.map(|h| h.as_bytes().to_vec()),
1754 i64::try_from(ban.banned_at).unwrap_or(i64::MAX),
1755 ban.expires_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1756 ban.reason,
1757 format!("{:?}", ban.source),
1758 ],
1759 )
1760 .map_err(|error| database_error("failed to insert provider ban", error))?;
1761 Ok(())
1762 })
1763 }
1764
1765 pub fn load_active_bans(&self, now: u64) -> Result<Vec<resilience::BanRecord>> {
1771 self.with_connection(|connection| {
1772 let mut stmt = connection
1773 .prepare(
1774 "SELECT provider, content_hash, banned_at, expires_at, reason, source
1775 FROM provider_bans_v2 WHERE expires_at IS NULL OR expires_at > ?1 ORDER BY banned_at",
1776 )
1777 .map_err(|error| database_error("failed to prepare active bans query", error))?;
1778 let bans = stmt
1779 .query_map(params![i64::try_from(now).unwrap_or(i64::MAX)], |row| {
1780 let provider_str: String = row.get(0)?;
1781 let content_hash: Option<Vec<u8>> = row.get(1)?;
1782 let source_str: String = row.get(5)?;
1783 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1784 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1785 "invalid provider",
1786 error,
1787 )))
1788 })?;
1789 let hash = content_hash.and_then(|b| {
1790 let arr: [u8; 32] = b.try_into().ok()?;
1791 Some(Hash::from(arr))
1792 });
1793 let source = match source_str.as_str() {
1794 "Automated" => resilience::BanSource::Automated,
1795 "WoT" => resilience::BanSource::WoT,
1796 _ => resilience::BanSource::Manual,
1797 };
1798 Ok(resilience::BanRecord {
1799 provider,
1800 hash,
1801 banned_at: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1802 expires_at: row
1803 .get::<_, Option<i64>>(3)?
1804 .map(|v| u64::try_from(v).unwrap_or_default()),
1805 reason: row.get::<_, String>(4)?,
1806 source,
1807 })
1808 })
1809 .map_err(|error| database_error("failed to query active provider bans", error))?
1810 .collect::<std::result::Result<Vec<_>, _>>()
1811 .map_err(|error| database_error("failed to read ban rows", error))?;
1812 Ok(bans)
1813 })
1814 }
1815
1816 pub fn load_all_reputations(&self) -> Result<HashMap<PublicKey, reputation::ProviderReputation>> {
1822 self.with_connection(|connection| {
1823 let mut stmt = connection
1824 .prepare(
1825 "SELECT provider, total_fetches, successful_fetches, failed_fetches,
1826 consecutive_failures, last_success_at, last_failure_at
1827 FROM provider_reputation",
1828 )
1829 .map_err(|error| database_error("failed to prepare reputations query", error))?;
1830 let rows = stmt
1831 .query_map([], |row| {
1832 let provider_str: String = row.get(0)?;
1833 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1834 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1835 "invalid provider",
1836 error,
1837 )))
1838 })?;
1839 let rep = reputation::ProviderReputation {
1840 provider,
1841 total_fetches: u64::try_from(row.get::<_, i64>(1)?).unwrap_or_default(),
1842 successful_fetches: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1843 failed_fetches: u64::try_from(row.get::<_, i64>(3)?).unwrap_or_default(),
1844 consecutive_failures: u32::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1845 last_success_at: row
1846 .get::<_, Option<i64>>(5)?
1847 .map(|v| u64::try_from(v).unwrap_or_default()),
1848 last_failure_at: row
1849 .get::<_, Option<i64>>(6)?
1850 .map(|v| u64::try_from(v).unwrap_or_default()),
1851 };
1852 Ok((provider, rep))
1853 })
1854 .map_err(|error| database_error("failed to query reputations", error))?
1855 .collect::<std::result::Result<HashMap<_, _>, _>>()
1856 .map_err(|error| database_error("failed to read reputation rows", error))?;
1857 Ok(rows)
1858 })
1859 }
1860
1861 pub fn load_signal_sequences(&self) -> Result<HashMap<(PublicKey, PublicKey), u64>> {
1867 self.with_connection(|connection| {
1868 let mut stmt = connection
1869 .prepare("SELECT reporter, provider, last_sequence FROM provider_signal_sequences")
1870 .map_err(|error| database_error("failed to prepare signal sequences query", error))?;
1871 let rows = stmt
1872 .query_map([], |row| {
1873 let reporter_str: String = row.get(0)?;
1874 let provider_str: String = row.get(1)?;
1875 let reporter = reporter_str.parse::<PublicKey>().map_err(|error| {
1876 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1877 "invalid reporter",
1878 error,
1879 )))
1880 })?;
1881 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1882 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1883 "invalid provider",
1884 error,
1885 )))
1886 })?;
1887 Ok((
1888 (reporter, provider),
1889 u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1890 ))
1891 })
1892 .map_err(|error| database_error("failed to query signal sequences", error))?
1893 .collect::<std::result::Result<HashMap<_, _>, _>>()
1894 .map_err(|error| database_error("failed to read signal sequence rows", error))?;
1895 Ok(rows)
1896 })
1897 }
1898
1899 pub fn upsert_reputation(
1905 &self,
1906 provider: &PublicKey,
1907 rep: &reputation::ProviderReputation,
1908 auto_ban_until: Option<u64>,
1909 auto_ban_count: u32,
1910 ) -> Result<()> {
1911 self.with_connection(|connection| {
1912 connection
1913 .execute(
1914 "INSERT OR REPLACE INTO provider_reputation
1915 (provider, total_fetches, successful_fetches, failed_fetches,
1916 consecutive_failures, last_success_at, last_failure_at,
1917 auto_ban_until, auto_ban_count)
1918 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
1919 params![
1920 provider.to_string(),
1921 i64::try_from(rep.total_fetches).unwrap_or(i64::MAX),
1922 i64::try_from(rep.successful_fetches).unwrap_or(i64::MAX),
1923 i64::try_from(rep.failed_fetches).unwrap_or(i64::MAX),
1924 i64::from(rep.consecutive_failures),
1925 rep.last_success_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1926 rep.last_failure_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1927 auto_ban_until.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1928 i64::from(auto_ban_count),
1929 ],
1930 )
1931 .map_err(|error| database_error("failed to upsert reputation", error))?;
1932 Ok(())
1933 })
1934 }
1935
1936 pub fn delete_stale_reputations(&self, now: u64, ttl_secs: u64) -> Result<()> {
1942 self.with_connection(|connection| {
1943 let cutoff = i64::try_from(now.saturating_sub(ttl_secs)).unwrap_or(i64::MIN);
1944 connection
1945 .execute(
1946 "DELETE FROM provider_reputation
1947 WHERE (last_success_at IS NULL OR last_success_at < ?1)
1948 AND (last_failure_at IS NULL OR last_failure_at < ?1)",
1949 params![cutoff],
1950 )
1951 .map_err(|error| database_error("failed to delete stale reputations", error))?;
1952 Ok(())
1953 })
1954 }
1955
1956 pub fn load_auto_bans(&self) -> Result<HashMap<PublicKey, (Option<u64>, u32)>> {
1964 self.with_connection(|connection| {
1965 let mut stmt = connection
1966 .prepare("SELECT provider, auto_ban_until, auto_ban_count FROM provider_reputation")
1967 .map_err(|error| database_error("failed to prepare auto bans query", error))?;
1968 let rows = stmt
1969 .query_map([], |row| {
1970 let provider_str: String = row.get(0)?;
1971 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1972 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1973 "invalid provider",
1974 error,
1975 )))
1976 })?;
1977 Ok((
1978 provider,
1979 (
1980 row.get::<_, Option<i64>>(1)?
1981 .map(|v| u64::try_from(v).unwrap_or_default()),
1982 u32::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1983 ),
1984 ))
1985 })
1986 .map_err(|error| database_error("failed to query auto bans", error))?
1987 .collect::<std::result::Result<HashMap<_, _>, _>>()
1988 .map_err(|error| database_error("failed to read auto ban rows", error))?;
1989 Ok(rows)
1990 })
1991 }
1992
1993 pub fn upsert_signal_sequence(&self, reporter: &PublicKey, provider: &PublicKey, sequence: u64) -> Result<()> {
1999 self.with_connection(|connection| {
2000 connection
2001 .execute(
2002 "INSERT OR REPLACE INTO provider_signal_sequences(reporter, provider, last_sequence)
2003 VALUES (?1, ?2, ?3)",
2004 params![
2005 reporter.to_string(),
2006 provider.to_string(),
2007 i64::try_from(sequence).unwrap_or(i64::MAX),
2008 ],
2009 )
2010 .map_err(|error| database_error("failed to upsert signal sequence", error))?;
2011 Ok(())
2012 })
2013 }
2014
2015 pub fn save_provider_trust_records(&self, records: &[wot::ProviderTrustRecord]) -> Result<()> {
2021 self.with_connection(|connection| {
2022 connection.execute("DELETE FROM provider_trust_records_v2", [])
2023 .map_err(|error| database_error("failed to clear provider trust records", error))?;
2024 for r in records {
2025 let action_str = format!("{:?}", r.action);
2026 connection.execute(
2027 "INSERT INTO provider_trust_records_v2(provider, action, scope, issuer, sequence, issued_at, expires_at, reason, signature)
2028 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
2029 params![
2030 r.provider.to_string(), action_str,
2031 r.scope.map(|h| h.as_bytes().to_vec()),
2032 r.issuer,
2033 i64::try_from(r.sequence).unwrap_or(i64::MAX),
2034 i64::try_from(r.issued_at).unwrap_or(i64::MAX),
2035 r.expires_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
2036 r.reason,
2037 r.signature,
2038 ],
2039 ).map_err(|error| database_error("failed to save provider trust record", error))?;
2040 }
2041 Ok(())
2042 })
2043 }
2044
2045 pub fn load_provider_trust_records(&self) -> Result<Vec<wot::ProviderTrustRecord>> {
2051 self.with_connection(|connection| {
2052 let mut stmt = connection
2053 .prepare(
2054 "SELECT provider, action, scope, issuer, sequence, issued_at, expires_at, reason, signature
2055 FROM provider_trust_records_v2 ORDER BY issued_at",
2056 )
2057 .map_err(|error| database_error("failed to prepare trust records query", error))?;
2058 let records = stmt
2059 .query_map([], |row| {
2060 let provider_str: String = row.get(0)?;
2061 let action_str: String = row.get(1)?;
2062 let scope: Option<Vec<u8>> = row.get(2)?;
2063 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
2064 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
2065 "invalid provider",
2066 error,
2067 )))
2068 })?;
2069 let action = match action_str.as_str() {
2070 "Trust" => wot::ProviderTrustAction::Trust,
2071 "Distrust" => wot::ProviderTrustAction::Distrust,
2072 "Vouch" => wot::ProviderTrustAction::Vouch,
2073 "Warn" => wot::ProviderTrustAction::Warn,
2074 _ => {
2075 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
2076 SyncwebError::operation("unknown trust action", action_str),
2077 )));
2078 }
2079 };
2080 let hash = scope.and_then(|b| {
2081 let arr: [u8; 32] = b.try_into().ok()?;
2082 Some(Hash::from(arr))
2083 });
2084 Ok(wot::ProviderTrustRecord {
2085 provider,
2086 action,
2087 scope: hash,
2088 issuer: row.get::<_, String>(3)?,
2089 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
2090 issued_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
2091 expires_at: row
2092 .get::<_, Option<i64>>(6)?
2093 .map(|v| u64::try_from(v).unwrap_or_default()),
2094 reason: row.get::<_, String>(7)?,
2095 signature: row.get(8)?,
2096 })
2097 })
2098 .map_err(|error| database_error("failed to query provider trust records", error))?
2099 .collect::<std::result::Result<Vec<_>, _>>()
2100 .map_err(|error| database_error("failed to read trust record rows", error))?;
2101 Ok(records)
2102 })
2103 }
2104
2105 pub fn save_provider_trust_signals(&self, signals: &[reputation::ProviderTrustSignal]) -> Result<()> {
2111 self.with_connection(|connection| {
2112 connection.execute("DELETE FROM provider_trust_signals_v2", [])
2113 .map_err(|error| database_error("failed to clear provider trust signals", error))?;
2114 for s in signals {
2115 let kind_str = format!("{:?}", s.signal);
2116 connection.execute(
2117 "INSERT INTO provider_trust_signals_v2(reporter, provider, signal_kind, content_hash, sequence, timestamp, signature)
2118 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
2119 params![
2120 s.reporter.to_string(), s.provider.to_string(),
2121 kind_str,
2122 s.hash.map(|h| h.as_bytes().to_vec()),
2123 i64::try_from(s.sequence).unwrap_or(i64::MAX),
2124 i64::try_from(s.timestamp).unwrap_or(i64::MAX),
2125 s.signature,
2126 ],
2127 ).map_err(|error| database_error("failed to save trust signal", error))?;
2128 }
2129 Ok(())
2130 })
2131 }
2132
2133 pub fn load_provider_trust_signals(&self) -> Result<Vec<reputation::ProviderTrustSignal>> {
2139 self.with_connection(|connection| {
2140 let mut stmt = connection
2141 .prepare(
2142 "SELECT reporter, provider, signal_kind, content_hash, sequence, timestamp, signature
2143 FROM provider_trust_signals_v2 ORDER BY timestamp",
2144 )
2145 .map_err(|error| database_error("failed to prepare trust signals query", error))?;
2146 let signals = stmt
2147 .query_map([], |row| {
2148 let reporter_str: String = row.get(0)?;
2149 let provider_str: String = row.get(1)?;
2150 let kind_str: String = row.get(2)?;
2151 let content_hash: Option<Vec<u8>> = row.get(3)?;
2152 let reporter = reporter_str.parse::<PublicKey>().map_err(|error| {
2153 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
2154 "invalid reporter",
2155 error,
2156 )))
2157 })?;
2158 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
2159 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
2160 "invalid provider",
2161 error,
2162 )))
2163 })?;
2164 let kind = match kind_str.as_str() {
2165 "ObservedSuccess" => reputation::TrustSignalKind::ObservedSuccess,
2166 "ObservedFailure" => reputation::TrustSignalKind::ObservedFailure,
2167 "ObservedCorruption" => reputation::TrustSignalKind::ObservedCorruption,
2168 _ => {
2169 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
2170 SyncwebError::operation("unknown signal kind", kind_str),
2171 )));
2172 }
2173 };
2174 let hash = content_hash.and_then(|b| {
2175 let arr: [u8; 32] = b.try_into().ok()?;
2176 Some(Hash::from(arr))
2177 });
2178 Ok(reputation::ProviderTrustSignal {
2179 provider,
2180 signal: kind,
2181 hash,
2182 reporter,
2183 timestamp: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
2184 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
2185 signature: row.get(6)?,
2186 })
2187 })
2188 .map_err(|error| database_error("failed to query trust signals", error))?
2189 .collect::<std::result::Result<Vec<_>, _>>()
2190 .map_err(|error| database_error("failed to read trust signal rows", error))?;
2191 Ok(signals)
2192 })
2193 }
2194
2195 pub fn save_trust_streams(&self, streams: &[String]) -> Result<()> {
2201 self.with_connection(|connection| {
2202 connection
2203 .execute("DELETE FROM trust_streams", [])
2204 .map_err(|error| database_error("failed to clear trust streams", error))?;
2205 let now = now_seconds();
2206 for ns in streams {
2207 connection
2208 .execute(
2209 "INSERT INTO trust_streams(namespace, subscribed_at) VALUES (?1, ?2)",
2210 params![ns, now],
2211 )
2212 .map_err(|error| database_error("failed to save trust stream", error))?;
2213 }
2214 Ok(())
2215 })
2216 }
2217
2218 pub fn load_trust_streams(&self) -> Result<Vec<String>> {
2224 self.with_connection(|connection| {
2225 let mut stmt = connection
2226 .prepare("SELECT namespace FROM trust_streams ORDER BY subscribed_at")
2227 .map_err(|error| database_error("failed to prepare trust streams query", error))?;
2228 let streams = stmt
2229 .query_map([], |row| row.get::<_, String>(0))
2230 .map_err(|error| database_error("failed to query trust streams", error))?
2231 .collect::<std::result::Result<Vec<_>, _>>()
2232 .map_err(|error| database_error("failed to read trust stream rows", error))?;
2233 Ok(streams)
2234 })
2235 }
2236}
2237
2238#[derive(Clone)]
2240pub struct IndexingService {
2241 database: IndexingDatabase,
2242 events: broadcast::Sender<IndexingEvent>,
2243 tasks: Arc<Mutex<HashMap<NamespaceId, JoinHandle<()>>>>,
2244 denylist: denylist::DenylistService,
2245}
2246
2247impl std::fmt::Debug for IndexingService {
2248 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2249 formatter
2250 .debug_struct("IndexingService")
2251 .field("database", &self.database)
2252 .finish_non_exhaustive()
2253 }
2254}
2255
2256impl IndexingService {
2257 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
2263 Self::with_database(IndexingDatabase::open(path)?)
2264 }
2265
2266 pub fn in_memory() -> Result<Self> {
2272 Self::with_database(IndexingDatabase::in_memory()?)
2273 }
2274
2275 pub fn with_database(database: IndexingDatabase) -> Result<Self> {
2281 let (events, _) = broadcast::channel(EVENT_CAPACITY);
2282 let denylist = denylist::DenylistService::with_database(database.clone())?;
2283 Ok(Self {
2284 database,
2285 events,
2286 tasks: Arc::new(Mutex::new(HashMap::new())),
2287 denylist,
2288 })
2289 }
2290
2291 #[must_use]
2293 pub fn catalog_service(
2294 &self,
2295 docs: &crate::node::docs_engine::DocsEngine,
2296 blobs: &crate::node::blob_store::BlobStore,
2297 author: iroh_docs::AuthorId,
2298 ) -> CatalogService {
2299 CatalogService::new(self, docs, blobs, author)
2300 }
2301
2302 #[must_use]
2304 pub fn resilience_service(&self, config: resilience::ResilienceConfig) -> resilience::ResilienceService {
2305 resilience::ResilienceService::with_database(self.database.clone(), config)
2306 }
2307
2308 #[must_use]
2310 pub fn resilience_service_with_wot(
2311 &self,
2312 config: resilience::ResilienceConfig,
2313 wot: wot::WotService,
2314 ) -> resilience::ResilienceService {
2315 resilience::ResilienceService::with_database(self.database.clone(), config).with_trust_policy(wot)
2316 }
2317
2318 #[must_use]
2320 pub fn wot_service(&self, policy: wot::TrustPolicy) -> wot::WotService {
2321 wot::WotService::new(self, policy)
2322 }
2323
2324 #[must_use]
2326 pub fn denylist_service(&self) -> denylist::DenylistService {
2327 self.denylist.clone()
2328 }
2329
2330 #[must_use]
2332 pub fn denylist(&self) -> denylist::DenylistService {
2333 self.denylist_service()
2334 }
2335
2336 #[must_use]
2337 pub const fn database(&self) -> &IndexingDatabase {
2338 &self.database
2339 }
2340
2341 #[must_use]
2342 pub const fn db(&self) -> &IndexingDatabase {
2343 &self.database
2344 }
2345
2346 pub fn search_local(&self, query: &str, limit: usize) -> Result<Vec<IndexedEntry>> {
2352 self.database.search(query, limit)
2353 }
2354
2355 pub fn search_global(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
2361 self.database.search_catalogs(query, limit)
2362 }
2363
2364 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
2372 self.search_global(query, limit)
2373 }
2374
2375 #[must_use]
2377 pub fn subscribe(&self) -> broadcast::Receiver<IndexingEvent> {
2378 self.events.subscribe()
2379 }
2380
2381 #[must_use]
2383 pub fn subscribe_events(&self) -> broadcast::Receiver<IndexingEvent> {
2384 self.subscribe()
2385 }
2386
2387 pub async fn enable_folder(&self, folder: &SyncwebFolder) -> Result<IndexingHandle> {
2395 let namespace_id = folder.namespace_id();
2396 if self
2397 .tasks
2398 .lock()
2399 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2400 .get(&namespace_id)
2401 .is_some_and(|task| !task.is_finished())
2402 {
2403 return Ok(IndexingHandle { namespace_id });
2404 }
2405
2406 let live_events = folder.docs_engine().watch(folder.doc()).await?;
2407 self.database.enable_folder(namespace_id, &namespace_id.to_string())?;
2408 send_event(&self.events, IndexingEvent::FolderEnabled { namespace_id });
2409
2410 for entry in folder.docs_engine().list_latest(folder.doc()).await? {
2411 let indexed = self.index_entry(namespace_id, &entry)?;
2412 send_event(&self.events, IndexingEvent::EntryIndexed(indexed));
2413 }
2414
2415 let database = self.database.clone();
2416 let events = self.events.clone();
2417 let task = tokio::spawn(async move {
2418 consume_folder_events(namespace_id, live_events, database, events).await;
2419 });
2420 self.tasks
2421 .lock()
2422 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2423 .insert(namespace_id, task);
2424 Ok(IndexingHandle { namespace_id })
2425 }
2426
2427 pub async fn disable_folder(&self, namespace_id: NamespaceId) -> Result<()> {
2433 let task_handle = self
2434 .tasks
2435 .lock()
2436 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2437 .remove(&namespace_id);
2438 if let Some(task) = task_handle {
2439 task.abort();
2440 }
2441 let database = self.database.clone();
2442 tokio::task::spawn_blocking(move || database.disable_folder(namespace_id))
2443 .await
2444 .map_err(|error| SyncwebError::operation("indexing folder removal task failed", error))??;
2445 send_event(&self.events, IndexingEvent::FolderDisabled { namespace_id });
2446 Ok(())
2447 }
2448
2449 pub fn is_folder_enabled(&self, namespace_id: NamespaceId) -> Result<bool> {
2455 let active_task = self
2456 .tasks
2457 .lock()
2458 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2459 .get(&namespace_id)
2460 .is_some_and(|task| !task.is_finished());
2461 Ok(active_task || self.database.is_folder_enabled(namespace_id)?)
2462 }
2463
2464 pub fn index_entry(&self, namespace_id: NamespaceId, entry: &Entry) -> Result<IndexedEntry> {
2474 if !self.database.is_folder_enabled(namespace_id)? {
2475 return Err(SyncwebError::FolderNotFound(namespace_id.to_string()));
2476 }
2477 self.database
2478 .upsert_entry(namespace_id, entry.key(), entry.content_hash(), entry.content_len())
2479 }
2480}
2481
2482async fn consume_folder_events(
2483 namespace_id: NamespaceId,
2484 mut live_events: impl n0_future::Stream<Item = Result<LiveEvent>> + Send + Unpin + 'static,
2485 database: IndexingDatabase,
2486 events: broadcast::Sender<IndexingEvent>,
2487) {
2488 while let Some(event_result) = live_events.next().await {
2489 let event = match event_result {
2490 Ok(event) => event,
2491 Err(error) => {
2492 send_event(
2493 &events,
2494 IndexingEvent::Error {
2495 namespace_id,
2496 message: error.to_string(),
2497 },
2498 );
2499 break;
2500 }
2501 };
2502 match event {
2503 LiveEvent::InsertLocal { entry } | LiveEvent::InsertRemote { entry, .. } => {
2504 match database.upsert_entry(namespace_id, entry.key(), entry.content_hash(), entry.content_len()) {
2505 Ok(indexed) => send_event(&events, IndexingEvent::EntryIndexed(indexed)),
2506 Err(error) => {
2507 send_event(
2508 &events,
2509 IndexingEvent::Error {
2510 namespace_id,
2511 message: error.to_string(),
2512 },
2513 );
2514 }
2515 }
2516 }
2517 LiveEvent::ContentReady { hash } => send_event(
2518 &events,
2519 IndexingEvent::Core {
2520 namespace_id,
2521 event: CoreIndexingEvent::ContentReady { hash },
2522 },
2523 ),
2524 LiveEvent::PendingContentReady => send_event(
2525 &events,
2526 IndexingEvent::Core {
2527 namespace_id,
2528 event: CoreIndexingEvent::PendingContentReady,
2529 },
2530 ),
2531 LiveEvent::NeighborUp(_) => send_event(
2532 &events,
2533 IndexingEvent::Core {
2534 namespace_id,
2535 event: CoreIndexingEvent::NeighborUp,
2536 },
2537 ),
2538 LiveEvent::NeighborDown(_) => send_event(
2539 &events,
2540 IndexingEvent::Core {
2541 namespace_id,
2542 event: CoreIndexingEvent::NeighborDown,
2543 },
2544 ),
2545 LiveEvent::SyncFinished(_) => send_event(
2546 &events,
2547 IndexingEvent::Core {
2548 namespace_id,
2549 event: CoreIndexingEvent::SyncFinished,
2550 },
2551 ),
2552 }
2553 }
2554}
2555
2556fn indexed_entry_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<IndexedEntry> {
2557 let namespace = row.get::<_, String>(0)?;
2558 let namespace_id = NamespaceId::from_str(&namespace).map_err(|error| {
2559 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
2560 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(conversion_error))
2561 })?;
2562 let key = row.get(1)?;
2563 let hash_bytes = row.get::<_, Vec<u8>>(2)?;
2564 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
2565 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(2, rusqlite::types::Type::Blob, Box::new(error)))?;
2566 let size_value = row.get::<_, i64>(3)?;
2567 let size = u64::try_from(size_value).map_err(|error| {
2568 rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Integer, Box::new(error))
2569 })?;
2570 Ok(IndexedEntry {
2571 namespace_id,
2572 key,
2573 hash: Hash::from_bytes(hash_array),
2574 size,
2575 })
2576}
2577
2578fn catalog_record_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<CatalogRecord> {
2579 let catalog_namespace = row.get::<_, String>(0)?;
2580 let catalog_namespace_id = NamespaceId::from_str(&catalog_namespace).map_err(|error| {
2581 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
2582 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(conversion_error))
2583 })?;
2584 let folder_namespace = row.get::<_, String>(1)?;
2585 let folder_namespace_id = NamespaceId::from_str(&folder_namespace).map_err(|error| {
2586 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
2587 rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(conversion_error))
2588 })?;
2589 let hash_bytes = row.get::<_, Vec<u8>>(3)?;
2590 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
2591 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Blob, Box::new(error)))?;
2592 let size_value = row.get::<_, i64>(4)?;
2593 let size = u64::try_from(size_value).map_err(|error| {
2594 rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Integer, Box::new(error))
2595 })?;
2596 let tags_json = row.get::<_, String>(7)?;
2597 let tags = serde_json::from_str(&tags_json)
2598 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(error)))?;
2599 Ok(CatalogRecord {
2600 catalog_namespace_id,
2601 folder_namespace_id,
2602 key: row.get(2)?,
2603 hash: Hash::from_bytes(hash_array),
2604 size,
2605 folder_name: row.get(5)?,
2606 title: row.get(6)?,
2607 tags,
2608 publisher: row.get(8)?,
2609 })
2610}
2611
2612fn wot_metadata_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<wot::MetadataEntry> {
2613 let hash_bytes = row.get::<_, Vec<u8>>(0)?;
2614 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
2615 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Blob, Box::new(error)))?;
2616 let sequence_value = row.get::<_, i64>(4)?;
2617 let sequence = u64::try_from(sequence_value).map_err(|error| {
2618 rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Integer, Box::new(error))
2619 })?;
2620 let created_at_value = row.get::<_, i64>(5)?;
2621 let created_at = u64::try_from(created_at_value).map_err(|error| {
2622 rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Integer, Box::new(error))
2623 })?;
2624 Ok(wot::MetadataEntry {
2625 content: Hash::from_bytes(hash_array),
2626 key: row.get(1)?,
2627 value: row.get(2)?,
2628 author: row.get(3)?,
2629 sequence,
2630 created_at,
2631 signature: row.get(6)?,
2632 })
2633}
2634
2635const SCHEMA_PART1: &str = "PRAGMA journal_mode = WAL;
2636 CREATE TABLE IF NOT EXISTS index_metadata (
2637 key TEXT PRIMARY KEY NOT NULL,
2638 value TEXT NOT NULL
2639 );
2640 CREATE TABLE IF NOT EXISTS indexed_folders (
2641 namespace_id TEXT PRIMARY KEY NOT NULL,
2642 label TEXT NOT NULL,
2643 enabled_at INTEGER NOT NULL
2644 );
2645 CREATE TABLE IF NOT EXISTS indexed_entries (
2646 id INTEGER PRIMARY KEY,
2647 namespace_id TEXT NOT NULL REFERENCES indexed_folders(namespace_id) ON DELETE CASCADE,
2648 entry_key BLOB NOT NULL,
2649 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2650 content_len INTEGER NOT NULL CHECK(content_len >= 0),
2651 updated_at INTEGER NOT NULL,
2652 UNIQUE(namespace_id, entry_key)
2653 );
2654 CREATE INDEX IF NOT EXISTS indexed_entries_namespace
2655 ON indexed_entries(namespace_id);
2656 CREATE VIRTUAL TABLE IF NOT EXISTS indexed_entries_fts USING fts5(
2657 namespace_id UNINDEXED,
2658 entry_key,
2659 title,
2660 tags,
2661 tokenize = 'unicode61'
2662 );
2663 CREATE TABLE IF NOT EXISTS indexed_catalogs (
2664 namespace_id TEXT PRIMARY KEY NOT NULL,
2665 label TEXT NOT NULL,
2666 subscribed_at INTEGER NOT NULL
2667 );
2668 CREATE TABLE IF NOT EXISTS indexed_catalog_entries (
2669 id INTEGER PRIMARY KEY,
2670 catalog_namespace_id TEXT NOT NULL REFERENCES indexed_catalogs(namespace_id) ON DELETE CASCADE,
2671 folder_namespace_id TEXT NOT NULL,
2672 entry_key BLOB NOT NULL,
2673 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2674 content_len INTEGER NOT NULL CHECK(content_len >= 0),
2675 folder_name TEXT NOT NULL,
2676 title TEXT NOT NULL,
2677 tags TEXT NOT NULL,
2678 publisher TEXT NOT NULL,
2679 updated_at INTEGER NOT NULL,
2680 UNIQUE(catalog_namespace_id, folder_namespace_id, entry_key)
2681 );
2682 CREATE INDEX IF NOT EXISTS indexed_catalog_entries_catalog
2683 ON indexed_catalog_entries(catalog_namespace_id);
2684 CREATE VIRTUAL TABLE IF NOT EXISTS indexed_catalog_entries_fts USING fts5(
2685 catalog_namespace_id UNINDEXED,
2686 folder_namespace_id UNINDEXED,
2687 entry_key,
2688 folder_name,
2689 title,
2690 tags,
2691 publisher,
2692 content_hash,
2693 tokenize = 'unicode61'
2694 );
2695 CREATE TABLE IF NOT EXISTS wot_metadata (
2696 id INTEGER PRIMARY KEY,
2697 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2698 metadata_key TEXT NOT NULL,
2699 metadata_value TEXT NOT NULL,
2700 author TEXT NOT NULL,
2701 sequence INTEGER NOT NULL CHECK(sequence > 0),
2702 created_at INTEGER NOT NULL CHECK(created_at >= 0),
2703 signature TEXT NOT NULL,
2704 UNIQUE(content_hash, metadata_key, author, sequence)
2705 );
2706 CREATE INDEX IF NOT EXISTS wot_metadata_content
2707 ON wot_metadata(content_hash);
2708 CREATE VIRTUAL TABLE IF NOT EXISTS wot_metadata_fts USING fts5(
2709 content_hash UNINDEXED,
2710 metadata_key,
2711 metadata_value,
2712 author,
2713 tokenize = 'unicode61'
2714 );";
2715
2716const SCHEMA_PART2: &str = "CREATE TABLE IF NOT EXISTS stable_links (
2717 link TEXT PRIMARY KEY NOT NULL,
2718 kind TEXT NOT NULL,
2719 publisher TEXT,
2720 alias TEXT,
2721 content_hash BLOB CHECK(content_hash IS NULL OR length(content_hash) = 32),
2722 sequence INTEGER NOT NULL CHECK(sequence >= 0),
2723 version TEXT,
2724 payload BLOB NOT NULL,
2725 updated_at INTEGER NOT NULL
2726 );
2727CREATE TABLE IF NOT EXISTS link_mirrors (
2728link TEXT NOT NULL,
2729provider TEXT NOT NULL,
2730ticket TEXT NOT NULL,
2731priority INTEGER NOT NULL DEFAULT 0,
2732updated_at INTEGER NOT NULL,
2733PRIMARY KEY(link, provider)
2734);
2735CREATE TABLE IF NOT EXISTS revoked_links (
2736 manifest BLOB NOT NULL CHECK(length(manifest) = 32),
2737 capability TEXT NOT NULL,
2738 revoked_at INTEGER NOT NULL,
2739 PRIMARY KEY(manifest, capability)
2740);
2741 CREATE TABLE IF NOT EXISTS denylist_rules (
2742 rule_type TEXT NOT NULL,
2743 rule_value BLOB NOT NULL,
2744 namespace_id TEXT,
2745 updated_at INTEGER NOT NULL,
2746 PRIMARY KEY(rule_type, rule_value, namespace_id)
2747 );
2748 CREATE TABLE IF NOT EXISTS filter_lists (
2749 namespace_id TEXT PRIMARY KEY NOT NULL,
2750 sequence INTEGER NOT NULL CHECK(sequence > 0),
2751 publisher TEXT NOT NULL,
2752 payload BLOB NOT NULL,
2753 updated_at INTEGER NOT NULL
2754 );
2755 CREATE TABLE IF NOT EXISTS moderation_records (
2756 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2757 scope TEXT NOT NULL,
2758 sequence INTEGER NOT NULL CHECK(sequence > 0),
2759 payload BLOB NOT NULL,
2760 updated_at INTEGER NOT NULL,
2761 PRIMARY KEY(content_hash, scope)
2762 );
2763 CREATE TABLE IF NOT EXISTS provider_leases (
2764 provider TEXT NOT NULL,
2765 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2766 ticket TEXT NOT NULL,
2767 sequence INTEGER NOT NULL,
2768 issued_at INTEGER NOT NULL,
2769 expires_at INTEGER NOT NULL,
2770 signature TEXT,
2771 PRIMARY KEY(provider, content_hash)
2772 );
2773 CREATE INDEX IF NOT EXISTS idx_provider_leases_hash ON provider_leases(content_hash);";
2774
2775const SCHEMA_PART3: &str = "CREATE TABLE IF NOT EXISTS provider_reputation (
2776 provider TEXT PRIMARY KEY NOT NULL,
2777 total_fetches INTEGER NOT NULL DEFAULT 0,
2778 successful_fetches INTEGER NOT NULL DEFAULT 0,
2779 failed_fetches INTEGER NOT NULL DEFAULT 0,
2780 consecutive_failures INTEGER NOT NULL DEFAULT 0,
2781 last_success_at INTEGER,
2782 last_failure_at INTEGER,
2783 auto_ban_until INTEGER,
2784 auto_ban_count INTEGER NOT NULL DEFAULT 0
2785 );
2786 CREATE TABLE IF NOT EXISTS provider_signal_sequences (
2787 reporter TEXT NOT NULL,
2788 provider TEXT NOT NULL,
2789 last_sequence INTEGER NOT NULL,
2790 PRIMARY KEY(reporter, provider)
2791 );
2792 CREATE TABLE IF NOT EXISTS trust_delegations (
2793 delegator TEXT NOT NULL,
2794 delegate TEXT NOT NULL,
2795 scope BLOB CHECK(scope IS NULL OR length(scope) = 32),
2796 max_depth INTEGER,
2797 sequence INTEGER NOT NULL,
2798 issued_at INTEGER NOT NULL,
2799 expires_at INTEGER NOT NULL,
2800 revoked_at INTEGER,
2801 signature TEXT,
2802 PRIMARY KEY(delegator, delegate, sequence)
2803 );
2804 CREATE TABLE IF NOT EXISTS moderation_records_v2 (
2805 id INTEGER PRIMARY KEY AUTOINCREMENT,
2806 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2807 moderator TEXT NOT NULL,
2808 action TEXT NOT NULL,
2809 scope_json TEXT NOT NULL,
2810 sequence INTEGER NOT NULL,
2811 created_at INTEGER NOT NULL,
2812 reason TEXT NOT NULL,
2813 signature TEXT
2814 );
2815 CREATE TABLE IF NOT EXISTS attestations_v2 (
2816 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2817 issuer TEXT NOT NULL,
2818 kind TEXT NOT NULL,
2819 kind_other TEXT,
2820 value TEXT NOT NULL,
2821 sequence INTEGER NOT NULL,
2822 issued_at INTEGER NOT NULL,
2823 signature TEXT,
2824 PRIMARY KEY(content_hash, issuer, kind)
2825 );
2826 CREATE TABLE IF NOT EXISTS content_reports_v2 (
2827 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2828 reporter TEXT NOT NULL,
2829 reason TEXT NOT NULL,
2830 scope TEXT NOT NULL DEFAULT 'global',
2831 created_at INTEGER NOT NULL,
2832 signature TEXT,
2833 PRIMARY KEY(content_hash, reporter, reason)
2834 );
2835 CREATE TABLE IF NOT EXISTS provider_trust_records_v2 (
2836 id INTEGER PRIMARY KEY AUTOINCREMENT,
2837 provider TEXT NOT NULL,
2838 action TEXT NOT NULL,
2839 scope BLOB CHECK(scope IS NULL OR length(scope) = 32),
2840 issuer TEXT NOT NULL,
2841 sequence INTEGER NOT NULL,
2842 issued_at INTEGER NOT NULL,
2843 expires_at INTEGER,
2844 reason TEXT NOT NULL,
2845 signature TEXT
2846 );
2847 CREATE TABLE IF NOT EXISTS provider_bans_v2 (
2848 provider TEXT NOT NULL,
2849 content_hash BLOB CHECK(content_hash IS NULL OR length(content_hash) = 32),
2850 banned_at INTEGER NOT NULL,
2851 expires_at INTEGER,
2852 reason TEXT NOT NULL,
2853 source TEXT NOT NULL,
2854 PRIMARY KEY(provider, content_hash)
2855 );
2856 CREATE TABLE IF NOT EXISTS provider_trust_signals_v2 (
2857 reporter TEXT NOT NULL,
2858 provider TEXT NOT NULL,
2859 signal_kind TEXT NOT NULL,
2860 content_hash BLOB CHECK(content_hash IS NULL OR length(content_hash) = 32),
2861 sequence INTEGER NOT NULL,
2862 timestamp INTEGER NOT NULL,
2863 signature TEXT,
2864 PRIMARY KEY(reporter, provider, sequence)
2865 );
2866 CREATE INDEX IF NOT EXISTS idx_trust_signals_provider_v2 ON provider_trust_signals_v2(provider);
2867 CREATE INDEX IF NOT EXISTS idx_trust_signals_ts_v2 ON provider_trust_signals_v2(timestamp);
2868 CREATE TABLE IF NOT EXISTS trust_streams (
2869 namespace TEXT PRIMARY KEY,
2870 subscribed_at INTEGER NOT NULL
2871 );";
2872
2873fn initialize_connection(connection: &Connection) -> Result<()> {
2874 connection
2875 .busy_timeout(std::time::Duration::from_secs(5))
2876 .map_err(|error| database_error("failed to configure indexing database", error))?;
2877 connection
2878 .pragma_update(None, "foreign_keys", "ON")
2879 .map_err(|error| database_error("failed to enable indexing foreign keys", error))?;
2880 initialize_schema(connection)?;
2881 connection
2882 .execute(
2883 "INSERT INTO index_metadata(key, value)
2884 VALUES ('schema_version', ?1)
2885 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
2886 [SCHEMA_VERSION],
2887 )
2888 .map_err(|error| database_error("failed to persist indexing schema version", error))?;
2889 Ok(())
2890}
2891
2892fn initialize_schema(connection: &Connection) -> Result<()> {
2893 for part in [SCHEMA_PART1, SCHEMA_PART2, SCHEMA_PART3] {
2894 connection
2895 .execute_batch(part)
2896 .map_err(|error| database_error("failed to initialize indexing database schema", error))?;
2897 }
2898 Ok(())
2899}
2900
2901fn now_seconds() -> i64 {
2902 SystemTime::now()
2903 .duration_since(UNIX_EPOCH)
2904 .map_or(0, |duration| i64::try_from(duration.as_secs()).unwrap_or(i64::MAX))
2905}
2906
2907fn database_error(context: &'static str, error: impl std::fmt::Display) -> SyncwebError {
2908 SyncwebError::operation(context, error)
2909}
2910
2911fn send_event(events: &broadcast::Sender<IndexingEvent>, event: IndexingEvent) {
2912 let _ = events.send(event);
2913}
2914
2915pub const REPORT_GOSSIP_TOPIC: &[u8] = b"syncweb/reports/v1";
2917
2918#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2920#[non_exhaustive]
2921pub struct ReportRecord {
2922 pub content: Hash,
2923 pub reason: String,
2924 pub created_at: u64,
2925 pub reporter: Option<String>,
2926 pub signature: Option<String>,
2927}
2928
2929impl ReportRecord {
2930 #[must_use]
2931 pub const fn new(content: Hash, reason: String, created_at: u64) -> Self {
2932 Self {
2933 content,
2934 reason,
2935 created_at,
2936 reporter: None,
2937 signature: None,
2938 }
2939 }
2940
2941 pub fn sign_with(mut self, signing_key: &SigningKey) -> Result<Self> {
2950 let payload = self.signing_payload()?;
2951 let sig = signing_key.sign(&payload);
2952 self.reporter = Some(hex::encode(signing_key.verifying_key().to_bytes()));
2953 self.signature = Some(hex::encode(sig.to_bytes()));
2954 Ok(self)
2955 }
2956
2957 pub fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
2963 let sig_hex = self
2964 .signature
2965 .as_ref()
2966 .ok_or_else(|| SyncwebError::MissingSignature("report has no signature".into()))?;
2967 let sig_bytes = hex::decode(sig_hex).map_err(|e| SyncwebError::operation("report signature hex decode", e))?;
2968 let sig_len = sig_bytes.len();
2969 let sig: [u8; 64] = sig_bytes.try_into().map_err(|_v| {
2970 SyncwebError::InvalidSignature(format!(
2971 "report signature has wrong length (expected 64, got {sig_len})"
2972 ))
2973 })?;
2974 let signature = Signature::from_bytes(&sig);
2975 public_key
2976 .verify(&self.signing_payload()?, &signature)
2977 .map_err(|e| SyncwebError::InvalidSignature(format!("report signature does not match: {e}")))
2978 }
2979
2980 fn signing_payload(&self) -> Result<Vec<u8>> {
2985 let canonical = (self.content.as_bytes(), &self.reason, self.created_at);
2986 serde_json::to_vec(&canonical).map_err(|e| SyncwebError::operation("canonical report serialization", e))
2987 }
2988}
2989
2990impl SignedGossipMessage for ReportRecord {
2991 fn verify_signature(&self) -> Result<()> {
2992 match &self.reporter {
2993 Some(reporter_hex) => {
2994 let bytes = hex::decode(reporter_hex).map_err(|e| SyncwebError::operation("decode reporter key", e))?;
2995 let key_len = bytes.len();
2996 let key_bytes: [u8; 32] = bytes.try_into().map_err(|_v| {
2997 SyncwebError::InvalidSignature(format!(
2998 "reporter key has wrong length (expected 32, got {key_len})"
2999 ))
3000 })?;
3001 let public_key = VerifyingKey::from_bytes(&key_bytes)
3002 .map_err(|e| SyncwebError::InvalidSignature(format!("invalid reporter key: {e}")))?;
3003 self.verify(&public_key)
3004 }
3005 None => {
3006 Ok(())
3008 }
3009 }
3010 }
3011}