tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit db67342c7416e1e4b03bd20ed8f17e0cf02caa44
parent 89c88d56c832052d2cb2d8b44025ce80c7160f1c
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 16:02:08 -0700

query: wire pocket scrape controls

Diffstat:
Mconfig/tangle.example.json | 7++++++-
Mcrates/tangle/tests/version.rs | 7++++++-
Mcrates/tangle_bench/src/lib.rs | 6+++++-
Mcrates/tangle_protocol/src/lib.rs | 10++++++++++
Mcrates/tangle_runtime/src/config.rs | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mcrates/tangle_runtime/src/nip11.rs | 7++++++-
Mcrates/tangle_runtime/src/relay/core.rs | 168++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mcrates/tangle_runtime/src/runtime.rs | 7++++++-
Mcrates/tangle_runtime/src/server.rs | 7++++++-
Mcrates/tangle_runtime/src/session.rs | 7++++++-
Mcrates/tangle_runtime/tests/base_relay_v2.rs | 224++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mcrates/tangle_runtime/tests/ops_truthfulness.rs | 7++++++-
Mcrates/tangle_runtime/tests/phase2_acceptance_targets.rs | 7++++++-
Mcrates/tangle_store_pocket/src/lib.rs | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
14 files changed, 557 insertions(+), 123 deletions(-)

