this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Tighten indexer URL priority chain

The `resolve_indexer_url` chain had four levels with priority 3 dead
and priority 2 inert on cold start:

1. Runtime override — `set_indexer_url()`
2. PDS accountConfig — mirrored from `accountConfig.indexerUrl`
3. Caller-provided `default: Option<&str>` (always None)
4. Compile-time `DEFAULT_INDEXER_URL`

Priority 3 was self-labeled "legacy arg, usually None"; every Rust
caller passed None and the SDK TS wrappers literally passed `null`
through to the WASM. Priority 2 was populated only by
`set_account_config`, so on cold start it was always empty — the
effective chain collapsed to "runtime override || compile-time
default," with the user's PDS-configured URL never consulted.

Collapse to three real levels:

1. Runtime override — unchanged.
2. PDS accountConfig — now seeded best-effort in `for_account`
via `get_account_config().await`, so the chain actually works
on a fresh boot. Offline, missing record, or auth blips fall
through silently to the compile-time default.
3. Compile-time default — unchanged.

Side effects of the collapse:

- `resolve_indexer_url` returns `String` rather than
`Result<String, Error>`. The `NotFound` arm was unreachable
under the old chain too (the const fallback is non-empty);
the signature now tells the truth.
- The `default: Option<&str>` parameter is removed from
`resolve_indexer_url` and from `request_sse_token`, `list_inbox`,
`list_workspace_documents`, `discover_member_keyrings`.
- The five WASM bindings mirroring those methods
(`listWorkspaces`, `listInbox`, `listWorkspaceDocuments`,
`discoverMemberKeyrings`, `requestSseToken`) stop taking the
`Option<String>` argument — matching what the SDK TS wrappers
already did internally.
- `startSseConsumer(indexerUrl)` keeps its JS-side parameter but
its semantics are upgraded: a caller-supplied URL is promoted to
the runtime override (priority 1) via `set_indexer_url` before
resolving, so it wins over PDS config and persists across
subsequent indexer calls on the same Opake. A dropped URL
landing on the old priority 3 would have lost to the PDS config
— this is arguably a bugfix.
- The CLI `--indexer` flag on `opake inbox` now promotes via
`set_indexer_url` rather than passing through a dead parameter,
matching the same runtime-override semantics.

