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 if manifest_bytes.len() != 32 {
1094 return Err(rusqlite::Error::InvalidColumnType(
1095 0,
1096 "manifest must be 32 bytes".to_owned(),
1097 rusqlite::types::Type::Blob,
1098 ));
1099 }
1100 if capability_blob.len() != 32 {
1101 return Err(rusqlite::Error::InvalidColumnType(
1102 1,
1103 "capability must be 32 bytes".to_owned(),
1104 rusqlite::types::Type::Blob,
1105 ));
1106 }
1107 let Ok(manifests) = manifest_bytes.try_into() else {
1108 return Err(rusqlite::Error::InvalidColumnType(
1109 0,
1110 "manifest conversion failed despite length check".to_owned(),
1111 rusqlite::types::Type::Blob,
1112 ));
1113 };
1114 let hash = Hash::from_bytes(manifests);
1115 let Ok(cap) = capability_blob.try_into() else {
1116 return Err(rusqlite::Error::InvalidColumnType(
1117 1,
1118 "capability conversion failed despite length check".to_owned(),
1119 rusqlite::types::Type::Blob,
1120 ));
1121 };
1122 Ok((hash, cap))
1123 })
1124 {
1125 for row in rev_rows.flatten() {
1126 if let Ok(link) = PrivateLink::new(row.0, row.1, u64::MAX) {
1127 revoked.push(link);
1128 }
1129 }
1130 }
1131 Ok((pointers, mirrors, revoked))
1132 })
1133 }
1134
1135 pub fn save_links(
1141 &self,
1142 pointers: &[links::MutablePointer],
1143 mirrors: &[String],
1144 revoked: &[links::PrivateLink],
1145 ) -> Result<()> {
1146 self.with_connection(|connection| {
1147 connection.execute("DELETE FROM stable_links", [])
1148 .map_err(|error| database_error("failed to clear stable links", error))?;
1149 connection.execute("DELETE FROM link_mirrors", [])
1150 .map_err(|error| database_error("failed to clear link mirrors", error))?;
1151 connection.execute("DELETE FROM revoked_links", [])
1152 .map_err(|error| database_error("failed to clear revoked links", error))?;
1153 let now = now_seconds();
1154 for pointer in pointers {
1155 let payload = serde_json::to_vec(pointer)
1156 .map_err(|error| database_error("failed to serialize pointer", error))?;
1157 connection.execute(
1158 "INSERT OR REPLACE INTO stable_links(link, kind, publisher, alias, content_hash, sequence, version, payload, updated_at)
1159 VALUES (?1, 'mutable', ?2, ?3, NULL, ?4, ?5, ?6, ?7)",
1160 params![
1161 format!("mutable:{}:{}", pointer.publisher, pointer.alias),
1162 pointer.publisher.to_string(),
1163 pointer.alias,
1164 i64::try_from(pointer.sequence).unwrap_or(i64::MAX),
1165 pointer.version.as_deref(),
1166 payload,
1167 now,
1168 ],
1169 ).map_err(|error| database_error("failed to save pointer", error))?;
1170 }
1171 for uri in mirrors {
1172 connection.execute(
1173 "INSERT INTO link_mirrors(link, provider, ticket, priority, updated_at)
1174 VALUES ('', 'local', ?1, 0, ?2)",
1175 params![uri, now],
1176 ).map_err(|error| database_error("failed to save mirror", error))?;
1177 }
1178 for link in revoked {
1179 connection.execute(
1180 "INSERT OR REPLACE INTO revoked_links(manifest, capability, revoked_at)
1181 VALUES (?1, ?2, ?3)",
1182 params![link.manifest.as_bytes(), link.capability.as_slice(), now],
1183 ).map_err(|error| database_error("failed to save revoked link", error))?;
1184 }
1185 Ok(())
1186 })
1187 }
1188
1189 pub fn upsert_link_pointer(&self, pointer: &links::MutablePointer) -> Result<()> {
1195 let payload =
1196 serde_json::to_vec(pointer).map_err(|error| database_error("failed to serialize pointer", error))?;
1197 self.with_connection(|connection| {
1198 connection.execute(
1199 "INSERT OR REPLACE INTO stable_links(link, kind, publisher, alias, content_hash, sequence, version, payload, updated_at)
1200 VALUES (?1, 'mutable', ?2, ?3, NULL, ?4, ?5, ?6, ?7)",
1201 params![
1202 format!("mutable:{}:{}", pointer.publisher, pointer.alias),
1203 pointer.publisher.to_string(),
1204 pointer.alias,
1205 i64::try_from(pointer.sequence).unwrap_or(i64::MAX),
1206 pointer.version.as_deref(),
1207 payload,
1208 now_seconds(),
1209 ],
1210 )
1211 .map_err(|error| database_error("failed to upsert link pointer", error))?;
1212 Ok(())
1213 })
1214 }
1215
1216 pub fn upsert_link_mirror(&self, ticket: &str) -> Result<()> {
1222 self.with_connection(|connection| {
1223 connection
1224 .execute(
1225 "INSERT OR REPLACE INTO link_mirrors(link, provider, ticket, priority, updated_at)
1226 VALUES ('', 'local', ?1, 0, ?2)",
1227 params![ticket, now_seconds()],
1228 )
1229 .map_err(|error| database_error("failed to upsert link mirror", error))?;
1230 Ok(())
1231 })
1232 }
1233
1234 pub fn save_leases(&self, leases: &[resilience::ProviderLease]) -> Result<()> {
1240 self.with_connection(|connection| {
1241 connection.execute("DELETE FROM provider_leases", [])
1242 .map_err(|error| database_error("failed to clear provider leases", error))?;
1243 for lease in leases {
1244 connection.execute(
1245 "INSERT INTO provider_leases(provider, content_hash, ticket, sequence, issued_at, expires_at, signature)
1246 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1247 params![
1248 lease.provider.to_string(),
1249 lease.hash.as_bytes().to_vec(),
1250 lease.ticket,
1251 i64::try_from(lease.sequence).unwrap_or(i64::MAX),
1252 i64::try_from(lease.issued_at).unwrap_or(i64::MAX),
1253 i64::try_from(lease.expires_at).unwrap_or(i64::MAX),
1254 lease.signature,
1255 ],
1256 ).map_err(|error| database_error("failed to save lease", error))?;
1257 }
1258 Ok(())
1259 })
1260 }
1261
1262 pub fn load_leases(&self) -> Result<Vec<resilience::ProviderLease>> {
1268 self.with_connection(|connection| {
1269 let mut stmt = connection
1270 .prepare(
1271 "SELECT provider, content_hash, ticket, sequence, issued_at, expires_at, signature
1272 FROM provider_leases ORDER BY issued_at",
1273 )
1274 .map_err(|error| database_error("failed to prepare leases query", error))?;
1275 let leases = stmt
1276 .query_map([], |row| {
1277 let provider_str: String = row.get(0)?;
1278 let hash_bytes: Vec<u8> = row.get(1)?;
1279 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1280 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1281 "invalid hash length",
1282 format!("expected 32 bytes, got {}", error.len()),
1283 )))
1284 })?;
1285 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1286 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1287 "invalid provider",
1288 error,
1289 )))
1290 })?;
1291 Ok(resilience::ProviderLease {
1292 hash: Hash::from(arr),
1293 provider,
1294 ticket: row.get(2)?,
1295 sequence: u64::try_from(row.get::<_, i64>(3)?).unwrap_or_default(),
1296 issued_at: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1297 expires_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1298 signature: row.get(6)?,
1299 })
1300 })
1301 .map_err(|error| database_error("failed to query leases", error))?
1302 .collect::<std::result::Result<Vec<_>, _>>()
1303 .map_err(|error| database_error("failed to read lease rows", error))?;
1304 Ok(leases)
1305 })
1306 }
1307
1308 pub fn save_trust_delegations(&self, delegations: &[wot::TrustDelegation]) -> Result<()> {
1314 self.with_connection(|connection| {
1315 connection.execute("DELETE FROM trust_delegations", [])
1316 .map_err(|error| database_error("failed to clear trust delegations", error))?;
1317 for d in delegations {
1318 connection.execute(
1319 "INSERT INTO trust_delegations(delegator, delegate, scope, max_depth, sequence, issued_at, expires_at, revoked_at, signature)
1320 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
1321 params![
1322 d.delegator, d.delegate,
1323 d.scope.map(|h| h.as_bytes().to_vec()),
1324 d.max_depth.map(i64::from),
1325 i64::try_from(d.sequence).unwrap_or(i64::MAX),
1326 i64::try_from(d.issued_at).unwrap_or(i64::MAX),
1327 i64::try_from(d.expires_at).unwrap_or(i64::MAX),
1328 d.revoked_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1329 d.signature,
1330 ],
1331 ).map_err(|error| database_error("failed to save delegation", error))?;
1332 }
1333 Ok(())
1334 })
1335 }
1336
1337 pub fn load_trust_delegations(&self) -> Result<Vec<wot::TrustDelegation>> {
1343 self.with_connection(|connection| {
1344 let mut stmt = connection
1345 .prepare(
1346 "SELECT delegator, delegate, scope, max_depth, sequence, issued_at, expires_at, revoked_at, signature
1347 FROM trust_delegations ORDER BY issued_at",
1348 )
1349 .map_err(|error| database_error("failed to prepare delegations query", error))?;
1350 let delegations = stmt
1351 .query_map([], |row| {
1352 let scope: Option<Vec<u8>> = row.get(2)?;
1353 let hash = scope.and_then(|b| {
1354 let arr: [u8; 32] = b.try_into().ok()?;
1355 Some(Hash::from(arr))
1356 });
1357 let max_depth: Option<i64> = row.get(3)?;
1358 let revoked_at: Option<i64> = row.get(7)?;
1359 Ok(wot::TrustDelegation {
1360 delegator: row.get::<_, String>(0)?,
1361 delegate: row.get::<_, String>(1)?,
1362 scope: hash,
1363 max_depth: max_depth.map(|v| u32::try_from(v).unwrap_or(u32::MAX)),
1364 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1365 issued_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1366 expires_at: u64::try_from(row.get::<_, i64>(6)?).unwrap_or_default(),
1367 revoked_at: revoked_at.map(|v| u64::try_from(v).unwrap_or(u64::MAX)),
1368 signature: row.get(8)?,
1369 })
1370 })
1371 .map_err(|error| database_error("failed to query delegations", error))?
1372 .collect::<std::result::Result<Vec<_>, _>>()
1373 .map_err(|error| database_error("failed to read delegation rows", error))?;
1374 Ok(delegations)
1375 })
1376 }
1377
1378 pub fn save_moderation_records(&self, records: &[wot::ModerationRecord]) -> Result<()> {
1384 self.with_connection(|connection| {
1385 connection.execute("DELETE FROM moderation_records_v2", [])
1386 .map_err(|error| database_error("failed to clear moderation records", error))?;
1387 for r in records {
1388 let scope_json = serde_json::to_string(&r.scope)
1389 .map_err(|error| database_error("failed to serialize scope", error))?;
1390 connection.execute(
1391 "INSERT INTO moderation_records_v2(content_hash, moderator, action, scope_json, sequence, created_at, reason, signature)
1392 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
1393 params![
1394 r.content.as_bytes().to_vec(), r.moderator,
1395 format!("{:?}", r.action), scope_json,
1396 i64::try_from(r.sequence).unwrap_or(i64::MAX),
1397 i64::try_from(r.created_at).unwrap_or(i64::MAX),
1398 r.reason,
1399 r.signature,
1400 ],
1401 ).map_err(|error| database_error("failed to save moderation record", error))?;
1402 }
1403 Ok(())
1404 })
1405 }
1406
1407 pub fn load_moderation_records(&self) -> Result<Vec<wot::ModerationRecord>> {
1413 self.with_connection(|connection| {
1414 let mut stmt = connection
1415 .prepare(
1416 "SELECT content_hash, moderator, action, scope_json, sequence, created_at, reason, signature
1417 FROM moderation_records_v2 ORDER BY created_at",
1418 )
1419 .map_err(|error| database_error("failed to prepare moderation query", error))?;
1420 let records = stmt
1421 .query_map([], |row| {
1422 let hash_bytes: Vec<u8> = row.get(0)?;
1423 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1424 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1425 "invalid hash length",
1426 format!("expected 32 bytes, got {}", error.len()),
1427 )))
1428 })?;
1429 let scope_json: String = row.get(3)?;
1430 let scope: wot::ModerationScope = serde_json::from_str(&scope_json)
1431 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
1432 let action_str: String = row.get(2)?;
1433 let action = match action_str.as_str() {
1434 "Show" => wot::ModerationAction::Show,
1435 "Hide" => wot::ModerationAction::Hide,
1436 "Warn" => wot::ModerationAction::Warn,
1437 "Quarantine" => wot::ModerationAction::Quarantine,
1438 "Restore" => wot::ModerationAction::Restore,
1439 _ => {
1440 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1441 SyncwebError::operation("unknown moderation action", action_str),
1442 )));
1443 }
1444 };
1445 Ok(wot::ModerationRecord {
1446 content: Hash::from(arr),
1447 moderator: row.get::<_, String>(1)?,
1448 action,
1449 scope,
1450 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1451 created_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1452 reason: row.get::<_, String>(6)?,
1453 signature: row.get(7)?,
1454 })
1455 })
1456 .map_err(|error| database_error("failed to query moderation records", error))?
1457 .collect::<std::result::Result<Vec<_>, _>>()
1458 .map_err(|error| database_error("failed to read moderation rows", error))?;
1459 Ok(records)
1460 })
1461 }
1462
1463 pub fn save_attestations(&self, attestations: &[wot::Attestation]) -> Result<()> {
1469 self.with_connection(|connection| {
1470 connection.execute("DELETE FROM attestations_v2", [])
1471 .map_err(|error| database_error("failed to clear attestations", error))?;
1472 for a in attestations {
1473 let (kind, kind_other) = match &a.kind {
1474 wot::AttestationKind::License => ("license", None),
1475 wot::AttestationKind::Provenance => ("provenance", None),
1476 wot::AttestationKind::Derivative => ("derivative", None),
1477 wot::AttestationKind::Other(v) => ("other", Some(v.as_str())),
1478 };
1479 connection.execute(
1480 "INSERT INTO attestations_v2(content_hash, issuer, kind, kind_other, value, sequence, issued_at, signature)
1481 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
1482 params![
1483 a.content.as_bytes().to_vec(), a.issuer,
1484 kind, kind_other, a.value,
1485 i64::try_from(a.sequence).unwrap_or(i64::MAX),
1486 i64::try_from(a.issued_at).unwrap_or(i64::MAX),
1487 a.signature,
1488 ],
1489 ).map_err(|error| database_error("failed to save attestation", error))?;
1490 }
1491 Ok(())
1492 })
1493 }
1494
1495 pub fn load_attestations(&self) -> Result<Vec<wot::Attestation>> {
1501 self.with_connection(|connection| {
1502 let mut stmt = connection
1503 .prepare(
1504 "SELECT content_hash, issuer, kind, kind_other, value, sequence, issued_at, signature
1505 FROM attestations_v2 ORDER BY issued_at",
1506 )
1507 .map_err(|error| database_error("failed to prepare attestations query", error))?;
1508 let attestations = stmt
1509 .query_map([], |row| {
1510 let hash_bytes: Vec<u8> = row.get(0)?;
1511 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1512 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1513 "invalid hash length",
1514 format!("expected 32 bytes, got {}", error.len()),
1515 )))
1516 })?;
1517 let kind_str: String = row.get(2)?;
1518 let kind_other: Option<String> = row.get(3)?;
1519 let kind = match kind_str.as_str() {
1520 "license" => wot::AttestationKind::License,
1521 "provenance" => wot::AttestationKind::Provenance,
1522 "derivative" => wot::AttestationKind::Derivative,
1523 "other" => wot::AttestationKind::Other(kind_other.unwrap_or_default()),
1524 _ => {
1525 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1526 SyncwebError::operation("unknown attestation kind", kind_str),
1527 )));
1528 }
1529 };
1530 Ok(wot::Attestation {
1531 content: Hash::from(arr),
1532 issuer: row.get::<_, String>(1)?,
1533 kind,
1534 value: row.get::<_, String>(4)?,
1535 sequence: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1536 issued_at: u64::try_from(row.get::<_, i64>(6)?).unwrap_or_default(),
1537 signature: row.get(7)?,
1538 })
1539 })
1540 .map_err(|error| database_error("failed to query attestations", error))?
1541 .collect::<std::result::Result<Vec<_>, _>>()
1542 .map_err(|error| database_error("failed to read attestation rows", error))?;
1543 Ok(attestations)
1544 })
1545 }
1546
1547 pub fn save_content_reports(&self, reports: &[ReportRecord]) -> Result<()> {
1553 self.with_connection(|connection| {
1554 connection
1555 .execute("DELETE FROM content_reports_v2", [])
1556 .map_err(|error| database_error("failed to clear content reports", error))?;
1557 for r in reports {
1558 let reporter = r.reporter.as_deref().unwrap_or("cli");
1559 connection
1560 .execute(
1561 "INSERT INTO content_reports_v2(content_hash, reporter, reason, scope, created_at, signature)
1562 VALUES (?1, ?2, ?3, 'global', ?4, ?5)",
1563 params![
1564 r.content.as_bytes().to_vec(),
1565 reporter,
1566 r.reason,
1567 i64::try_from(r.created_at).unwrap_or(i64::MAX),
1568 r.signature,
1569 ],
1570 )
1571 .map_err(|error| database_error("failed to save content report", error))?;
1572 }
1573 Ok(())
1574 })
1575 }
1576
1577 pub fn load_content_reports(&self) -> Result<Vec<ReportRecord>> {
1583 self.with_connection(|connection| {
1584 let mut stmt = connection
1585 .prepare(
1586 "SELECT content_hash, reason, created_at, reporter, signature
1587 FROM content_reports_v2 ORDER BY created_at",
1588 )
1589 .map_err(|error| database_error("failed to prepare reports query", error))?;
1590 let reports = stmt
1591 .query_map([], |row| {
1592 let hash_bytes: Vec<u8> = row.get(0)?;
1593 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1594 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1595 "invalid hash length",
1596 format!("expected 32 bytes, got {}", error.len()),
1597 )))
1598 })?;
1599 Ok(ReportRecord {
1600 content: Hash::from(arr),
1601 reason: row.get::<_, String>(1)?,
1602 created_at: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1603 reporter: row.get::<_, Option<String>>(3)?,
1604 signature: row.get::<_, Option<String>>(4)?,
1605 })
1606 })
1607 .map_err(|error| database_error("failed to query content reports", error))?
1608 .collect::<std::result::Result<Vec<_>, _>>()
1609 .map_err(|error| database_error("failed to read report rows", error))?;
1610 Ok(reports)
1611 })
1612 }
1613
1614 pub fn save_provider_bans(&self, bans: &[resilience::BanRecord]) -> Result<()> {
1620 self.with_connection(|connection| {
1621 connection
1622 .execute("DELETE FROM provider_bans_v2", [])
1623 .map_err(|error| database_error("failed to clear provider bans", error))?;
1624 for b in bans {
1625 connection
1626 .execute(
1627 "INSERT INTO provider_bans_v2(provider, content_hash, banned_at, expires_at, reason, source)
1628 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1629 params![
1630 b.provider.to_string(),
1631 b.hash.map(|h| h.as_bytes().to_vec()),
1632 i64::try_from(b.banned_at).unwrap_or(i64::MAX),
1633 b.expires_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1634 b.reason,
1635 format!("{:?}", b.source),
1636 ],
1637 )
1638 .map_err(|error| database_error("failed to save provider ban", error))?;
1639 }
1640 Ok(())
1641 })
1642 }
1643
1644 pub fn load_provider_bans(&self) -> Result<Vec<resilience::BanRecord>> {
1650 self.with_connection(|connection| {
1651 let mut stmt = connection
1652 .prepare(
1653 "SELECT provider, content_hash, banned_at, expires_at, reason, source
1654 FROM provider_bans_v2 ORDER BY banned_at",
1655 )
1656 .map_err(|error| database_error("failed to prepare bans query", error))?;
1657 let bans = stmt
1658 .query_map([], |row| {
1659 let provider_str: String = row.get(0)?;
1660 let content_hash: Option<Vec<u8>> = row.get(1)?;
1661 let source_str: String = row.get(5)?;
1662 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1663 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1664 "invalid provider",
1665 error,
1666 )))
1667 })?;
1668 let hash = content_hash.and_then(|b| {
1669 let arr: [u8; 32] = b.try_into().ok()?;
1670 Some(Hash::from(arr))
1671 });
1672 let source = match source_str.as_str() {
1673 "Automated" => resilience::BanSource::Automated,
1674 "WoT" => resilience::BanSource::WoT,
1675 _ => resilience::BanSource::Manual,
1676 };
1677 Ok(resilience::BanRecord {
1678 provider,
1679 hash,
1680 banned_at: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1681 expires_at: row
1682 .get::<_, Option<i64>>(3)?
1683 .map(|v| u64::try_from(v).unwrap_or_default()),
1684 reason: row.get::<_, String>(4)?,
1685 source,
1686 })
1687 })
1688 .map_err(|error| database_error("failed to query provider bans", error))?
1689 .collect::<std::result::Result<Vec<_>, _>>()
1690 .map_err(|error| database_error("failed to read ban rows", error))?;
1691 Ok(bans)
1692 })
1693 }
1694
1695 pub fn insert_provider_lease(&self, lease: &resilience::ProviderLease) -> Result<()> {
1701 self.with_connection(|connection| {
1702 connection.execute(
1703 "INSERT OR REPLACE INTO provider_leases(provider, content_hash, ticket, sequence, issued_at, expires_at, signature)
1704 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1705 params![
1706 lease.provider.to_string(),
1707 lease.hash.as_bytes().to_vec(),
1708 lease.ticket,
1709 i64::try_from(lease.sequence).unwrap_or(i64::MAX),
1710 i64::try_from(lease.issued_at).unwrap_or(i64::MAX),
1711 i64::try_from(lease.expires_at).unwrap_or(i64::MAX),
1712 lease.signature,
1713 ],
1714 )
1715 .map_err(|error| database_error("failed to insert provider lease", error))?;
1716 Ok(())
1717 })
1718 }
1719
1720 pub fn load_active_leases(&self, now: u64) -> Result<Vec<resilience::ProviderLease>> {
1726 self.with_connection(|connection| {
1727 let mut stmt = connection
1728 .prepare(
1729 "SELECT provider, content_hash, ticket, sequence, issued_at, expires_at, signature
1730 FROM provider_leases WHERE expires_at > ?1 ORDER BY issued_at",
1731 )
1732 .map_err(|error| database_error("failed to prepare active leases query", error))?;
1733 let leases = stmt
1734 .query_map(params![i64::try_from(now).unwrap_or(i64::MAX)], |row| {
1735 let provider_str: String = row.get(0)?;
1736 let hash_bytes: Vec<u8> = row.get(1)?;
1737 let arr: [u8; 32] = hash_bytes.try_into().map_err(|error: Vec<u8>| {
1738 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1739 "invalid hash length",
1740 format!("expected 32 bytes, got {}", error.len()),
1741 )))
1742 })?;
1743 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1744 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1745 "invalid provider",
1746 error,
1747 )))
1748 })?;
1749 Ok(resilience::ProviderLease {
1750 hash: Hash::from(arr),
1751 provider,
1752 ticket: row.get(2)?,
1753 sequence: u64::try_from(row.get::<_, i64>(3)?).unwrap_or_default(),
1754 issued_at: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1755 expires_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
1756 signature: row.get(6)?,
1757 })
1758 })
1759 .map_err(|error| database_error("failed to query active leases", error))?
1760 .collect::<std::result::Result<Vec<_>, _>>()
1761 .map_err(|error| database_error("failed to read lease rows", error))?;
1762 Ok(leases)
1763 })
1764 }
1765
1766 pub fn insert_provider_ban(&self, ban: &resilience::BanRecord) -> Result<()> {
1772 self.with_connection(|connection| {
1773 connection
1774 .execute(
1775 "INSERT OR REPLACE INTO provider_bans_v2(provider, content_hash, banned_at, expires_at, reason, source)
1776 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1777 params![
1778 ban.provider.to_string(),
1779 ban.hash.map(|h| h.as_bytes().to_vec()),
1780 i64::try_from(ban.banned_at).unwrap_or(i64::MAX),
1781 ban.expires_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1782 ban.reason,
1783 format!("{:?}", ban.source),
1784 ],
1785 )
1786 .map_err(|error| database_error("failed to insert provider ban", error))?;
1787 Ok(())
1788 })
1789 }
1790
1791 pub fn load_active_bans(&self, now: u64) -> Result<Vec<resilience::BanRecord>> {
1797 self.with_connection(|connection| {
1798 let mut stmt = connection
1799 .prepare(
1800 "SELECT provider, content_hash, banned_at, expires_at, reason, source
1801 FROM provider_bans_v2 WHERE expires_at IS NULL OR expires_at > ?1 ORDER BY banned_at",
1802 )
1803 .map_err(|error| database_error("failed to prepare active bans query", error))?;
1804 let bans = stmt
1805 .query_map(params![i64::try_from(now).unwrap_or(i64::MAX)], |row| {
1806 let provider_str: String = row.get(0)?;
1807 let content_hash: Option<Vec<u8>> = row.get(1)?;
1808 let source_str: String = row.get(5)?;
1809 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1810 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1811 "invalid provider",
1812 error,
1813 )))
1814 })?;
1815 let hash = content_hash.and_then(|b| {
1816 let arr: [u8; 32] = b.try_into().ok()?;
1817 Some(Hash::from(arr))
1818 });
1819 let source = match source_str.as_str() {
1820 "Automated" => resilience::BanSource::Automated,
1821 "WoT" => resilience::BanSource::WoT,
1822 _ => resilience::BanSource::Manual,
1823 };
1824 Ok(resilience::BanRecord {
1825 provider,
1826 hash,
1827 banned_at: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1828 expires_at: row
1829 .get::<_, Option<i64>>(3)?
1830 .map(|v| u64::try_from(v).unwrap_or_default()),
1831 reason: row.get::<_, String>(4)?,
1832 source,
1833 })
1834 })
1835 .map_err(|error| database_error("failed to query active provider bans", error))?
1836 .collect::<std::result::Result<Vec<_>, _>>()
1837 .map_err(|error| database_error("failed to read ban rows", error))?;
1838 Ok(bans)
1839 })
1840 }
1841
1842 pub fn load_all_reputations(&self) -> Result<HashMap<PublicKey, reputation::ProviderReputation>> {
1848 self.with_connection(|connection| {
1849 let mut stmt = connection
1850 .prepare(
1851 "SELECT provider, total_fetches, successful_fetches, failed_fetches,
1852 consecutive_failures, last_success_at, last_failure_at
1853 FROM provider_reputation",
1854 )
1855 .map_err(|error| database_error("failed to prepare reputations query", error))?;
1856 let rows = stmt
1857 .query_map([], |row| {
1858 let provider_str: String = row.get(0)?;
1859 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1860 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1861 "invalid provider",
1862 error,
1863 )))
1864 })?;
1865 let rep = reputation::ProviderReputation {
1866 provider,
1867 total_fetches: u64::try_from(row.get::<_, i64>(1)?).unwrap_or_default(),
1868 successful_fetches: u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1869 failed_fetches: u64::try_from(row.get::<_, i64>(3)?).unwrap_or_default(),
1870 consecutive_failures: u32::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
1871 last_success_at: row
1872 .get::<_, Option<i64>>(5)?
1873 .map(|v| u64::try_from(v).unwrap_or_default()),
1874 last_failure_at: row
1875 .get::<_, Option<i64>>(6)?
1876 .map(|v| u64::try_from(v).unwrap_or_default()),
1877 };
1878 Ok((provider, rep))
1879 })
1880 .map_err(|error| database_error("failed to query reputations", error))?
1881 .collect::<std::result::Result<HashMap<_, _>, _>>()
1882 .map_err(|error| database_error("failed to read reputation rows", error))?;
1883 Ok(rows)
1884 })
1885 }
1886
1887 pub fn load_signal_sequences(&self) -> Result<HashMap<(PublicKey, PublicKey), u64>> {
1893 self.with_connection(|connection| {
1894 let mut stmt = connection
1895 .prepare("SELECT reporter, provider, last_sequence FROM provider_signal_sequences")
1896 .map_err(|error| database_error("failed to prepare signal sequences query", error))?;
1897 let rows = stmt
1898 .query_map([], |row| {
1899 let reporter_str: String = row.get(0)?;
1900 let provider_str: String = row.get(1)?;
1901 let reporter = reporter_str.parse::<PublicKey>().map_err(|error| {
1902 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1903 "invalid reporter",
1904 error,
1905 )))
1906 })?;
1907 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1908 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1909 "invalid provider",
1910 error,
1911 )))
1912 })?;
1913 Ok((
1914 (reporter, provider),
1915 u64::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
1916 ))
1917 })
1918 .map_err(|error| database_error("failed to query signal sequences", error))?
1919 .collect::<std::result::Result<HashMap<_, _>, _>>()
1920 .map_err(|error| database_error("failed to read signal sequence rows", error))?;
1921 Ok(rows)
1922 })
1923 }
1924
1925 pub fn upsert_reputation(
1931 &self,
1932 provider: &PublicKey,
1933 rep: &reputation::ProviderReputation,
1934 auto_ban_until: Option<u64>,
1935 auto_ban_count: u32,
1936 ) -> Result<()> {
1937 self.with_connection(|connection| {
1938 connection
1939 .execute(
1940 "INSERT OR REPLACE INTO provider_reputation
1941 (provider, total_fetches, successful_fetches, failed_fetches,
1942 consecutive_failures, last_success_at, last_failure_at,
1943 auto_ban_until, auto_ban_count)
1944 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
1945 params![
1946 provider.to_string(),
1947 i64::try_from(rep.total_fetches).unwrap_or(i64::MAX),
1948 i64::try_from(rep.successful_fetches).unwrap_or(i64::MAX),
1949 i64::try_from(rep.failed_fetches).unwrap_or(i64::MAX),
1950 i64::from(rep.consecutive_failures),
1951 rep.last_success_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1952 rep.last_failure_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1953 auto_ban_until.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
1954 i64::from(auto_ban_count),
1955 ],
1956 )
1957 .map_err(|error| database_error("failed to upsert reputation", error))?;
1958 Ok(())
1959 })
1960 }
1961
1962 pub fn delete_stale_reputations(&self, now: u64, ttl_secs: u64) -> Result<()> {
1968 self.with_connection(|connection| {
1969 let cutoff = i64::try_from(now.saturating_sub(ttl_secs)).unwrap_or(i64::MIN);
1970 connection
1971 .execute(
1972 "DELETE FROM provider_reputation
1973 WHERE (last_success_at IS NULL OR last_success_at < ?1)
1974 AND (last_failure_at IS NULL OR last_failure_at < ?1)",
1975 params![cutoff],
1976 )
1977 .map_err(|error| database_error("failed to delete stale reputations", error))?;
1978 Ok(())
1979 })
1980 }
1981
1982 pub fn load_auto_bans(&self) -> Result<HashMap<PublicKey, (Option<u64>, u32)>> {
1990 self.with_connection(|connection| {
1991 let mut stmt = connection
1992 .prepare("SELECT provider, auto_ban_until, auto_ban_count FROM provider_reputation")
1993 .map_err(|error| database_error("failed to prepare auto bans query", error))?;
1994 let rows = stmt
1995 .query_map([], |row| {
1996 let provider_str: String = row.get(0)?;
1997 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
1998 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
1999 "invalid provider",
2000 error,
2001 )))
2002 })?;
2003 Ok((
2004 provider,
2005 (
2006 row.get::<_, Option<i64>>(1)?
2007 .map(|v| u64::try_from(v).unwrap_or_default()),
2008 u32::try_from(row.get::<_, i64>(2)?).unwrap_or_default(),
2009 ),
2010 ))
2011 })
2012 .map_err(|error| database_error("failed to query auto bans", error))?
2013 .collect::<std::result::Result<HashMap<_, _>, _>>()
2014 .map_err(|error| database_error("failed to read auto ban rows", error))?;
2015 Ok(rows)
2016 })
2017 }
2018
2019 pub fn upsert_signal_sequence(&self, reporter: &PublicKey, provider: &PublicKey, sequence: u64) -> Result<()> {
2025 self.with_connection(|connection| {
2026 connection
2027 .execute(
2028 "INSERT OR REPLACE INTO provider_signal_sequences(reporter, provider, last_sequence)
2029 VALUES (?1, ?2, ?3)",
2030 params![
2031 reporter.to_string(),
2032 provider.to_string(),
2033 i64::try_from(sequence).unwrap_or(i64::MAX),
2034 ],
2035 )
2036 .map_err(|error| database_error("failed to upsert signal sequence", error))?;
2037 Ok(())
2038 })
2039 }
2040
2041 pub fn save_provider_trust_records(&self, records: &[wot::ProviderTrustRecord]) -> Result<()> {
2047 self.with_connection(|connection| {
2048 connection.execute("DELETE FROM provider_trust_records_v2", [])
2049 .map_err(|error| database_error("failed to clear provider trust records", error))?;
2050 for r in records {
2051 let action_str = format!("{:?}", r.action);
2052 connection.execute(
2053 "INSERT INTO provider_trust_records_v2(provider, action, scope, issuer, sequence, issued_at, expires_at, reason, signature)
2054 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
2055 params![
2056 r.provider.to_string(), action_str,
2057 r.scope.map(|h| h.as_bytes().to_vec()),
2058 r.issuer,
2059 i64::try_from(r.sequence).unwrap_or(i64::MAX),
2060 i64::try_from(r.issued_at).unwrap_or(i64::MAX),
2061 r.expires_at.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
2062 r.reason,
2063 r.signature,
2064 ],
2065 ).map_err(|error| database_error("failed to save provider trust record", error))?;
2066 }
2067 Ok(())
2068 })
2069 }
2070
2071 pub fn load_provider_trust_records(&self) -> Result<Vec<wot::ProviderTrustRecord>> {
2077 self.with_connection(|connection| {
2078 let mut stmt = connection
2079 .prepare(
2080 "SELECT provider, action, scope, issuer, sequence, issued_at, expires_at, reason, signature
2081 FROM provider_trust_records_v2 ORDER BY issued_at",
2082 )
2083 .map_err(|error| database_error("failed to prepare trust records query", error))?;
2084 let records = stmt
2085 .query_map([], |row| {
2086 let provider_str: String = row.get(0)?;
2087 let action_str: String = row.get(1)?;
2088 let scope: Option<Vec<u8>> = row.get(2)?;
2089 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
2090 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
2091 "invalid provider",
2092 error,
2093 )))
2094 })?;
2095 let action = match action_str.as_str() {
2096 "Trust" => wot::ProviderTrustAction::Trust,
2097 "Distrust" => wot::ProviderTrustAction::Distrust,
2098 "Vouch" => wot::ProviderTrustAction::Vouch,
2099 "Warn" => wot::ProviderTrustAction::Warn,
2100 _ => {
2101 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
2102 SyncwebError::operation("unknown trust action", action_str),
2103 )));
2104 }
2105 };
2106 let hash = scope.and_then(|b| {
2107 let arr: [u8; 32] = b.try_into().ok()?;
2108 Some(Hash::from(arr))
2109 });
2110 Ok(wot::ProviderTrustRecord {
2111 provider,
2112 action,
2113 scope: hash,
2114 issuer: row.get::<_, String>(3)?,
2115 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
2116 issued_at: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
2117 expires_at: row
2118 .get::<_, Option<i64>>(6)?
2119 .map(|v| u64::try_from(v).unwrap_or_default()),
2120 reason: row.get::<_, String>(7)?,
2121 signature: row.get(8)?,
2122 })
2123 })
2124 .map_err(|error| database_error("failed to query provider trust records", error))?
2125 .collect::<std::result::Result<Vec<_>, _>>()
2126 .map_err(|error| database_error("failed to read trust record rows", error))?;
2127 Ok(records)
2128 })
2129 }
2130
2131 pub fn save_provider_trust_signals(&self, signals: &[reputation::ProviderTrustSignal]) -> Result<()> {
2137 self.with_connection(|connection| {
2138 connection.execute("DELETE FROM provider_trust_signals_v2", [])
2139 .map_err(|error| database_error("failed to clear provider trust signals", error))?;
2140 for s in signals {
2141 let kind_str = format!("{:?}", s.signal);
2142 connection.execute(
2143 "INSERT INTO provider_trust_signals_v2(reporter, provider, signal_kind, content_hash, sequence, timestamp, signature)
2144 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
2145 params![
2146 s.reporter.to_string(), s.provider.to_string(),
2147 kind_str,
2148 s.hash.map(|h| h.as_bytes().to_vec()),
2149 i64::try_from(s.sequence).unwrap_or(i64::MAX),
2150 i64::try_from(s.timestamp).unwrap_or(i64::MAX),
2151 s.signature,
2152 ],
2153 ).map_err(|error| database_error("failed to save trust signal", error))?;
2154 }
2155 Ok(())
2156 })
2157 }
2158
2159 pub fn load_provider_trust_signals(&self) -> Result<Vec<reputation::ProviderTrustSignal>> {
2165 self.with_connection(|connection| {
2166 let mut stmt = connection
2167 .prepare(
2168 "SELECT reporter, provider, signal_kind, content_hash, sequence, timestamp, signature
2169 FROM provider_trust_signals_v2 ORDER BY timestamp",
2170 )
2171 .map_err(|error| database_error("failed to prepare trust signals query", error))?;
2172 let signals = stmt
2173 .query_map([], |row| {
2174 let reporter_str: String = row.get(0)?;
2175 let provider_str: String = row.get(1)?;
2176 let kind_str: String = row.get(2)?;
2177 let content_hash: Option<Vec<u8>> = row.get(3)?;
2178 let reporter = reporter_str.parse::<PublicKey>().map_err(|error| {
2179 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
2180 "invalid reporter",
2181 error,
2182 )))
2183 })?;
2184 let provider = provider_str.parse::<PublicKey>().map_err(|error| {
2185 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
2186 "invalid provider",
2187 error,
2188 )))
2189 })?;
2190 let kind = match kind_str.as_str() {
2191 "ObservedSuccess" => reputation::TrustSignalKind::ObservedSuccess,
2192 "ObservedFailure" => reputation::TrustSignalKind::ObservedFailure,
2193 "ObservedCorruption" => reputation::TrustSignalKind::ObservedCorruption,
2194 _ => {
2195 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
2196 SyncwebError::operation("unknown signal kind", kind_str),
2197 )));
2198 }
2199 };
2200 let hash = content_hash.and_then(|b| {
2201 let arr: [u8; 32] = b.try_into().ok()?;
2202 Some(Hash::from(arr))
2203 });
2204 Ok(reputation::ProviderTrustSignal {
2205 provider,
2206 signal: kind,
2207 hash,
2208 reporter,
2209 timestamp: u64::try_from(row.get::<_, i64>(5)?).unwrap_or_default(),
2210 sequence: u64::try_from(row.get::<_, i64>(4)?).unwrap_or_default(),
2211 signature: row.get(6)?,
2212 })
2213 })
2214 .map_err(|error| database_error("failed to query trust signals", error))?
2215 .collect::<std::result::Result<Vec<_>, _>>()
2216 .map_err(|error| database_error("failed to read trust signal rows", error))?;
2217 Ok(signals)
2218 })
2219 }
2220
2221 pub fn save_trust_streams(&self, streams: &[String]) -> Result<()> {
2227 self.with_connection(|connection| {
2228 connection
2229 .execute("DELETE FROM trust_streams", [])
2230 .map_err(|error| database_error("failed to clear trust streams", error))?;
2231 let now = now_seconds();
2232 for ns in streams {
2233 connection
2234 .execute(
2235 "INSERT INTO trust_streams(namespace, subscribed_at) VALUES (?1, ?2)",
2236 params![ns, now],
2237 )
2238 .map_err(|error| database_error("failed to save trust stream", error))?;
2239 }
2240 Ok(())
2241 })
2242 }
2243
2244 pub fn load_trust_streams(&self) -> Result<Vec<String>> {
2250 self.with_connection(|connection| {
2251 let mut stmt = connection
2252 .prepare("SELECT namespace FROM trust_streams ORDER BY subscribed_at")
2253 .map_err(|error| database_error("failed to prepare trust streams query", error))?;
2254 let streams = stmt
2255 .query_map([], |row| row.get::<_, String>(0))
2256 .map_err(|error| database_error("failed to query trust streams", error))?
2257 .collect::<std::result::Result<Vec<_>, _>>()
2258 .map_err(|error| database_error("failed to read trust stream rows", error))?;
2259 Ok(streams)
2260 })
2261 }
2262}
2263
2264#[derive(Clone)]
2266pub struct IndexingService {
2267 database: IndexingDatabase,
2268 events: broadcast::Sender<IndexingEvent>,
2269 tasks: Arc<Mutex<HashMap<NamespaceId, JoinHandle<()>>>>,
2270 denylist: denylist::DenylistService,
2271}
2272
2273impl std::fmt::Debug for IndexingService {
2274 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2275 formatter
2276 .debug_struct("IndexingService")
2277 .field("database", &self.database)
2278 .finish_non_exhaustive()
2279 }
2280}
2281
2282impl IndexingService {
2283 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
2289 Self::with_database(IndexingDatabase::open(path)?)
2290 }
2291
2292 pub fn in_memory() -> Result<Self> {
2298 Self::with_database(IndexingDatabase::in_memory()?)
2299 }
2300
2301 pub fn with_database(database: IndexingDatabase) -> Result<Self> {
2307 let (events, _) = broadcast::channel(EVENT_CAPACITY);
2308 let denylist = denylist::DenylistService::with_database(database.clone())?;
2309 Ok(Self {
2310 database,
2311 events,
2312 tasks: Arc::new(Mutex::new(HashMap::new())),
2313 denylist,
2314 })
2315 }
2316
2317 #[must_use]
2319 pub fn catalog_service(
2320 &self,
2321 docs: &crate::node::docs_engine::DocsEngine,
2322 blobs: &crate::node::blob_store::BlobStore,
2323 author: iroh_docs::AuthorId,
2324 ) -> CatalogService {
2325 CatalogService::new(self, docs, blobs, author)
2326 }
2327
2328 #[must_use]
2330 pub fn resilience_service(&self, config: resilience::ResilienceConfig) -> resilience::ResilienceService {
2331 resilience::ResilienceService::with_database(self.database.clone(), config)
2332 }
2333
2334 #[must_use]
2336 pub fn resilience_service_with_wot(
2337 &self,
2338 config: resilience::ResilienceConfig,
2339 wot: wot::WotService,
2340 ) -> resilience::ResilienceService {
2341 resilience::ResilienceService::with_database(self.database.clone(), config).with_trust_policy(wot)
2342 }
2343
2344 #[must_use]
2346 pub fn wot_service(&self, policy: wot::TrustPolicy) -> wot::WotService {
2347 wot::WotService::new(self, policy)
2348 }
2349
2350 #[must_use]
2352 pub fn denylist_service(&self) -> denylist::DenylistService {
2353 self.denylist.clone()
2354 }
2355
2356 #[must_use]
2358 pub fn denylist(&self) -> denylist::DenylistService {
2359 self.denylist_service()
2360 }
2361
2362 #[must_use]
2363 pub const fn database(&self) -> &IndexingDatabase {
2364 &self.database
2365 }
2366
2367 #[must_use]
2368 pub const fn db(&self) -> &IndexingDatabase {
2369 &self.database
2370 }
2371
2372 pub fn search_local(&self, query: &str, limit: usize) -> Result<Vec<IndexedEntry>> {
2378 self.database.search(query, limit)
2379 }
2380
2381 pub fn search_global(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
2387 self.database.search_catalogs(query, limit)
2388 }
2389
2390 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
2398 self.search_global(query, limit)
2399 }
2400
2401 #[must_use]
2403 pub fn subscribe(&self) -> broadcast::Receiver<IndexingEvent> {
2404 self.events.subscribe()
2405 }
2406
2407 #[must_use]
2409 pub fn subscribe_events(&self) -> broadcast::Receiver<IndexingEvent> {
2410 self.subscribe()
2411 }
2412
2413 pub async fn enable_folder(&self, folder: &SyncwebFolder) -> Result<IndexingHandle> {
2421 let namespace_id = folder.namespace_id();
2422 if self
2423 .tasks
2424 .lock()
2425 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2426 .get(&namespace_id)
2427 .is_some_and(|task| !task.is_finished())
2428 {
2429 return Ok(IndexingHandle { namespace_id });
2430 }
2431
2432 let live_events = folder.docs_engine().watch(folder.doc()).await?;
2433 self.database.enable_folder(namespace_id, &namespace_id.to_string())?;
2434 send_event(&self.events, IndexingEvent::FolderEnabled { namespace_id });
2435
2436 for entry in folder.docs_engine().list_latest(folder.doc()).await? {
2437 let indexed = self.index_entry(namespace_id, &entry)?;
2438 send_event(&self.events, IndexingEvent::EntryIndexed(indexed));
2439 }
2440
2441 let database = self.database.clone();
2442 let events = self.events.clone();
2443 let task = tokio::spawn(async move {
2444 consume_folder_events(namespace_id, live_events, database, events).await;
2445 });
2446 self.tasks
2447 .lock()
2448 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2449 .insert(namespace_id, task);
2450 Ok(IndexingHandle { namespace_id })
2451 }
2452
2453 pub async fn disable_folder(&self, namespace_id: NamespaceId) -> Result<()> {
2459 let task_handle = self
2460 .tasks
2461 .lock()
2462 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2463 .remove(&namespace_id);
2464 if let Some(task) = task_handle {
2465 task.abort();
2466 }
2467 let database = self.database.clone();
2468 tokio::task::spawn_blocking(move || database.disable_folder(namespace_id))
2469 .await
2470 .map_err(|error| SyncwebError::operation("indexing folder removal task failed", error))??;
2471 send_event(&self.events, IndexingEvent::FolderDisabled { namespace_id });
2472 Ok(())
2473 }
2474
2475 pub fn is_folder_enabled(&self, namespace_id: NamespaceId) -> Result<bool> {
2481 let active_task = self
2482 .tasks
2483 .lock()
2484 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
2485 .get(&namespace_id)
2486 .is_some_and(|task| !task.is_finished());
2487 Ok(active_task || self.database.is_folder_enabled(namespace_id)?)
2488 }
2489
2490 pub fn index_entry(&self, namespace_id: NamespaceId, entry: &Entry) -> Result<IndexedEntry> {
2500 if !self.database.is_folder_enabled(namespace_id)? {
2501 return Err(SyncwebError::FolderNotFound(namespace_id.to_string()));
2502 }
2503 self.database
2504 .upsert_entry(namespace_id, entry.key(), entry.content_hash(), entry.content_len())
2505 }
2506}
2507
2508async fn consume_folder_events(
2509 namespace_id: NamespaceId,
2510 mut live_events: impl n0_future::Stream<Item = Result<LiveEvent>> + Send + Unpin + 'static,
2511 database: IndexingDatabase,
2512 events: broadcast::Sender<IndexingEvent>,
2513) {
2514 while let Some(event_result) = live_events.next().await {
2515 let event = match event_result {
2516 Ok(event) => event,
2517 Err(error) => {
2518 send_event(
2519 &events,
2520 IndexingEvent::Error {
2521 namespace_id,
2522 message: error.to_string(),
2523 },
2524 );
2525 break;
2526 }
2527 };
2528 match event {
2529 LiveEvent::InsertLocal { entry } | LiveEvent::InsertRemote { entry, .. } => {
2530 match database.upsert_entry(namespace_id, entry.key(), entry.content_hash(), entry.content_len()) {
2531 Ok(indexed) => send_event(&events, IndexingEvent::EntryIndexed(indexed)),
2532 Err(error) => {
2533 send_event(
2534 &events,
2535 IndexingEvent::Error {
2536 namespace_id,
2537 message: error.to_string(),
2538 },
2539 );
2540 }
2541 }
2542 }
2543 LiveEvent::ContentReady { hash } => send_event(
2544 &events,
2545 IndexingEvent::Core {
2546 namespace_id,
2547 event: CoreIndexingEvent::ContentReady { hash },
2548 },
2549 ),
2550 LiveEvent::PendingContentReady => send_event(
2551 &events,
2552 IndexingEvent::Core {
2553 namespace_id,
2554 event: CoreIndexingEvent::PendingContentReady,
2555 },
2556 ),
2557 LiveEvent::NeighborUp(_) => send_event(
2558 &events,
2559 IndexingEvent::Core {
2560 namespace_id,
2561 event: CoreIndexingEvent::NeighborUp,
2562 },
2563 ),
2564 LiveEvent::NeighborDown(_) => send_event(
2565 &events,
2566 IndexingEvent::Core {
2567 namespace_id,
2568 event: CoreIndexingEvent::NeighborDown,
2569 },
2570 ),
2571 LiveEvent::SyncFinished(_) => send_event(
2572 &events,
2573 IndexingEvent::Core {
2574 namespace_id,
2575 event: CoreIndexingEvent::SyncFinished,
2576 },
2577 ),
2578 }
2579 }
2580}
2581
2582fn indexed_entry_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<IndexedEntry> {
2583 let namespace = row.get::<_, String>(0)?;
2584 let namespace_id = NamespaceId::from_str(&namespace).map_err(|error| {
2585 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
2586 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(conversion_error))
2587 })?;
2588 let key = row.get(1)?;
2589 let hash_bytes = row.get::<_, Vec<u8>>(2)?;
2590 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
2591 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(2, rusqlite::types::Type::Blob, Box::new(error)))?;
2592 let size_value = row.get::<_, i64>(3)?;
2593 let size = u64::try_from(size_value).map_err(|error| {
2594 rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Integer, Box::new(error))
2595 })?;
2596 Ok(IndexedEntry {
2597 namespace_id,
2598 key,
2599 hash: Hash::from_bytes(hash_array),
2600 size,
2601 })
2602}
2603
2604fn catalog_record_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<CatalogRecord> {
2605 let catalog_namespace = row.get::<_, String>(0)?;
2606 let catalog_namespace_id = NamespaceId::from_str(&catalog_namespace).map_err(|error| {
2607 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
2608 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(conversion_error))
2609 })?;
2610 let folder_namespace = row.get::<_, String>(1)?;
2611 let folder_namespace_id = NamespaceId::from_str(&folder_namespace).map_err(|error| {
2612 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
2613 rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(conversion_error))
2614 })?;
2615 let hash_bytes = row.get::<_, Vec<u8>>(3)?;
2616 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
2617 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Blob, Box::new(error)))?;
2618 let size_value = row.get::<_, i64>(4)?;
2619 let size = u64::try_from(size_value).map_err(|error| {
2620 rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Integer, Box::new(error))
2621 })?;
2622 let tags_json = row.get::<_, String>(7)?;
2623 let tags = serde_json::from_str(&tags_json)
2624 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(error)))?;
2625 Ok(CatalogRecord {
2626 catalog_namespace_id,
2627 folder_namespace_id,
2628 key: row.get(2)?,
2629 hash: Hash::from_bytes(hash_array),
2630 size,
2631 folder_name: row.get(5)?,
2632 title: row.get(6)?,
2633 tags,
2634 publisher: row.get(8)?,
2635 })
2636}
2637
2638fn wot_metadata_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<wot::MetadataEntry> {
2639 let hash_bytes = row.get::<_, Vec<u8>>(0)?;
2640 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
2641 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Blob, Box::new(error)))?;
2642 let sequence_value = row.get::<_, i64>(4)?;
2643 let sequence = u64::try_from(sequence_value).map_err(|error| {
2644 rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Integer, Box::new(error))
2645 })?;
2646 let created_at_value = row.get::<_, i64>(5)?;
2647 let created_at = u64::try_from(created_at_value).map_err(|error| {
2648 rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Integer, Box::new(error))
2649 })?;
2650 Ok(wot::MetadataEntry {
2651 content: Hash::from_bytes(hash_array),
2652 key: row.get(1)?,
2653 value: row.get(2)?,
2654 author: row.get(3)?,
2655 sequence,
2656 created_at,
2657 signature: row.get(6)?,
2658 })
2659}
2660
2661const SCHEMA_PART1: &str = "PRAGMA journal_mode = WAL;
2662 CREATE TABLE IF NOT EXISTS index_metadata (
2663 key TEXT PRIMARY KEY NOT NULL,
2664 value TEXT NOT NULL
2665 );
2666 CREATE TABLE IF NOT EXISTS indexed_folders (
2667 namespace_id TEXT PRIMARY KEY NOT NULL,
2668 label TEXT NOT NULL,
2669 enabled_at INTEGER NOT NULL
2670 );
2671 CREATE TABLE IF NOT EXISTS indexed_entries (
2672 id INTEGER PRIMARY KEY,
2673 namespace_id TEXT NOT NULL REFERENCES indexed_folders(namespace_id) ON DELETE CASCADE,
2674 entry_key BLOB NOT NULL,
2675 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2676 content_len INTEGER NOT NULL CHECK(content_len >= 0),
2677 updated_at INTEGER NOT NULL,
2678 UNIQUE(namespace_id, entry_key)
2679 );
2680 CREATE INDEX IF NOT EXISTS indexed_entries_namespace
2681 ON indexed_entries(namespace_id);
2682 CREATE VIRTUAL TABLE IF NOT EXISTS indexed_entries_fts USING fts5(
2683 namespace_id UNINDEXED,
2684 entry_key,
2685 title,
2686 tags,
2687 tokenize = 'unicode61'
2688 );
2689 CREATE TABLE IF NOT EXISTS indexed_catalogs (
2690 namespace_id TEXT PRIMARY KEY NOT NULL,
2691 label TEXT NOT NULL,
2692 subscribed_at INTEGER NOT NULL
2693 );
2694 CREATE TABLE IF NOT EXISTS indexed_catalog_entries (
2695 id INTEGER PRIMARY KEY,
2696 catalog_namespace_id TEXT NOT NULL REFERENCES indexed_catalogs(namespace_id) ON DELETE CASCADE,
2697 folder_namespace_id TEXT NOT NULL,
2698 entry_key BLOB NOT NULL,
2699 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2700 content_len INTEGER NOT NULL CHECK(content_len >= 0),
2701 folder_name TEXT NOT NULL,
2702 title TEXT NOT NULL,
2703 tags TEXT NOT NULL,
2704 publisher TEXT NOT NULL,
2705 updated_at INTEGER NOT NULL,
2706 UNIQUE(catalog_namespace_id, folder_namespace_id, entry_key)
2707 );
2708 CREATE INDEX IF NOT EXISTS indexed_catalog_entries_catalog
2709 ON indexed_catalog_entries(catalog_namespace_id);
2710 CREATE VIRTUAL TABLE IF NOT EXISTS indexed_catalog_entries_fts USING fts5(
2711 catalog_namespace_id UNINDEXED,
2712 folder_namespace_id UNINDEXED,
2713 entry_key,
2714 folder_name,
2715 title,
2716 tags,
2717 publisher,
2718 content_hash,
2719 tokenize = 'unicode61'
2720 );
2721 CREATE TABLE IF NOT EXISTS wot_metadata (
2722 id INTEGER PRIMARY KEY,
2723 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2724 metadata_key TEXT NOT NULL,
2725 metadata_value TEXT NOT NULL,
2726 author TEXT NOT NULL,
2727 sequence INTEGER NOT NULL CHECK(sequence > 0),
2728 created_at INTEGER NOT NULL CHECK(created_at >= 0),
2729 signature TEXT NOT NULL,
2730 UNIQUE(content_hash, metadata_key, author, sequence)
2731 );
2732 CREATE INDEX IF NOT EXISTS wot_metadata_content
2733 ON wot_metadata(content_hash);
2734 CREATE VIRTUAL TABLE IF NOT EXISTS wot_metadata_fts USING fts5(
2735 content_hash UNINDEXED,
2736 metadata_key,
2737 metadata_value,
2738 author,
2739 tokenize = 'unicode61'
2740 );";
2741
2742const SCHEMA_PART2: &str = "CREATE TABLE IF NOT EXISTS stable_links (
2743 link TEXT PRIMARY KEY NOT NULL,
2744 kind TEXT NOT NULL,
2745 publisher TEXT,
2746 alias TEXT,
2747 content_hash BLOB CHECK(content_hash IS NULL OR length(content_hash) = 32),
2748 sequence INTEGER NOT NULL CHECK(sequence >= 0),
2749 version TEXT,
2750 payload BLOB NOT NULL,
2751 updated_at INTEGER NOT NULL
2752 );
2753CREATE TABLE IF NOT EXISTS link_mirrors (
2754link TEXT NOT NULL,
2755provider TEXT NOT NULL,
2756ticket TEXT NOT NULL,
2757priority INTEGER NOT NULL DEFAULT 0,
2758updated_at INTEGER NOT NULL,
2759PRIMARY KEY(link, provider)
2760);
2761CREATE TABLE IF NOT EXISTS revoked_links (
2762 manifest BLOB NOT NULL CHECK(length(manifest) = 32),
2763 capability TEXT NOT NULL,
2764 revoked_at INTEGER NOT NULL,
2765 PRIMARY KEY(manifest, capability)
2766);
2767 CREATE TABLE IF NOT EXISTS denylist_rules (
2768 rule_type TEXT NOT NULL,
2769 rule_value BLOB NOT NULL,
2770 namespace_id TEXT,
2771 updated_at INTEGER NOT NULL,
2772 PRIMARY KEY(rule_type, rule_value, namespace_id)
2773 );
2774 CREATE TABLE IF NOT EXISTS filter_lists (
2775 namespace_id TEXT PRIMARY KEY NOT NULL,
2776 sequence INTEGER NOT NULL CHECK(sequence > 0),
2777 publisher TEXT NOT NULL,
2778 payload BLOB NOT NULL,
2779 updated_at INTEGER NOT NULL
2780 );
2781 CREATE TABLE IF NOT EXISTS moderation_records (
2782 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2783 scope TEXT NOT NULL,
2784 sequence INTEGER NOT NULL CHECK(sequence > 0),
2785 payload BLOB NOT NULL,
2786 updated_at INTEGER NOT NULL,
2787 PRIMARY KEY(content_hash, scope)
2788 );
2789 CREATE TABLE IF NOT EXISTS provider_leases (
2790 provider TEXT NOT NULL,
2791 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2792 ticket TEXT NOT NULL,
2793 sequence INTEGER NOT NULL,
2794 issued_at INTEGER NOT NULL,
2795 expires_at INTEGER NOT NULL,
2796 signature TEXT,
2797 PRIMARY KEY(provider, content_hash)
2798 );
2799 CREATE INDEX IF NOT EXISTS idx_provider_leases_hash ON provider_leases(content_hash);";
2800
2801const SCHEMA_PART3: &str = "CREATE TABLE IF NOT EXISTS provider_reputation (
2802 provider TEXT PRIMARY KEY NOT NULL,
2803 total_fetches INTEGER NOT NULL DEFAULT 0,
2804 successful_fetches INTEGER NOT NULL DEFAULT 0,
2805 failed_fetches INTEGER NOT NULL DEFAULT 0,
2806 consecutive_failures INTEGER NOT NULL DEFAULT 0,
2807 last_success_at INTEGER,
2808 last_failure_at INTEGER,
2809 auto_ban_until INTEGER,
2810 auto_ban_count INTEGER NOT NULL DEFAULT 0
2811 );
2812 CREATE TABLE IF NOT EXISTS provider_signal_sequences (
2813 reporter TEXT NOT NULL,
2814 provider TEXT NOT NULL,
2815 last_sequence INTEGER NOT NULL,
2816 PRIMARY KEY(reporter, provider)
2817 );
2818 CREATE TABLE IF NOT EXISTS trust_delegations (
2819 delegator TEXT NOT NULL,
2820 delegate TEXT NOT NULL,
2821 scope BLOB CHECK(scope IS NULL OR length(scope) = 32),
2822 max_depth INTEGER,
2823 sequence INTEGER NOT NULL,
2824 issued_at INTEGER NOT NULL,
2825 expires_at INTEGER NOT NULL,
2826 revoked_at INTEGER,
2827 signature TEXT,
2828 PRIMARY KEY(delegator, delegate, sequence)
2829 );
2830 CREATE TABLE IF NOT EXISTS moderation_records_v2 (
2831 id INTEGER PRIMARY KEY AUTOINCREMENT,
2832 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2833 moderator TEXT NOT NULL,
2834 action TEXT NOT NULL,
2835 scope_json TEXT NOT NULL,
2836 sequence INTEGER NOT NULL,
2837 created_at INTEGER NOT NULL,
2838 reason TEXT NOT NULL,
2839 signature TEXT
2840 );
2841 CREATE TABLE IF NOT EXISTS attestations_v2 (
2842 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2843 issuer TEXT NOT NULL,
2844 kind TEXT NOT NULL,
2845 kind_other TEXT,
2846 value TEXT NOT NULL,
2847 sequence INTEGER NOT NULL,
2848 issued_at INTEGER NOT NULL,
2849 signature TEXT,
2850 PRIMARY KEY(content_hash, issuer, kind)
2851 );
2852 CREATE TABLE IF NOT EXISTS content_reports_v2 (
2853 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
2854 reporter TEXT NOT NULL,
2855 reason TEXT NOT NULL,
2856 scope TEXT NOT NULL DEFAULT 'global',
2857 created_at INTEGER NOT NULL,
2858 signature TEXT,
2859 PRIMARY KEY(content_hash, reporter, reason)
2860 );
2861 CREATE TABLE IF NOT EXISTS provider_trust_records_v2 (
2862 id INTEGER PRIMARY KEY AUTOINCREMENT,
2863 provider TEXT NOT NULL,
2864 action TEXT NOT NULL,
2865 scope BLOB CHECK(scope IS NULL OR length(scope) = 32),
2866 issuer TEXT NOT NULL,
2867 sequence INTEGER NOT NULL,
2868 issued_at INTEGER NOT NULL,
2869 expires_at INTEGER,
2870 reason TEXT NOT NULL,
2871 signature TEXT
2872 );
2873 CREATE TABLE IF NOT EXISTS provider_bans_v2 (
2874 provider TEXT NOT NULL,
2875 content_hash BLOB CHECK(content_hash IS NULL OR length(content_hash) = 32),
2876 banned_at INTEGER NOT NULL,
2877 expires_at INTEGER,
2878 reason TEXT NOT NULL,
2879 source TEXT NOT NULL,
2880 PRIMARY KEY(provider, content_hash)
2881 );
2882 CREATE TABLE IF NOT EXISTS provider_trust_signals_v2 (
2883 reporter TEXT NOT NULL,
2884 provider TEXT NOT NULL,
2885 signal_kind TEXT NOT NULL,
2886 content_hash BLOB CHECK(content_hash IS NULL OR length(content_hash) = 32),
2887 sequence INTEGER NOT NULL,
2888 timestamp INTEGER NOT NULL,
2889 signature TEXT,
2890 PRIMARY KEY(reporter, provider, sequence)
2891 );
2892 CREATE INDEX IF NOT EXISTS idx_trust_signals_provider_v2 ON provider_trust_signals_v2(provider);
2893 CREATE INDEX IF NOT EXISTS idx_trust_signals_ts_v2 ON provider_trust_signals_v2(timestamp);
2894 CREATE TABLE IF NOT EXISTS trust_streams (
2895 namespace TEXT PRIMARY KEY,
2896 subscribed_at INTEGER NOT NULL
2897 );";
2898
2899fn initialize_connection(connection: &Connection) -> Result<()> {
2900 connection
2901 .busy_timeout(std::time::Duration::from_secs(5))
2902 .map_err(|error| database_error("failed to configure indexing database", error))?;
2903 connection
2904 .pragma_update(None, "foreign_keys", "ON")
2905 .map_err(|error| database_error("failed to enable indexing foreign keys", error))?;
2906 initialize_schema(connection)?;
2907 connection
2908 .execute(
2909 "INSERT INTO index_metadata(key, value)
2910 VALUES ('schema_version', ?1)
2911 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
2912 [SCHEMA_VERSION],
2913 )
2914 .map_err(|error| database_error("failed to persist indexing schema version", error))?;
2915 Ok(())
2916}
2917
2918fn initialize_schema(connection: &Connection) -> Result<()> {
2919 for part in [SCHEMA_PART1, SCHEMA_PART2, SCHEMA_PART3] {
2920 connection
2921 .execute_batch(part)
2922 .map_err(|error| database_error("failed to initialize indexing database schema", error))?;
2923 }
2924 Ok(())
2925}
2926
2927fn now_seconds() -> i64 {
2928 SystemTime::now()
2929 .duration_since(UNIX_EPOCH)
2930 .map_or(0, |duration| i64::try_from(duration.as_secs()).unwrap_or(i64::MAX))
2931}
2932
2933fn database_error(context: &'static str, error: impl std::fmt::Display) -> SyncwebError {
2934 SyncwebError::operation(context, error)
2935}
2936
2937fn send_event(events: &broadcast::Sender<IndexingEvent>, event: IndexingEvent) {
2938 let _ = events.send(event);
2939}
2940
2941pub const REPORT_GOSSIP_TOPIC: &[u8] = b"syncweb/reports/v1";
2943
2944#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2946#[non_exhaustive]
2947pub struct ReportRecord {
2948 pub content: Hash,
2949 pub reason: String,
2950 pub created_at: u64,
2951 pub reporter: Option<String>,
2952 pub signature: Option<String>,
2953}
2954
2955impl ReportRecord {
2956 #[must_use]
2957 pub const fn new(content: Hash, reason: String, created_at: u64) -> Self {
2958 Self {
2959 content,
2960 reason,
2961 created_at,
2962 reporter: None,
2963 signature: None,
2964 }
2965 }
2966
2967 pub fn sign_with(mut self, signing_key: &SigningKey) -> Result<Self> {
2976 let payload = self.signing_payload()?;
2977 let sig = signing_key.sign(&payload);
2978 self.reporter = Some(hex::encode(signing_key.verifying_key().to_bytes()));
2979 self.signature = Some(hex::encode(sig.to_bytes()));
2980 Ok(self)
2981 }
2982
2983 pub fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
2989 let sig_hex = self
2990 .signature
2991 .as_ref()
2992 .ok_or_else(|| SyncwebError::MissingSignature("report has no signature".into()))?;
2993 let sig_bytes = hex::decode(sig_hex).map_err(|e| SyncwebError::operation("report signature hex decode", e))?;
2994 let sig_len = sig_bytes.len();
2995 let sig: [u8; 64] = sig_bytes.try_into().map_err(|_v| {
2996 SyncwebError::InvalidSignature(format!(
2997 "report signature has wrong length (expected 64, got {sig_len})"
2998 ))
2999 })?;
3000 let signature = Signature::from_bytes(&sig);
3001 public_key
3002 .verify(&self.signing_payload()?, &signature)
3003 .map_err(|e| SyncwebError::InvalidSignature(format!("report signature does not match: {e}")))
3004 }
3005
3006 fn signing_payload(&self) -> Result<Vec<u8>> {
3011 let canonical = (self.content.as_bytes(), &self.reason, self.created_at);
3012 serde_json::to_vec(&canonical).map_err(|e| SyncwebError::operation("canonical report serialization", e))
3013 }
3014}
3015
3016impl SignedGossipMessage for ReportRecord {
3017 fn verify_signature(&self) -> Result<()> {
3018 let reporter_hex = self.reporter.as_ref().ok_or_else(|| {
3019 SyncwebError::MissingSignature("report has no reporter; unsigned reports must not be gossiped".into())
3020 })?;
3021 let bytes = hex::decode(reporter_hex).map_err(|e| SyncwebError::operation("decode reporter key", e))?;
3022 let key_len = bytes.len();
3023 let key_bytes: [u8; 32] = bytes.try_into().map_err(|_v| {
3024 SyncwebError::InvalidSignature(format!("reporter key has wrong length (expected 32, got {key_len})"))
3025 })?;
3026 let public_key = VerifyingKey::from_bytes(&key_bytes)
3027 .map_err(|e| SyncwebError::InvalidSignature(format!("invalid reporter key: {e}")))?;
3028 self.verify(&public_key)
3029 }
3030}