diff --git a/config/tangle.example.json b/config/tangle.example.json @@ -5,7 +5,12 @@ }, "pocket": { "data_directory": "runtime/pocket", - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": true, diff --git a/crates/tangle/tests/version.rs b/crates/tangle/tests/version.rs @@ -102,7 +102,12 @@ fn tangle_run_starts_server_and_stays_alive_until_shutdown() { }, "pocket": { "data_directory": data_dir, - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": true, diff --git a/crates/tangle_bench/src/lib.rs b/crates/tangle_bench/src/lib.rs @@ -15,7 +15,7 @@ use tangle_runtime::relay::{ auth::BaseAuthState, core::{BaseRelay, BaseRelayLimitSettings, BaseRelayLimits}, }; -use tangle_store_pocket::{PocketStoreConfig, PocketSyncPolicy}; +use tangle_store_pocket::{PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy}; use tangle_test_support::{ FixtureKey, TANGLE_V2_RELAY_URL, tangle_v2_auth_event, tangle_v2_event, tangle_v2_group_config, tangle_v2_group_create_event, tangle_v2_group_event, tangle_v2_put_user_event, tangle_v2_tag, @@ -959,6 +959,7 @@ fn run_projection_rebuild_benchmark(dataset: &BenchDataset) -> Result<ScenarioRe &materialized.store_config, relay_limits(128), &group_config()?, + PocketQueryConfig::default(), ) .map_err(|error| error.to_string())?; let elapsed = elapsed_micros(started); @@ -997,6 +998,7 @@ fn run_outbox_replay_benchmark(dataset: &BenchDataset) -> Result<ScenarioReport, &materialized.store_config, relay_limits(128), &group_config()?, + PocketQueryConfig::default(), ) .map_err(|error| error.to_string())?; let after_first = generated_state_counts(&reopened)?; @@ -1005,6 +1007,7 @@ fn run_outbox_replay_benchmark(dataset: &BenchDataset) -> Result<ScenarioReport, &materialized.store_config, relay_limits(128), &group_config()?, + PocketQueryConfig::default(), ) .map_err(|error| error.to_string())?; let after_second = generated_state_counts(&reopened)?; @@ -1109,6 +1112,7 @@ fn materialize_dataset( &store_config, relay_limits(max_pending_events), &group_config()?, + PocketQueryConfig::default(), ) .map_err(|error| error.to_string())?; let owner_auth = authenticated(FixtureKey::Owner)?; diff --git a/crates/tangle_protocol/src/lib.rs b/crates/tangle_protocol/src/lib.rs @@ -692,6 +692,12 @@ impl Filter { filter } + pub fn with_limit(&self, limit: u64) -> Self { + let mut filter = self.clone(); + filter.limit = Some(limit); + filter + } + pub fn search(&self) -> Option<&str> { self.search.as_deref() } @@ -1985,6 +1991,10 @@ mod tests { assert_eq!(without_limit.limit(), None); assert_eq!(without_limit.search(), filter.search()); assert!(without_limit.matches(&event)); + let with_limit = without_limit.with_limit(2); + assert_eq!(with_limit.limit(), Some(2)); + assert_eq!(with_limit.search(), filter.search()); + assert!(with_limit.matches(&event)); assert_eq!( filter_from_value(&filter_to_value(&filter)).expect("encoded"), filter diff --git a/crates/tangle_runtime/src/config.rs b/crates/tangle_runtime/src/config.rs @@ -15,13 +15,16 @@ use serde::Deserialize; use std::{net::SocketAddr, path::PathBuf}; use tangle_groups::GroupRuntimeConfig; use tangle_protocol::SubscriptionId; -use tangle_store_pocket::{PocketStoreConfig, PocketSyncPolicy}; +use tangle_store_pocket::{PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy}; + +const MAX_POCKET_QUERY_SCRAPE_WINDOW_SECONDS: u64 = 86_400; #[derive(Debug, Clone, PartialEq, Eq)] pub struct BaseRelayRuntimeConfig { listen_addr: SocketAddr, relay_url: String, pocket: PocketStoreConfig, + pocket_query: PocketQueryConfig, groups: GroupRuntimeConfig, auth_ttl_seconds: u64, auth_created_at_skew_seconds: u64, @@ -43,6 +46,10 @@ impl BaseRelayRuntimeConfig { &self.pocket } + pub fn pocket_query_config(&self) -> PocketQueryConfig { + self.pocket_query + } + pub fn groups(&self) -> &GroupRuntimeConfig { &self.groups } @@ -68,7 +75,12 @@ impl BaseRelayRuntimeConfig { } pub fn open_relay(&self) -> Result<BaseRelay, BaseRelayError> { - BaseRelay::open_with_groups(&self.pocket, self.limits.base_relay_limits()?, &self.groups) + BaseRelay::open_with_groups( + &self.pocket, + self.limits.base_relay_limits()?, + &self.groups, + self.pocket_query, + ) } pub fn auth_state(&self) -> Result<BaseAuthState, BaseRelayError> { @@ -306,6 +318,15 @@ struct BaseRelayServerConfigDocument { struct BaseRelayPocketConfigDocument { data_directory: String, sync_policy: BaseRelayPocketSyncPolicyDocument, + query: BaseRelayPocketQueryConfigDocument, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct BaseRelayPocketQueryConfigDocument { + allow_scraping: bool, + allow_scrape_if_limited_to: u32, + allow_scrape_if_max_seconds: u64, } #[derive(Debug, Clone, Copy, Deserialize)] @@ -433,9 +454,10 @@ pub fn parse_base_relay_runtime_config_json( .map_err(|error| { BaseRelayError::invalid(format!("server.listen_addr is invalid: {error}")) })?; + let pocket_document = document.pocket; let pocket = PocketStoreConfig::new( - PathBuf::from(document.pocket.data_directory), - match document.pocket.sync_policy { + PathBuf::from(pocket_document.data_directory), + match pocket_document.sync_policy { BaseRelayPocketSyncPolicyDocument::FlushOnWrite => PocketSyncPolicy::FlushOnWrite, BaseRelayPocketSyncPolicyDocument::FlushOnShutdown => PocketSyncPolicy::FlushOnShutdown, }, @@ -447,6 +469,7 @@ pub fn parse_base_relay_runtime_config_json( let groups = tangle_groups::parse_group_runtime_config_json(&groups_raw) .map_err(|error| BaseRelayError::invalid(error.to_string()))?; let limits = BaseRelayRuntimeLimitsConfig::from_document(document.limits)?; + let pocket_query = pocket_query_config_from_document(pocket_document.query, limits)?; let rate_limits = base_relay_rate_limits_from_document(document.rate_limits)?; if document.auth.created_at_skew_seconds == 0 { return Err(BaseRelayError::invalid( @@ -458,6 +481,7 @@ pub fn parse_base_relay_runtime_config_json( listen_addr, relay_url: document.server.relay_url, pocket, + pocket_query, groups, auth_ttl_seconds: document.auth.challenge_ttl_seconds, auth_created_at_skew_seconds: document.auth.created_at_skew_seconds, @@ -467,6 +491,27 @@ pub fn parse_base_relay_runtime_config_json( }) } +fn pocket_query_config_from_document( + document: BaseRelayPocketQueryConfigDocument, + limits: BaseRelayRuntimeLimitsConfig, +) -> Result<PocketQueryConfig, BaseRelayError> { + if u64::from(document.allow_scrape_if_limited_to) > limits.max_limit() { + return Err(BaseRelayError::invalid( + "pocket.query.allow_scrape_if_limited_to must be less than or equal to limits.max_limit", + )); + } + if document.allow_scrape_if_max_seconds > MAX_POCKET_QUERY_SCRAPE_WINDOW_SECONDS { + return Err(BaseRelayError::invalid(format!( + "pocket.query.allow_scrape_if_max_seconds must be less than or equal to {MAX_POCKET_QUERY_SCRAPE_WINDOW_SECONDS}" + ))); + } + Ok(PocketQueryConfig::new( + document.allow_scraping, + document.allow_scrape_if_limited_to, + document.allow_scrape_if_max_seconds, + )) +} + fn require_positive(field: &str, value: usize) -> Result<(), BaseRelayError> { if value == 0 { return Err(BaseRelayError::invalid(format!( @@ -626,6 +671,15 @@ mod tests { config.pocket_config().sync_policy(), PocketSyncPolicy::FlushOnShutdown ); + assert!(!config.pocket_query_config().allow_scraping()); + assert_eq!( + config.pocket_query_config().allow_scrape_if_limited_to(), + 100 + ); + assert_eq!( + config.pocket_query_config().allow_scrape_if_max_seconds(), + 3_600 + ); assert!(config.groups().enabled()); assert!(!config.groups().policy().public_join()); assert!(!config.groups().policy().invites_enabled()); @@ -694,7 +748,12 @@ mod tests { }, "pocket": { "data_directory": "runtime/pocket", - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": false @@ -773,7 +832,12 @@ mod tests { }, "pocket": { "data_directory": "runtime/pocket", - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": false @@ -849,7 +913,12 @@ mod tests { }, "pocket": { "data_directory": "runtime/pocket", - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": false @@ -945,6 +1014,31 @@ mod tests { } #[test] + fn base_relay_runtime_config_validates_pocket_query_controls() { + let raw = include_str!("../../../config/tangle.example.json").replace( + " \"allow_scrape_if_limited_to\": 100,\n", + " \"allow_scrape_if_limited_to\": 501,\n", + ); + assert_eq!( + parse_base_relay_runtime_config_json(&raw) + .expect_err("query scrape limit") + .prefixed_message(), + "invalid: pocket.query.allow_scrape_if_limited_to must be less than or equal to limits.max_limit" + ); + + let raw = include_str!("../../../config/tangle.example.json").replace( + " \"allow_scrape_if_max_seconds\": 3600\n", + " \"allow_scrape_if_max_seconds\": 86401\n", + ); + assert_eq!( + parse_base_relay_runtime_config_json(&raw) + .expect_err("query scrape window") + .prefixed_message(), + "invalid: pocket.query.allow_scrape_if_max_seconds must be less than or equal to 86400" + ); + } + + #[test] fn base_relay_runtime_config_requires_explicit_query_complexity() { let raw = include_str!("../../../config/tangle.example.json") .replace(" \"max_query_complexity\": 2048,\n", ""); diff --git a/crates/tangle_runtime/src/nip11.rs b/crates/tangle_runtime/src/nip11.rs @@ -390,7 +390,12 @@ mod tests { }, "pocket": { "data_directory": "runtime/pocket", - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": groups, "auth": { diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs @@ -16,7 +16,9 @@ use tangle_groups::{ StoreOffset, classify_group_event, validate_client_group_event_structure, }; use tangle_protocol::{ClientMessage, Event, Filter, RelayMessage, SubscriptionId, UnixTimestamp}; -use tangle_store_pocket::{PocketScreenResult, PocketStoreConfig, PocketStoreHandle}; +use tangle_store_pocket::{ + PocketQueryConfig, PocketScreenResult, PocketStoreConfig, PocketStoreHandle, +}; pub struct BaseRelay { store: PocketStoreHandle, @@ -24,6 +26,7 @@ pub struct BaseRelay { groups: Option<GroupService>, readiness: BaseRelayReadinessState, limits: BaseRelayLimits, + query: PocketQueryConfig, } #[derive(Debug, Clone, PartialEq)] @@ -412,28 +415,35 @@ impl BaseRelay { pub fn open( config: &PocketStoreConfig, limits: BaseRelayLimits, + query: PocketQueryConfig, ) -> Result<Self, BaseRelayError> { let store = PocketStoreHandle::open(config).map_err(BaseRelayError::from)?; - Self::new(store, limits) + Self::new(store, limits, query) } pub fn open_with_groups( config: &PocketStoreConfig, limits: BaseRelayLimits, groups: &GroupRuntimeConfig, + query: PocketQueryConfig, ) -> Result<Self, BaseRelayError> { let store = PocketStoreHandle::open(config).map_err(BaseRelayError::from)?; - Self::new_with_groups(store, limits, groups) + Self::new_with_groups(store, limits, groups, query) } - pub fn new(store: PocketStoreHandle, limits: BaseRelayLimits) -> Result<Self, BaseRelayError> { - Self::new_with_groups(store, limits, &GroupRuntimeConfig::disabled()) + pub fn new( + store: PocketStoreHandle, + limits: BaseRelayLimits, + query: PocketQueryConfig, + ) -> Result<Self, BaseRelayError> { + Self::new_with_groups(store, limits, &GroupRuntimeConfig::disabled(), query) } pub fn new_with_groups( store: PocketStoreHandle, limits: BaseRelayLimits, groups: &GroupRuntimeConfig, + query: PocketQueryConfig, ) -> Result<Self, BaseRelayError> { let groups = GroupService::from_config(&store, groups)?; let subscriptions = @@ -445,6 +455,7 @@ impl BaseRelay { groups, readiness, limits, + query, }) } @@ -922,38 +933,36 @@ impl BaseRelay { filter: &Filter, auth: &GroupAuthContext, ) -> Result<BaseRelayEventQueryReport, BaseRelayError> { - let pocket_filter = tangle_filter_to_pocket(filter)?; + let effective_filter = self.filter_with_effective_limit(filter); + let pocket_filter = tangle_filter_to_pocket(&effective_filter)?; let screen_error = RefCell::new(None); - let screened = self.store.find_events_with_screen( - &pocket_filter, - true, - 0, - u64::MAX, - |pocket_event| { - if screen_error.borrow().is_some() { - return PocketScreenResult::Mismatch; - } - match pocket_filter.event_matches(pocket_event) { - Ok(false) => PocketScreenResult::Mismatch, - Ok(true) => match Self::group_read_gate_visible_to_auth( - self.groups.as_ref(), - pocket_event, - auth, - ) { - Ok(true) => PocketScreenResult::Match, - Ok(false) => PocketScreenResult::Redacted, + let screened = + self.store + .find_events_with_screen(&pocket_filter, self.query, |pocket_event| { + if screen_error.borrow().is_some() { + return PocketScreenResult::Mismatch; + } + match pocket_filter.event_matches(pocket_event) { + Ok(false) => PocketScreenResult::Mismatch, + Ok(true) => match Self::group_read_gate_visible_to_auth( + self.groups.as_ref(), + pocket_event, + auth, + ) { + Ok(true) => PocketScreenResult::Match, + Ok(false) => PocketScreenResult::Redacted, + Err(error) => { + *screen_error.borrow_mut() = Some(error); + PocketScreenResult::Mismatch + } + }, Err(error) => { - *screen_error.borrow_mut() = Some(error); + *screen_error.borrow_mut() = + Some(BaseRelayError::error(error.to_string())); PocketScreenResult::Mismatch } - }, - Err(error) => { - *screen_error.borrow_mut() = Some(BaseRelayError::error(error.to_string())); - PocketScreenResult::Mismatch } - } - }, - )?; + })?; if let Some(error) = screen_error.into_inner() { return Err(error); } @@ -966,6 +975,13 @@ impl BaseRelay { Ok(BaseRelayEventQueryReport::new(events, group_read_denied)) } + fn filter_with_effective_limit(&self, filter: &Filter) -> Filter { + match filter.limit() { + Some(_) => filter.clone(), + None => filter.with_limit(self.limits.default_limit()), + } + } + fn sort_and_dedupe_query_events(mut events: Vec<Event>) -> Vec<Event> { events.sort_by(|left, right| { right @@ -1023,7 +1039,7 @@ mod tests { ClientMessage, Event, EventId, Filter, Kind, PublicKeyHex, RelayMessage, SubscriptionId, Tag, UnixTimestamp, UnsignedEvent, filter_from_value, }; - use tangle_store_pocket::{PocketStoreConfig, PocketSyncPolicy}; + use tangle_store_pocket::{PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy}; #[test] fn base_relay_stores_queries_counts_closes_and_fans_out_public_events() { let mut relay = test_relay("base-relay-public", 4); @@ -1075,6 +1091,60 @@ mod tests { } #[test] + fn base_relay_uses_configured_pocket_query_scrape_controls() { + let strict_config = test_store_config("base-relay-query-strict"); + let mut strict = BaseRelay::open( + &strict_config, + relay_limits(4), + PocketQueryConfig::new(false, 0, 0), + ) + .expect("strict"); + let strict_event = signed_public_event(7, 1, Vec::new(), "strict"); + let broad = filter_from_value(&serde_json::json!({"limit":1})).expect("filter"); + + assert_accepted( + strict + .handle_event(strict_event.clone()) + .expect("strict event"), + &strict_event, + ); + assert!( + strict + .handle_req( + SubscriptionId::new("strict").expect("sub"), + vec![broad.clone()] + ) + .expect_err("strict scrape") + .prefixed_message() + .to_lowercase() + .contains("scraper") + ); + + let limited_config = test_store_config("base-relay-query-limited"); + let mut limited = BaseRelay::open( + &limited_config, + relay_limits(4), + PocketQueryConfig::new(false, 1, 0), + ) + .expect("limited"); + let limited_event = signed_public_event(8, 1, Vec::new(), "limited"); + + assert_accepted( + limited + .handle_event(limited_event.clone()) + .expect("limited event"), + &limited_event, + ); + let messages = limited + .handle_req(SubscriptionId::new("limited").expect("sub"), vec![broad]) + .expect("limited scrape"); + + assert!( + matches!(&messages[0], RelayMessage::Event { event, .. } if event.id() == limited_event.id()) + ); + } + + #[test] fn base_relay_fetches_events_by_store_offset() { let relay = test_relay("base-relay-offset-lookup", 4); let event = signed_public_event(7, 1, Vec::new(), "offset"); @@ -1169,6 +1239,7 @@ mod tests { default_limit: 1, }) .expect("limits"), + PocketQueryConfig::default(), ) .expect("relay"); let first = signed_event_at(7, 1, Vec::new(), "one", 1_714_124_430); @@ -1281,6 +1352,7 @@ mod tests { default_limit: 1, }) .expect("limits"), + PocketQueryConfig::default(), ) .expect("relay"); let complex = filter_from_value(&serde_json::json!({ @@ -2230,7 +2302,8 @@ mod tests { #[test] fn base_relay_shutdown_closes_live_subscriptions_and_syncs_store() { let config = test_store_config("base-relay-shutdown"); - let mut relay = BaseRelay::open(&config, relay_limits(4)).expect("relay"); + let mut relay = + BaseRelay::open(&config, relay_limits(4), PocketQueryConfig::default()).expect("relay"); let event = signed_public_event(7, 1, Vec::new(), "shutdown"); let subscription_id = SubscriptionId::new("sub-shutdown").expect("sub"); @@ -2247,7 +2320,8 @@ mod tests { assert_eq!(relay.active_subscription_count(), 0); assert!(relay.fanout(&event).is_empty()); - let reopened = BaseRelay::open(&config, relay_limits(4)).expect("reopened"); + let reopened = BaseRelay::open(&config, relay_limits(4), PocketQueryConfig::default()) + .expect("reopened"); assert_eq!(count_kind(&reopened, 1), 1); } @@ -2296,7 +2370,9 @@ mod tests { #[test] fn base_relay_enforces_event_and_filter_runtime_limits() { let config = test_store_config("base-relay-event-filter-runtime-limits"); - let mut relay = BaseRelay::open(&config, strict_relay_limits()).expect("relay"); + let mut relay = + BaseRelay::open(&config, strict_relay_limits(), PocketQueryConfig::default()) + .expect("relay"); let first = signed_public_event(7, 1, Vec::new(), "a"); let second = signed_event_at(8, 1, Vec::new(), "b", 1_714_124_434); @@ -2374,7 +2450,9 @@ mod tests { #[test] fn base_relay_enforces_subscription_id_and_count_limits() { let config = test_store_config("base-relay-subscription-limits"); - let mut relay = BaseRelay::open(&config, strict_relay_limits()).expect("relay"); + let mut relay = + BaseRelay::open(&config, strict_relay_limits(), PocketQueryConfig::default()) + .expect("relay"); assert!( relay @@ -2412,7 +2490,12 @@ mod tests { fn test_relay(name: &str, max_pending_events: usize) -> BaseRelay { let config = test_store_config(name); - BaseRelay::open(&config, relay_limits(max_pending_events)).expect("relay") + BaseRelay::open( + &config, + relay_limits(max_pending_events), + PocketQueryConfig::default(), + ) + .expect("relay") } fn test_relay_with_groups( @@ -2421,8 +2504,13 @@ mod tests { groups: &tangle_groups::GroupRuntimeConfig, ) -> BaseRelay { let config = test_store_config(name); - BaseRelay::open_with_groups(&config, relay_limits(max_pending_events), groups) - .expect("relay") + BaseRelay::open_with_groups( + &config, + relay_limits(max_pending_events), + groups, + PocketQueryConfig::default(), + ) + .expect("relay") } fn relay_limits(max_pending_events: usize) -> BaseRelayLimits { diff --git a/crates/tangle_runtime/src/runtime.rs b/crates/tangle_runtime/src/runtime.rs @@ -2715,7 +2715,12 @@ mod tests { }, "pocket": { "data_directory": root.join("pocket"), - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": true, diff --git a/crates/tangle_runtime/src/server.rs b/crates/tangle_runtime/src/server.rs @@ -524,7 +524,12 @@ mod tests { }, "pocket": { "data_directory": root.join("pocket"), - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": true, diff --git a/crates/tangle_runtime/src/session.rs b/crates/tangle_runtime/src/session.rs @@ -738,7 +738,12 @@ mod tests { }, "pocket": { "data_directory": root.join("pocket"), - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": false diff --git a/crates/tangle_runtime/tests/base_relay_v2.rs b/crates/tangle_runtime/tests/base_relay_v2.rs @@ -25,8 +25,9 @@ use tangle_runtime::{ }, }; use tangle_store_pocket::{ - PocketStoreConfig, PocketStoreHandle, PocketSyncPolicy, TANGLE_GROUP_CHECKPOINT_TABLE, - TANGLE_GROUP_OUTBOX_TABLE, TANGLE_GROUP_PROJECTION_TABLE, parse_pocket_event_json, + PocketQueryConfig, PocketStoreConfig, PocketStoreHandle, PocketSyncPolicy, + TANGLE_GROUP_CHECKPOINT_TABLE, TANGLE_GROUP_OUTBOX_TABLE, TANGLE_GROUP_PROJECTION_TABLE, + parse_pocket_event_json, }; use tangle_test_support::{ FixtureKey, TANGLE_V2_RELAY_SECRET_HEX, TANGLE_V2_RELAY_URL, tangle_v2_auth_event, @@ -40,7 +41,8 @@ use tangle_test_support::{ #[test] fn public_relay_smoke_stores_queries_counts_and_fans_out() { let config = test_store_config("public-smoke"); - let mut relay = BaseRelay::open(&config, relay_limits(4)).expect("relay"); + let mut relay = + BaseRelay::open(&config, relay_limits(4), PocketQueryConfig::default()).expect("relay"); let first = tangle_v2_event(FixtureKey::Member, 1_714_124_433, 1, Vec::new(), "hello").expect("first"); let query_id = subscription("public-query"); @@ -177,7 +179,13 @@ fn auth_integration_covers_challenge_edges() { fn group_auth_lifecycle_membership_and_flag_flows_pass_in_process() { let config = test_store_config("group-flows"); let groups = group_config_with_public_join(); - let mut relay = BaseRelay::open_with_groups(&config, relay_limits(8), &groups).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &groups, + PocketQueryConfig::default(), + ) + .expect("relay"); let owner_auth = authenticated(FixtureKey::Owner); let admin_auth = authenticated(FixtureKey::Admin); let member_auth = authenticated(FixtureKey::Member); @@ -301,8 +309,13 @@ fn group_auth_lifecycle_membership_and_flag_flows_pass_in_process() { #[test] fn relay_override_role_changes_generate_admin_snapshots() { let config = test_store_config("role-admin-snapshots"); - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); let owner_auth = authenticated(FixtureKey::Owner); let admin_auth = authenticated(FixtureKey::Admin); let member = FixtureKey::Member.public_key().as_str().to_owned(); @@ -374,8 +387,13 @@ fn relay_override_role_changes_generate_admin_snapshots() { #[test] fn group_join_requests_are_denied_by_default() { let config = test_store_config("group-public-join-default"); - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); let owner_auth = authenticated(FixtureKey::Owner); let outsider_auth = authenticated(FixtureKey::Outsider); let create = tangle_v2_group_create_event(FixtureKey::Owner, "Farm", 1, &[]).expect("create"); @@ -408,8 +426,13 @@ fn group_join_requests_are_denied_by_default() { #[test] fn metadata_flags_and_read_privacy_cover_req_count_and_fanout() { let config = test_store_config("privacy-flags"); - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); let owner_auth = authenticated(FixtureKey::Owner); let outsider_auth = authenticated(FixtureKey::Outsider); @@ -573,8 +596,13 @@ fn metadata_flags_and_read_privacy_cover_req_count_and_fanout() { #[test] fn nip29_privacy_leak_suite_covers_relay_exposure_and_rejection_paths() { let config = test_store_config("nip29-leak-suite"); - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(16), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(16), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); let owner_auth = authenticated(FixtureKey::Owner); let admin_auth = authenticated(FixtureKey::Admin); let member_auth = authenticated(FixtureKey::Member); @@ -953,8 +981,13 @@ fn nip29_privacy_leak_suite_covers_relay_exposure_and_rejection_paths() { #[test] fn delete_and_secondary_privacy_surfaces_are_read_gated_or_absent() { let config = test_store_config("delete-privacy"); - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); let owner_auth = authenticated(FixtureKey::Owner); accept_group_create(&mut relay, "DeleteFarm", &[], 1, &owner_auth); @@ -1037,8 +1070,13 @@ fn projection_rebuild_after_restart_matches_live_state_and_outbox_is_idempotent( let config = test_store_config("projection-restart"); let owner_auth = authenticated(FixtureKey::Owner); { - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); accept_group_create(&mut relay, "RestartFarm", &[], 1, &owner_auth); let put = tangle_v2_put_user_event(FixtureKey::Admin, "RestartFarm", FixtureKey::Member, 2) .expect("put"); @@ -1056,8 +1094,13 @@ fn projection_rebuild_after_restart_matches_live_state_and_outbox_is_idempotent( } delete_group_extra_records(&config); - let relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("reopen"); + let relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("reopen"); assert_eq!( relay .readiness_state() @@ -1100,8 +1143,13 @@ fn projection_rebuild_after_restart_matches_live_state_and_outbox_is_idempotent( &GroupCheckpointStatus::Current { .. } )); - let relay = BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()) - .expect("second reopen"); + let relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("second reopen"); assert_eq!(count_kind(&relay, KIND_GROUP_METADATA), 1); assert_eq!(count_kind(&relay, KIND_GROUP_ADMINS), 1); assert_eq!(count_kind(&relay, KIND_GROUP_MEMBERS), 1); @@ -1117,8 +1165,13 @@ fn projection_applies_canonical_events_after_checkpoint_on_restart() { let put = tangle_v2_put_user_event(FixtureKey::Admin, "IncrementalFarm", FixtureKey::Member, 2) .expect("put"); { - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); assert_accepted( relay .handle_event_with_auth(create.clone(), &owner_auth) @@ -1136,8 +1189,13 @@ fn projection_applies_canonical_events_after_checkpoint_on_restart() { let create_offset = stored_event_offset(&config, &create); regress_member_projection_to_checkpoint(&config, create_offset, "IncrementalFarm"); - let relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("reopen"); + let relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("reopen"); assert_eq!( relay .group_projection() @@ -1168,8 +1226,13 @@ fn source_store_crash_recovery_rebuilds_projection_outbox_and_generated_events() store_source_events(&config, &[create, put]); - let relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("reopen"); + let relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("reopen"); assert_eq!( relay .readiness_state() @@ -1224,8 +1287,13 @@ fn rebuilt_projection_matches_live_projection_for_moderation_stream() { let members_before; { - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(16), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(16), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); accept_group_create(&mut relay, "EquivFarm", &[], 1, &owner_auth); let metadata = tangle_v2_group_metadata_event(FixtureKey::Admin, "EquivFarm", "Market", 2, &[]) @@ -1304,8 +1372,13 @@ fn rebuilt_projection_matches_live_projection_for_moderation_stream() { delete_group_extra_records(&config); - let relay = - BaseRelay::open_with_groups(&config, relay_limits(16), &group_config()).expect("reopen"); + let relay = BaseRelay::open_with_groups( + &config, + relay_limits(16), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("reopen"); assert_projection_without_checkpoint_eq( &live_projection, relay.group_projection().expect("projection"), @@ -1329,8 +1402,13 @@ fn pending_and_retryable_group_outbox_records_materialize_on_restart() { let config = test_store_config("outbox-retryable-restart"); let owner_auth = authenticated(FixtureKey::Owner); { - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); accept_group_create(&mut relay, "OutboxFarm", &[], 1, &owner_auth); relay.shutdown().expect("shutdown"); } @@ -1338,8 +1416,13 @@ fn pending_and_retryable_group_outbox_records_materialize_on_restart() { assert_eq!(outbox_status_counts(&config).pending, 1); assert_eq!(outbox_status_counts(&config).retryable, 1); - let relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("reopen"); + let relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("reopen"); assert_eq!( relay .readiness_state() @@ -1360,9 +1443,13 @@ fn pending_and_retryable_group_outbox_records_materialize_on_restart() { fn max_outbox_replay_batch_one_drains_all_pending_generated_records() { let config = test_store_config("outbox-batch-one"); let owner_auth = authenticated(FixtureKey::Owner); - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config_with_outbox_batch(1)) - .expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config_with_outbox_batch(1), + PocketQueryConfig::default(), + ) + .expect("relay"); accept_group_create(&mut relay, "BatchFarm", &[], 1, &owner_auth); @@ -1379,8 +1466,13 @@ fn already_stored_generated_events_mark_outbox_stored_without_duplication_on_res let config = test_store_config("outbox-generated-already-stored"); let owner_auth = authenticated(FixtureKey::Owner); { - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); accept_group_create(&mut relay, "StoredGeneratedFarm", &[], 1, &owner_auth); relay.shutdown().expect("shutdown"); } @@ -1392,8 +1484,13 @@ fn already_stored_generated_events_mark_outbox_stored_without_duplication_on_res regress_outbox_records_to_pending(&config); assert_eq!(outbox_status_counts(&config).pending, 2); - let relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("reopen"); + let relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("reopen"); assert_eq!(count_kind(&relay, KIND_GROUP_METADATA), 1); assert_eq!(count_kind(&relay, KIND_GROUP_ADMINS), 1); assert_eq!( @@ -1416,8 +1513,13 @@ fn crash_point_recovery_states_match_live_projection_and_generated_events() { let pending_outbox_config = test_store_config("crash-equivalence-pending-outbox"); let events = recovery_equivalence_events(); let expected = { - let mut relay = BaseRelay::open_with_groups(&live_config, relay_limits(8), &group_config()) - .expect("live"); + let mut relay = BaseRelay::open_with_groups( + &live_config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("live"); let owner_auth = authenticated(FixtureKey::Owner); let admin_auth = authenticated(FixtureKey::Admin); assert_accepted( @@ -1438,17 +1540,25 @@ fn crash_point_recovery_states_match_live_projection_and_generated_events() { }; store_source_events(&source_only_config, &events); - let mut source_only = - BaseRelay::open_with_groups(&source_only_config, relay_limits(8), &group_config()) - .expect("source only"); + let mut source_only = BaseRelay::open_with_groups( + &source_only_config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("source only"); assert_eq!(recovery_summary(&mut source_only, "CrashFarm"), expected); assert_eq!(outbox_status_counts(&source_only_config).stored, 5); let offsets = store_source_events(&pending_outbox_config, &events); seed_pending_create_outbox_records(&pending_outbox_config, &events[0], offsets[0]); - let mut pending_outbox = - BaseRelay::open_with_groups(&pending_outbox_config, relay_limits(8), &group_config()) - .expect("pending outbox"); + let mut pending_outbox = BaseRelay::open_with_groups( + &pending_outbox_config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("pending outbox"); assert_eq!(recovery_summary(&mut pending_outbox, "CrashFarm"), expected); let counts = outbox_status_counts(&pending_outbox_config); assert_eq!(counts.pending, 0); @@ -1763,8 +1873,13 @@ fn sorted_strings(values: impl IntoIterator<Item = String>) -> Vec<String> { fn final_group_name_for_order(name: &str, edits: [&Event; 2]) -> String { let config = test_store_config(name); - let mut relay = - BaseRelay::open_with_groups(&config, relay_limits(8), &group_config()).expect("relay"); + let mut relay = BaseRelay::open_with_groups( + &config, + relay_limits(8), + &group_config(), + PocketQueryConfig::default(), + ) + .expect("relay"); let auth = authenticated(FixtureKey::Owner); accept_group_create(&mut relay, "ClockFarm", &[], 1, &auth); for edit in edits { @@ -1981,7 +2096,12 @@ fn runtime_config(groups_enabled: bool) -> BaseRelayRuntimeConfig { }, "pocket": { "data_directory": "runtime/pocket", - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": groups, "auth": { diff --git a/crates/tangle_runtime/tests/ops_truthfulness.rs b/crates/tangle_runtime/tests/ops_truthfulness.rs @@ -136,7 +136,12 @@ fn runtime_config(root: &Path) -> BaseRelayRuntimeConfig { }, "pocket": { "data_directory": root.join("pocket"), - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": true, diff --git a/crates/tangle_runtime/tests/phase2_acceptance_targets.rs b/crates/tangle_runtime/tests/phase2_acceptance_targets.rs @@ -1605,7 +1605,12 @@ fn runtime_config_value(root: &Path, listen_addr: SocketAddr) -> Value { }, "pocket": { "data_directory": root.join("pocket"), - "sync_policy": "flush_on_shutdown" + "sync_policy": "flush_on_shutdown", + "query": { + "allow_scraping": false, + "allow_scrape_if_limited_to": 100, + "allow_scrape_if_max_seconds": 3600 + } }, "groups": { "enabled": true, diff --git a/crates/tangle_store_pocket/src/lib.rs b/crates/tangle_store_pocket/src/lib.rs @@ -93,6 +93,45 @@ pub const TANGLE_POCKET_EXTRA_TABLES: [&str; 3] = [ ]; #[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PocketQueryConfig { + allow_scraping: bool, + allow_scrape_if_limited_to: u32, + allow_scrape_if_max_seconds: u64, +} + +impl PocketQueryConfig { + pub const fn new( + allow_scraping: bool, + allow_scrape_if_limited_to: u32, + allow_scrape_if_max_seconds: u64, + ) -> Self { + Self { + allow_scraping, + allow_scrape_if_limited_to, + allow_scrape_if_max_seconds, + } + } + + pub fn allow_scraping(self) -> bool { + self.allow_scraping + } + + pub fn allow_scrape_if_limited_to(self) -> u32 { + self.allow_scrape_if_limited_to + } + + pub fn allow_scrape_if_max_seconds(self) -> u64 { + self.allow_scrape_if_max_seconds + } +} + +impl Default for PocketQueryConfig { + fn default() -> Self { + Self::new(false, 100, 3_600) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PocketDependencyBoundary { source_repository: &'static str, source_revision: &'static str, @@ -173,17 +212,16 @@ impl PocketStoreHandle { pub fn find_events( &self, filter: &PocketFilter, + query: PocketQueryConfig, ) -> Result<Vec<PocketOwnedEvent>, PocketStoreError> { - self.find_events_with_screen(filter, true, 0, u64::MAX, |_| PocketScreenResult::Match) + self.find_events_with_screen(filter, query, |_| PocketScreenResult::Match) .map(PocketScreenedEvents::into_events) } pub fn find_events_with_screen<F>( &self, filter: &PocketFilter, - allow_scraping: bool, - allow_scrape_if_limited_to: u32, - allow_scrape_if_max_seconds: u64, + query: PocketQueryConfig, screen: F, ) -> Result<PocketScreenedEvents, PocketStoreError> where @@ -193,9 +231,9 @@ impl PocketStoreHandle { .store .find_events( filter, - allow_scraping, - allow_scrape_if_limited_to, - allow_scrape_if_max_seconds, + query.allow_scraping(), + query.allow_scrape_if_limited_to(), + query.allow_scrape_if_max_seconds(), screen, ) .map_err(PocketStoreError::from_pocket)?; @@ -205,8 +243,12 @@ impl PocketStoreHandle { )) } - pub fn count_events(&self, filter: &PocketFilter) -> Result<u64, PocketStoreError> { - self.find_events(filter) + pub fn count_events( + &self, + filter: &PocketFilter, + query: PocketQueryConfig, + ) -> Result<u64, PocketStoreError> { + self.find_events(filter, query) .map(|events| u64::try_from(events.len()).expect("usize count fits in u64")) } @@ -517,9 +559,9 @@ fn event_len_u64(event: &PocketEvent) -> Result<u64, PocketStoreError> { mod tests { use super::{ POCKET_SOURCE_REPOSITORY, POCKET_SOURCE_REVISION, PocketDependencyBoundary, - PocketStoreConfig, PocketStoreHandle, PocketSyncPolicy, TANGLE_GROUP_CHECKPOINT_TABLE, - TANGLE_GROUP_OUTBOX_TABLE, TANGLE_GROUP_PROJECTION_TABLE, TANGLE_POCKET_EXTRA_TABLES, - parse_pocket_event_json, parse_pocket_filter_json, + PocketQueryConfig, PocketStoreConfig, PocketStoreHandle, PocketSyncPolicy, + TANGLE_GROUP_CHECKPOINT_TABLE, TANGLE_GROUP_OUTBOX_TABLE, TANGLE_GROUP_PROJECTION_TABLE, + TANGLE_POCKET_EXTRA_TABLES, parse_pocket_event_json, parse_pocket_filter_json, }; use pocket_db::ScreenResult; use std::time::{SystemTime, UNIX_EPOCH}; @@ -575,13 +617,20 @@ mod tests { .expect("lookup") .expect("event"); let offset_event = handle.event_by_offset(offset).expect("offset lookup"); - let found = handle.find_events(&filter).expect("find"); + let found = handle + .find_events(&filter, PocketQueryConfig::default()) + .expect("find"); assert_eq!(stored.id(), event.id()); assert_eq!(offset_event.id(), event.id()); assert_eq!(found.len(), 1); assert_eq!(found[0].id(), event.id()); - assert_eq!(handle.count_events(&filter).expect("count"), 1); + assert_eq!( + handle + .count_events(&filter, PocketQueryConfig::default()) + .expect("count"), + 1 + ); let _ = std::fs::remove_dir_all(root); } @@ -632,7 +681,7 @@ mod tests { handle.store_event(&redacted).expect("store redacted"); let screened = handle - .find_events_with_screen(&filter, true, 0, u64::MAX, |event| { + .find_events_with_screen(&filter, PocketQueryConfig::default(), |event| { if event.id() == visible.id() { ScreenResult::Match } else { @@ -649,6 +698,35 @@ mod tests { } #[test] + fn pocket_store_query_config_controls_scraping() { + let root = temp_root("tangle-pocket-query-config"); + let config = PocketStoreConfig::new(root.join("pocket"), PocketSyncPolicy::FlushOnShutdown) + .expect("config"); + let handle = PocketStoreHandle::open(&config).expect("open"); + let event = + parse_pocket_event_json(event_json_with("f", "6", "scrape").as_bytes()).expect("event"); + let broad = parse_pocket_filter_json(r#"{"limit":1}"#.as_bytes()).expect("filter"); + + handle.store_event(&event).expect("store"); + + assert!( + handle + .find_events(&broad, PocketQueryConfig::new(false, 0, 0)) + .expect_err("scrape rejected") + .message() + .contains("scraper") + ); + let found = handle + .find_events(&broad, PocketQueryConfig::new(false, 1, 0)) + .expect("limited scrape"); + + assert_eq!(found.len(), 1); + assert_eq!(found[0].id(), event.id()); + + let _ = std::fs::remove_dir_all(root); + } + + #[test] fn pocket_store_handle_persists_extra_table_records() { let root = temp_root("tangle-pocket-extra"); let config = PocketStoreConfig::new(root.join("pocket"), PocketSyncPolicy::FlushOnShutdown)