+73 -98
+2 -8
apps/cli/src/commands/daemon/mod.rs
··· 263 263 } 264 264 }; 265 265 266 - let indexer_url = match opake.resolve_indexer_url(None) { 267 - Ok(url) => url, 268 - Err(e) => { 269 - warn!("sync: no indexer URL for {did}: {e}"); 270 - return; 271 - } 272 - }; 266 + let indexer_url = opake.resolve_indexer_url(); 273 267 274 268 let opake = Rc::new(Mutex::new(opake)); 275 269 ··· 389 383 let opake = Rc::clone(&opake); 390 384 Box::pin(async move { 391 385 let mut guard = opake.lock().await; 392 - guard.request_sse_token(None).await 386 + guard.request_sse_token().await 393 387 }) 394 388 }) 395 389 }
+7 -4
apps/cli/src/commands/inbox.rs
··· 43 43 async fn execute(self, ctx: &CommandContext) -> Result<Option<Session>> { 44 44 let mut opake = ctx.opake().await?; 45 45 46 - // CLI: flag → env var → account config (core handles the last one). 47 - let env_url = std::env::var("OPAKE_INDEXER_URL").ok(); 48 - let indexer_url = self.indexer.as_deref().or(env_url.as_deref()); 46 + // `--indexer` promotes to the runtime override (priority 1), above 47 + // OPAKE_INDEXER_URL (already seeded by `ctx.opake()`) and the user's 48 + // accountConfig. This is a per-invocation debugging knob. 49 + if let Some(url) = self.indexer { 50 + opake.set_indexer_url(url); 51 + } 49 52 50 - let grants = opake.list_inbox(indexer_url).await?; 53 + let grants = opake.list_inbox().await?; 51 54 52 55 if grants.is_empty() { 53 56 println!("no incoming grants");
+1 -1
apps/cli/src/commands/workspace.rs
··· 117 117 // member of — owned workspaces included, since the owner is always a 118 118 // member of their own. Staleness window exists after `workspace 119 119 // create` until Jetstream delivers the commit to the indexer's firehose consumer. 120 - let keyrings = opake.discover_member_keyrings(None).await?; 120 + let keyrings = opake.discover_member_keyrings().await?; 121 121 122 122 if keyrings.is_empty() { 123 123 println!("no workspaces");
+2 -7
crates/opake-core/src/manager/tree.rs
··· 391 391 &self, 392 392 cached: CachedCollection, 393 393 ) -> Result<(Vec<CachedRecord>, Vec<crate::indexer::TreeProposal>), Error> { 394 - let indexer_url = match self.opake.resolve_indexer_url(None) { 395 - Ok(url) => url, 396 - Err(_) => return Ok((cached.records, Vec::new())), 397 - }; 394 + let indexer_url = self.opake.resolve_indexer_url(); 398 395 let signing_key = match self.opake.require_identity() { 399 396 Ok(id) => match id.signing_key_bytes() { 400 397 Ok(Some(k)) => k, ··· 542 539 /// Fetches a full snapshot from the Indexer, caches it locally, 543 540 /// and returns a tree built from the cached records. 544 541 async fn bootstrap_tree(&mut self) -> Result<DirectoryTree, Error> { 545 - let indexer_url = self.opake.resolve_indexer_url(None).map_err(|_| { 546 - Error::Storage("Indexer URL required — configure one or self-host".into()) 547 - })?; 542 + let indexer_url = self.opake.resolve_indexer_url(); 548 543 549 544 let identity = self.opake.require_identity()?; 550 545 let signing_key = identity
+32 -36
crates/opake-core/src/opake.rs
··· 133 133 }; 134 134 135 135 let client = XrpcClient::with_session(transport, account.pds_url.clone(), session); 136 - let opake = Self::new(client, target_did, identity, rng, storage, now_micros); 137 - // Indexer URL resolution happens lazily in `resolve_indexer_url`: 138 - // runtime override → PDS config → compile-time default. No seeding 139 - // needed here — the priority chain has a const fallback. 136 + let mut opake = Self::new(client, target_did, identity, rng, storage, now_micros); 137 + // Seed `config_indexer_url` (priority 2 of `resolve_indexer_url`) from 138 + // the user's PDS accountConfig so the priority chain actually works on 139 + // cold start. Best-effort — offline, missing record, or auth blips 140 + // fall through silently to the compile-time default. Runtime overrides 141 + // installed after construction via `set_indexer_url` still win. 142 + if let Ok(Some(config)) = opake.get_account_config().await { 143 + opake.config_indexer_url = config.indexer_url; 144 + } 140 145 Ok(opake) 141 146 } 142 147 ··· 236 241 let identity = self.require_identity()?; 237 242 let private_key = identity.private_key_bytes()?; 238 243 239 - let keyrings = self.discover_member_keyrings(None).await?; 244 + let keyrings = self.discover_member_keyrings().await?; 240 245 let matches: Vec<String> = keyrings 241 246 .iter() 242 247 .filter(|kr| { ··· 478 483 &mut self, 479 484 ) -> Result<Vec<crate::indexer::daemon::WorkspaceSyncResult>, Error> { 480 485 log::trace!("sync: discovering workspaces for {}", self.did); 481 - let indexer_keyrings = self.discover_member_keyrings(None).await?; 486 + let indexer_keyrings = self.discover_member_keyrings().await?; 482 487 log::trace!("sync: found {} workspaces", indexer_keyrings.len()); 483 488 let identity = self.require_identity()?; 484 489 let private_key = identity.private_key_bytes()?; ··· 501 506 &mut self, 502 507 keyring_uri: &str, 503 508 ) -> Result<Option<crate::indexer::daemon::WorkspaceSyncResult>, Error> { 504 - let indexer_keyrings = self.discover_member_keyrings(None).await?; 509 + let indexer_keyrings = self.discover_member_keyrings().await?; 505 510 let target = indexer_keyrings.iter().find(|kr| kr.uri == keyring_uri); 506 511 let Some(kr) = target else { return Ok(None) }; 507 512 ··· 1420 1425 /// Resolve the indexer URL to use for a request. 1421 1426 /// 1422 1427 /// Priority (highest first): 1423 - /// 1. Runtime override (`set_indexer_url`) 1424 - /// 2. PDS account config (`config_indexer_url`, mirrored from 1425 - /// `accountConfig.indexerUrl`) 1426 - /// 3. Caller-provided fallback (legacy arg, usually `None`) 1427 - /// 4. Compile-time `DEFAULT_INDEXER_URL` 1428 + /// 1. Runtime override — `set_indexer_url()`. Host-level knob populated 1429 + /// at boot from `OPAKE_INDEXER_URL` (CLI) or `VITE_INDEXER_URL` 1430 + /// (web). Wins so dev/ops overrides aren't undone by stored config. 1431 + /// 2. PDS accountConfig — `config_indexer_url`, mirrored from the 1432 + /// user's `app.opake.accountConfig` record. Seeded best-effort by 1433 + /// `for_account` at boot; kept in sync by `set_account_config`. 1434 + /// 3. Compile-time `DEFAULT_INDEXER_URL` — always present. 1428 1435 /// 1429 - /// Always resolves — the compile-time default is a non-empty const, 1430 - /// so the `Result` shape is preserved for API compatibility but the 1431 - /// `NotFound` arm is unreachable under normal operation. 1432 - pub fn resolve_indexer_url(&self, default: Option<&str>) -> Result<String, Error> { 1436 + /// Always returns a valid URL. Plain `String` rather than `Result` 1437 + /// because the const fallback is unreachable-free. 1438 + pub fn resolve_indexer_url(&self) -> String { 1433 1439 self.runtime_indexer_url 1434 1440 .clone() 1435 1441 .or_else(|| self.config_indexer_url.clone()) 1436 - .or_else(|| default.map(|s| s.to_string())) 1437 - .or_else(|| Some(DEFAULT_INDEXER_URL.to_string())) 1438 - .ok_or_else(|| Error::NotFound("no indexer URL configured".into())) 1442 + .unwrap_or_else(|| DEFAULT_INDEXER_URL.to_string()) 1439 1443 } 1440 1444 1441 1445 // -- SSE token (for EventSource auth) -- ··· 1444 1448 /// 1445 1449 /// The token is passed as a query parameter to the SSE endpoint, 1446 1450 /// sidestepping EventSource's inability to send custom headers. 1447 - pub async fn request_sse_token( 1448 - &mut self, 1449 - default_indexer_url: Option<&str>, 1450 - ) -> Result<String, Error> { 1451 + pub async fn request_sse_token(&mut self) -> Result<String, Error> { 1451 1452 let identity = self.require_identity()?; 1452 1453 let signing_key = identity 1453 1454 .signing_key_bytes()? 1454 1455 .ok_or_else(|| Error::Auth("no signing key for SSE token request".into()))?; 1455 1456 1456 - let url = self.resolve_indexer_url(default_indexer_url)?; 1457 + let url = self.resolve_indexer_url(); 1457 1458 crate::indexer::request_sse_token(self.client.transport(), &url, &self.did, &signing_key) 1458 1459 .await 1459 1460 } ··· 1462 1463 1463 1464 /// Fetch all incoming grants from the Indexer. 1464 1465 /// 1465 - /// Returns an empty list if no indexer URL is configured or no signing key exists. 1466 - pub async fn list_inbox( 1467 - &mut self, 1468 - default_indexer_url: Option<&str>, 1469 - ) -> Result<Vec<crate::indexer::InboxGrant>, Error> { 1466 + /// Returns an empty list if no signing key exists. 1467 + pub async fn list_inbox(&mut self) -> Result<Vec<crate::indexer::InboxGrant>, Error> { 1470 1468 let identity = match self.identity.as_ref() { 1471 1469 Some(id) => id, 1472 1470 None => return Ok(vec![]), ··· 1476 1474 None => return Ok(vec![]), 1477 1475 }; 1478 1476 1479 - let url = self.resolve_indexer_url(default_indexer_url)?; 1477 + let url = self.resolve_indexer_url(); 1480 1478 1481 1479 crate::indexer::fetch_inbox_all(self.client.transport(), &url, &self.did, &signing_key) 1482 1480 .await ··· 1484 1482 1485 1483 /// Fetch workspace documents from the Indexer. 1486 1484 /// 1487 - /// Returns an empty list if no indexer URL is configured or no signing key exists. 1485 + /// Returns an empty list if no signing key exists. 1488 1486 pub async fn list_workspace_documents( 1489 1487 &mut self, 1490 1488 keyring_uri: &str, 1491 - default_indexer_url: Option<&str>, 1492 1489 ) -> Result<Vec<crate::indexer::WorkspaceDocument>, Error> { 1493 1490 let identity = match self.identity.as_ref() { 1494 1491 Some(id) => id, ··· 1499 1496 None => return Ok(vec![]), 1500 1497 }; 1501 1498 1502 - let url = self.resolve_indexer_url(default_indexer_url)?; 1499 + let url = self.resolve_indexer_url(); 1503 1500 1504 1501 crate::indexer::fetch_workspace_documents( 1505 1502 self.client.transport(), ··· 1513 1510 1514 1511 /// Fetch all keyrings the user is a member of, with full record data. 1515 1512 /// 1516 - /// Returns an empty list if no indexer URL is configured or no signing key exists. 1513 + /// Returns an empty list if no signing key exists. 1517 1514 pub async fn discover_member_keyrings( 1518 1515 &mut self, 1519 - default_indexer_url: Option<&str>, 1520 1516 ) -> Result<Vec<crate::indexer::IndexerKeyring>, Error> { 1521 1517 let identity = match self.identity.as_ref() { 1522 1518 Some(id) => id, ··· 1527 1523 None => return Ok(vec![]), 1528 1524 }; 1529 1525 1530 - let url = self.resolve_indexer_url(default_indexer_url)?; 1526 + let url = self.resolve_indexer_url(); 1531 1527 1532 1528 crate::indexer::fetch_member_keyrings( 1533 1529 self.client.transport(),
+10 -29
crates/opake-wasm/src/opake_wasm.rs
··· 225 225 /// bootstrapped, incremental SSE events keep the keeper in sync 226 226 /// without further `listWorkspaces` round-trips. 227 227 #[wasm_bindgen(js_name = listWorkspaces)] 228 - pub async fn list_workspaces( 229 - &self, 230 - default_indexer_url: Option<String>, 231 - ) -> Result<JsValue, JsError> { 228 + pub async fn list_workspaces(&self) -> Result<JsValue, JsError> { 232 229 let mut opake = self.opake().await?; 233 230 let identity = opake.require_identity().map_err(wasm_err)?; 234 231 let private_key = identity.private_key_bytes().map_err(wasm_err)?; 235 232 let did = opake.did().to_string(); 236 233 237 234 let keyrings = opake 238 - .discover_member_keyrings(default_indexer_url.as_deref()) 235 + .discover_member_keyrings() 239 236 .await 240 237 .map_err(wasm_err)?; 241 238 drop(opake); ··· 694 691 695 692 /// Fetch workspace documents from the Indexer. 696 693 #[wasm_bindgen(js_name = listWorkspaceDocuments)] 697 - pub async fn list_workspace_documents( 698 - &self, 699 - keyring_uri: &str, 700 - default_indexer_url: Option<String>, 701 - ) -> Result<JsValue, JsError> { 694 + pub async fn list_workspace_documents(&self, keyring_uri: &str) -> Result<JsValue, JsError> { 702 695 let mut opake = self.opake().await?; 703 696 let docs = opake 704 - .list_workspace_documents(keyring_uri, default_indexer_url.as_deref()) 697 + .list_workspace_documents(keyring_uri) 705 698 .await 706 699 .map_err(wasm_err)?; 707 700 to_js(&docs) ··· 709 702 710 703 /// Discover keyrings the user is a member of (across all PDSes). 711 704 #[wasm_bindgen(js_name = discoverMemberKeyrings)] 712 - pub async fn discover_member_keyrings( 713 - &self, 714 - default_indexer_url: Option<String>, 715 - ) -> Result<JsValue, JsError> { 705 + pub async fn discover_member_keyrings(&self) -> Result<JsValue, JsError> { 716 706 let mut opake = self.opake().await?; 717 - let keyrings = opake 718 - .discover_member_keyrings(default_indexer_url.as_deref()) 719 - .await 720 - .map_err(wasm_err)?; 707 + let keyrings = opake.discover_member_keyrings().await.map_err(wasm_err)?; 721 708 to_js(&keyrings) 722 709 } 723 710 724 711 /// Request a short-lived SSE token from the Indexer. 725 712 #[wasm_bindgen(js_name = requestSseToken)] 726 - pub async fn request_sse_token(&self, indexer_url: Option<String>) -> Result<String, JsError> { 713 + pub async fn request_sse_token(&self) -> Result<String, JsError> { 727 714 let mut opake = self.opake().await?; 728 - opake 729 - .request_sse_token(indexer_url.as_deref()) 730 - .await 731 - .map_err(wasm_err) 715 + opake.request_sse_token().await.map_err(wasm_err) 732 716 } 733 717 734 718 /// Fetch all incoming grants from the Indexer. ··· 740 724 /// events keep the keeper in sync without further `listInbox` 741 725 /// round-trips. 742 726 #[wasm_bindgen(js_name = listInbox)] 743 - pub async fn list_inbox(&self, indexer_url: Option<String>) -> Result<JsValue, JsError> { 727 + pub async fn list_inbox(&self) -> Result<JsValue, JsError> { 744 728 let mut opake = self.opake().await?; 745 - let grants = opake 746 - .list_inbox(indexer_url.as_deref()) 747 - .await 748 - .map_err(wasm_err)?; 729 + let grants = opake.list_inbox().await.map_err(wasm_err)?; 749 730 drop(opake); 750 731 751 732 let entries: Vec<opake_core::indexer::inbox_keeper::InboxEntry> =
+17 -11
crates/opake-wasm/src/sse_wasm.rs
··· 317 317 /// and dispatches them to the shared TreeKeeper. 318 318 /// 319 319 /// `indexer_url` is optional: if omitted, the URL is resolved from 320 - /// the Opake instance's stored config (loaded during `init`). Pass 321 - /// an explicit value as a fallback for Opake instances whose config 322 - /// doesn't include an indexer URL. 320 + /// the Opake instance's priority chain (runtime override, PDS 321 + /// accountConfig, compile-time default). If provided, it's promoted 322 + /// to the runtime override (priority 1) for this Opake instance — 323 + /// it wins over accountConfig and persists across subsequent indexer 324 + /// calls within the same session. 323 325 /// 324 326 /// Idempotent: subsequent calls are no-ops while an existing 325 327 /// consumer is running. React StrictMode's double-mount is thus 326 328 /// harmless — only one consumer task exists per OpakeContext. 327 329 #[wasm_bindgen(js_name = startSseConsumer)] 328 330 pub async fn start_sse_consumer(&self, indexer_url: Option<String>) -> Result<(), JsError> { 329 - // Resolve the URL BEFORE flipping the started flag — if no URL 330 - // is available anywhere, we want to fail loudly without leaving 331 - // the flag in a broken state. 331 + // If the caller supplied a URL, promote it to the runtime override 332 + // (priority 1) before resolving — so passing a URL here wins over 333 + // PDS accountConfig and is consistent with every subsequent 334 + // indexer call made through this Opake instance. Resolve BEFORE 335 + // flipping `sse_started` so a URL-less call on a fresh Opake 336 + // without a default still surfaces the error cleanly. 332 337 let resolved_url = { 333 - let guard = self.inner.lock().await; 338 + let mut guard = self.inner.lock().await; 334 339 let opake = guard 335 - .as_ref() 340 + .as_mut() 336 341 .ok_or_else(|| JsError::new("Opake context already consumed"))?; 337 - opake 338 - .resolve_indexer_url(indexer_url.as_deref()) 339 - .map_err(wasm_err)? 342 + if let Some(url) = indexer_url { 343 + opake.set_indexer_url(url); 344 + } 345 + opake.resolve_indexer_url() 340 346 }; 341 347 342 348 if self.sse_started.get() {
+2 -2
packages/opake-sdk/src/opake.ts
··· 595 595 @withTokenGuard 596 596 listWorkspaces(): Promise<readonly WorkspaceEntry[]> { 597 597 return this.requireContext() 598 - .listWorkspaces(null) 598 + .listWorkspaces() 599 599 .then(listWorkspacesResultSchema.parse); 600 600 } 601 601 ··· 933 933 @withTokenGuard 934 934 listInbox(): Promise<readonly InboxGrant[]> { 935 935 return this.requireContext() 936 - .listInbox(null) 936 + .listInbox() 937 937 .then((raw) => inboxGrantsSchema.parse(raw)) as Promise<readonly InboxGrant[]>; 938 938 } 939 939