Fall back to a parent's token when a stored one is refused; send the signer's identity with a bookmark move 68a924846379
Two things that stopped agents tonight. A subrepo checkout with no token of its own took the user's credential store before an enclosing checkout's remote on the same hub. A stale stored login (or another account's) then won, and a private subrepo answered 404 although the parent's own token reads it (the R* agent and rstar-api). Now, when the token came from the store or an enclosing checkout, the other tokens enclosing checkouts hold for that hub travel as RemoteOptions::fallback_tokens, and the HTTP remote tries the next one when a request is refused (401, 403 or 404), saying so once. A remote's own token and $OXY_TOKEN are explicit and never swapped. A delegate that may move references made a bookmark at its owner's change and was refused 'unknown key': a push carried only the identities of its changes' authors, so a host that had seen no change by the delegate could not check its attestation. A push that moves a bookmark now carries the identity that signed the move and the one it acts for. Tested: a nested checkout with a stale stored login pushes with its parent's token (and one with its own wrong token still fails); a delegate's first push of a bookmark at its owner's change is accepted (without the fix: unknown key). closes #112, refs andymccall/oxygenhub#129
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-14 04:06 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
68a9248463792051b4dfa1b01061a7c7f2ed33ac4c63126b0cca064b3bd52437- Depends on
- 096f447ccda0 2b5ee371060e 5b9c6ad45f6e 6f8aeb996558 750251b21a96 75167672ebcd 7ba155f20aa0 84b9e9ac0ab4 982864068a59 9f9e72b93e4e be451356e3ef ee06a6c557b7 ff0af1ee299d ff1a64f81c1c ff1f62fee654
- Depended on by
- 0588de69a84d 569802ebae0e ee130dd9eef2 67a4fe1aa2d5 d36ba2f79f95 f0a517d28237
- In bookmarks
- carried-not-removed doctor-trust identical-sides keyless-tags main merge-remote-bookmark paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state
- In tags
- v0.10.0 v0.11.0 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 31 atoms, 0 tree operations, 8 files
Browse the tree at this change
modified crates/oxy-core/src/credentials.rs +2 −3
| … | 333 unchanged lines |
||
| 334 | 334 | RemoteOptions { |
|
| 335 | 335 | token: env_token.or(token), |
|
| 336 | 336 | ca: env_ca.or(ca), |
|
| 337 | - | repo_id: None, |
|
| 338 | - | cache_dir: None, |
|
| 337 | + | ..RemoteOptions::default() |
|
| 339 | 338 | } |
|
| 340 | 339 | } |
|
| 341 | 340 | } |
|
| … | 9 unchanged lines |
||
| 351 | 350 | cache().lock().unwrap().clear(); |
|
| 352 | 351 | } |
|
| 353 | 352 | ||
| 354 | - | fn warn_once(message: &str) { |
|
| 353 | + | pub(crate) fn warn_once(message: &str) { |
|
| 355 | 354 | static SEEN: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new(); |
|
| 356 | 355 | if SEEN |
|
| 357 | 356 | .get_or_init(Default::default) |
|
| … | 220 unchanged lines |
modified crates/oxy-core/src/protocol.rs +52 −3
| … | 229 unchanged lines |
||
| 230 | 230 | /// `.oxy/remotes`. None (a clone, before there is a repository) means |
|
| 231 | 231 | /// a temporary directory. |
|
| 232 | 232 | pub cache_dir: Option<PathBuf>, |
|
| 233 | + | /// Other tokens for the same hub, tried in turn when `token` is |
|
| 234 | + | /// refused (401, 403 or 404): those of enclosing checkouts, when |
|
| 235 | + | /// `token` came from the credential store rather than from this |
|
| 236 | + | /// remote's own configuration (oxygen#112). |
|
| 237 | + | pub fallback_tokens: Vec<String>, |
|
| 233 | 238 | } |
|
| 234 | 239 | ||
| 235 | 240 | /// Opens a store for a URL it understands, or passes (`None`). Stores that |
|
| … | 35 unchanged lines |
||
| 271 | 276 | /// A remote served by `oxy-server` at `<base>/oxy/v1/...`. |
|
| 272 | 277 | pub struct HttpRemote { |
|
| 273 | 278 | base: String, |
|
| 274 | - | token: Option<String>, |
|
| 279 | + | /// The token in use, and those still to try when it is refused. |
|
| 280 | + | token: Mutex<Option<String>>, |
|
| 281 | + | fallbacks: Mutex<Vec<String>>, |
|
| 275 | 282 | repo_id: Option<[u8; 16]>, |
|
| 276 | 283 | agent: ureq::Agent, |
|
| 277 | 284 | } |
|
| … | 38 unchanged lines |
||
| 316 | 323 | .build(); |
|
| 317 | 324 | Ok(HttpRemote { |
|
| 318 | 325 | base: base.trim_end_matches('/').to_string(), |
|
| 319 | - | token: options.token.clone(), |
|
| 326 | + | token: Mutex::new(options.token.clone()), |
|
| 327 | + | fallbacks: Mutex::new(options.fallback_tokens.iter().rev().cloned().collect()), |
|
| 320 | 328 | repo_id: options.repo_id, |
|
| 321 | 329 | agent, |
|
| 322 | 330 | }) |
|
| 323 | 331 | } |
|
| 324 | 332 | ||
| 333 | + | /// A request, tried again with the next fallback token while the |
|
| 334 | + | /// hub refuses the one in use (oxygen#112). |
|
| 325 | 335 | fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> { |
|
| 336 | + | loop { |
|
| 337 | + | let token = self.token.lock().unwrap_or_else(|p| p.into_inner()).clone(); |
|
| 338 | + | match self.call_once(post, endpoint, body, token.as_deref()) { |
|
| 339 | + | Err(RepoError::Msg(m)) if token.is_some() && refused(&m) => { |
|
| 340 | + | let next = self |
|
| 341 | + | .fallbacks |
|
| 342 | + | .lock() |
|
| 343 | + | .unwrap_or_else(|p| p.into_inner()) |
|
| 344 | + | .pop(); |
|
| 345 | + | let Some(next) = next else { |
|
| 346 | + | return Err(RepoError::Msg(m)); |
|
| 347 | + | }; |
|
| 348 | + | crate::credentials::warn_once(&format!( |
|
| 349 | + | "{}: the stored token was refused; using the one an enclosing checkout has for this hub", |
|
| 350 | + | self.base |
|
| 351 | + | )); |
|
| 352 | + | *self.token.lock().unwrap_or_else(|p| p.into_inner()) = Some(next); |
|
| 353 | + | } |
|
| 354 | + | other => return other, |
|
| 355 | + | } |
|
| 356 | + | } |
|
| 357 | + | } |
|
| 358 | + | ||
| 359 | + | fn call_once( |
|
| 360 | + | &self, |
|
| 361 | + | post: bool, |
|
| 362 | + | endpoint: &str, |
|
| 363 | + | body: Option<&[u8]>, |
|
| 364 | + | token: Option<&str>, |
|
| 365 | + | ) -> Result<Vec<u8>> { |
|
| 326 | 366 | let url = format!("{}/oxy/v1/{endpoint}", self.base); |
|
| 327 | 367 | let mut req = if post { |
|
| 328 | 368 | self.agent.post(&url) |
|
| … | 1 unchanged lines |
||
| 330 | 370 | self.agent.get(&url) |
|
| 331 | 371 | }; |
|
| 332 | 372 | req = req.set("Accept", CONTENT_TYPE); |
|
| 333 | - | if let Some(t) = &self.token { |
|
| 373 | + | if let Some(t) = token { |
|
| 334 | 374 | req = req.set("Authorization", &format!("Bearer {t}")); |
|
| 335 | 375 | } |
|
| 336 | 376 | if let Some(id) = &self.repo_id { |
|
| … | 20 unchanged lines |
||
| 357 | 397 | Err(e) => Err(RepoError::Msg(with_hint(e.to_string()))), |
|
| 358 | 398 | } |
|
| 359 | 399 | } |
|
| 400 | + | } |
|
| 401 | + | ||
| 402 | + | /// Whether a failed request's message says the hub refused its token: |
|
| 403 | + | /// HTTP 401, 403, or 404 (a hub hides a private repository from a token |
|
| 404 | + | /// that may not read it). |
|
| 405 | + | fn refused(message: &str) -> bool { |
|
| 406 | + | [": HTTP 401", ": HTTP 403", ": HTTP 404"] |
|
| 407 | + | .iter() |
|
| 408 | + | .any(|c| message.contains(c)) |
|
| 360 | 409 | } |
|
| 361 | 410 | ||
| 362 | 411 | /// Refuse to send a token where anyone on the way could read it: plain |
|
| … | 882 unchanged lines |
modified crates/oxy-core/src/repo/hosted.rs +37 −0
| … | 630 unchanged lines |
||
| 631 | 631 | )); |
|
| 632 | 632 | } |
|
| 633 | 633 | ||
| 634 | + | /// oxygenhub#129: a delegate that may move references makes a bookmark |
|
| 635 | + | /// at its owner's change, signing no change of its own. The push |
|
| 636 | + | /// carries its identity, so the host can check the move (it was |
|
| 637 | + | /// refused as "unknown key" until the delegate had pushed a change). |
|
| 638 | + | #[test] |
|
| 639 | + | fn a_delegate_s_first_push_of_a_bookmark_carries_its_identity() { |
|
| 640 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 641 | + | let (host, mut user, _, _, _) = setup(dir.path(), true, false); |
|
| 642 | + | let agent_home = dir.path().join("agent-home"); |
|
| 643 | + | let mut agent_repo = Repo::init(&dir.path().join("agent-work")) |
|
| 644 | + | .unwrap() |
|
| 645 | + | .with_home(agent_home.clone()); |
|
| 646 | + | let (agent, agent_id) = agent_repo |
|
| 647 | + | .create_identity("agent", "agent@example", IdentityScope::User) |
|
| 648 | + | .unwrap(); |
|
| 649 | + | user.delegate_add(agent_id, Scope::CHANGES | Scope::REFS, None) |
|
| 650 | + | .unwrap(); |
|
| 651 | + | assert!(user.push("origin", false).unwrap().iter().all(|x| x.ok)); |
|
| 652 | + | let work = dir.path().join("work"); |
|
| 653 | + | let mut as_agent = Repo::open(&work).unwrap().with_home(agent_home); |
|
| 654 | + | as_agent |
|
| 655 | + | .set_user_config("author.signer", &agent.to_hex()) |
|
| 656 | + | .unwrap(); |
|
| 657 | + | assert!( |
|
| 658 | + | host.store().get_identity(&agent).is_err(), |
|
| 659 | + | "the host has never seen the agent" |
|
| 660 | + | ); |
|
| 661 | + | let heads = as_agent.bookmark_heads().unwrap(); |
|
| 662 | + | as_agent.bookmark_create("feature", Some(&heads)).unwrap(); |
|
| 663 | + | let r = as_agent.push_bookmark("origin", "feature", false).unwrap(); |
|
| 664 | + | assert!(r.iter().all(|x| x.ok), "{r:?}"); |
|
| 665 | + | assert!( |
|
| 666 | + | host.store().get_identity(&agent).is_ok(), |
|
| 667 | + | "its identity came with the move" |
|
| 668 | + | ); |
|
| 669 | + | } |
|
| 670 | + | ||
| 634 | 671 | /// A delegation past its expiry authenticates nothing made after it, |
|
| 635 | 672 | /// and the delegator may revoke the key; a stranger's key, they may not. |
|
| 636 | 673 | #[test] |
|
| … | 125 unchanged lines |
modified crates/oxy-core/src/repo/sync.rs +52 −12
| … | 913 unchanged lines |
||
| 914 | 914 | token: self.config.remote_tokens.get(name).cloned(), |
|
| 915 | 915 | ca: self.config.remote_cas.get(name).map(PathBuf::from), |
|
| 916 | 916 | cache_dir: Some(self.oxy.join("remotes").join(name).join("strata")), |
|
| 917 | + | ..RemoteOptions::default() |
|
| 917 | 918 | }; |
|
| 918 | 919 | self.fill_credentials(&url, &mut options); |
|
| 919 | 920 | options |
|
| … | 13 unchanged lines |
||
| 933 | 934 | /// Fill in what `options` lacks for `url`: `$OXY_TOKEN` and `$OXY_CA`, |
|
| 934 | 935 | /// then the user's credential store for its hub, then a remote on the |
|
| 935 | 936 | /// same hub in an enclosing checkout (a subrepo borrows its parent's). |
|
| 937 | + | /// When the token is not this remote's own or the environment's, the |
|
| 938 | + | /// other tokens enclosing checkouts hold for the hub are kept to try |
|
| 939 | + | /// if it is refused: a stale stored login must not hide a parent's |
|
| 940 | + | /// good token (oxygen#112). |
|
| 936 | 941 | fn fill_credentials(&self, url: &str, options: &mut RemoteOptions) { |
|
| 937 | 942 | if options.token.is_some() && options.ca.is_some() { |
|
| 938 | 943 | return; |
|
| 939 | 944 | } |
|
| 945 | + | let explicit = |
|
| 946 | + | options.token.is_some() || std::env::var("OXY_TOKEN").is_ok_and(|t| !t.is_empty()); |
|
| 940 | 947 | let stored = self.credentials().options_for(url); |
|
| 941 | 948 | options.token = options.token.take().or(stored.token); |
|
| 942 | 949 | options.ca = options.ca.take().or(stored.ca); |
|
| 943 | - | if options.token.is_some() && options.ca.is_some() { |
|
| 944 | - | return; |
|
| 950 | + | let enclosing = self.enclosing_credentials(url); |
|
| 951 | + | if options.token.is_none() { |
|
| 952 | + | options.token = enclosing.iter().find_map(|(t, _)| t.clone()); |
|
| 953 | + | } |
|
| 954 | + | if options.ca.is_none() { |
|
| 955 | + | options.ca = enclosing.iter().find_map(|(_, c)| c.clone()); |
|
| 956 | + | } |
|
| 957 | + | if !explicit { |
|
| 958 | + | for t in enclosing.into_iter().filter_map(|(t, _)| t) { |
|
| 959 | + | if options.token.as_ref() != Some(&t) && !options.fallback_tokens.contains(&t) { |
|
| 960 | + | options.fallback_tokens.push(t); |
|
| 961 | + | } |
|
| 962 | + | } |
|
| 945 | 963 | } |
|
| 964 | + | } |
|
| 965 | + | ||
| 966 | + | /// The tokens and CAs of remotes on `url`'s hub in enclosing |
|
| 967 | + | /// checkouts, nearest first. |
|
| 968 | + | fn enclosing_credentials(&self, url: &str) -> Vec<(Option<String>, Option<PathBuf>)> { |
|
| 969 | + | let mut out = Vec::new(); |
|
| 946 | 970 | let Some(hub) = crate::credentials::hub_of(url) else { |
|
| 947 | - | return; |
|
| 971 | + | return out; |
|
| 948 | 972 | }; |
|
| 949 | 973 | let mut at = self.root.parent(); |
|
| 950 | 974 | while let Some(dir) = at { |
|
| … | 10 unchanged lines |
||
| 961 | 985 | if crate::credentials::hub_of(remote_url).as_deref() != Some(hub.as_str()) { |
|
| 962 | 986 | continue; |
|
| 963 | 987 | } |
|
| 964 | - | if options.token.is_none() { |
|
| 965 | - | options.token = cfg.get(&format!("remote.{name}.token")).cloned(); |
|
| 966 | - | } |
|
| 967 | - | if options.ca.is_none() { |
|
| 968 | - | options.ca = cfg.get(&format!("remote.{name}.ca")).map(PathBuf::from); |
|
| 969 | - | } |
|
| 970 | - | } |
|
| 971 | - | if options.token.is_some() && options.ca.is_some() { |
|
| 972 | - | return; |
|
| 988 | + | out.push(( |
|
| 989 | + | cfg.get(&format!("remote.{name}.token")).cloned(), |
|
| 990 | + | cfg.get(&format!("remote.{name}.ca")).map(PathBuf::from), |
|
| 991 | + | )); |
|
| 973 | 992 | } |
|
| 974 | 993 | at = dir.parent(); |
|
| 975 | 994 | } |
|
| 995 | + | out |
|
| 976 | 996 | } |
|
| 977 | 997 | ||
| 978 | 998 | /// Push seal attestations on their own (hub spec §7.4): a bundle with |
|
| … | 679 unchanged lines |
||
| 1658 | 1678 | })?), |
|
| 1659 | 1679 | None => None, |
|
| 1660 | 1680 | }; |
|
| 1681 | + | // Whoever signed the move, and whom a delegate acts for, travel with |
|
| 1682 | + | // it: a receiver that has never seen a change by this delegate could |
|
| 1683 | + | // not otherwise check its signature (oxygenhub#129). |
|
| 1684 | + | if attestation.is_some() { |
|
| 1685 | + | for k in [self.signing_key(), self.config.author_key] |
|
| 1686 | + | .into_iter() |
|
| 1687 | + | .flatten() |
|
| 1688 | + | { |
|
| 1689 | + | if let Ok(id) = self.identity(&k) { |
|
| 1690 | + | let body = id.encode(); |
|
| 1691 | + | if !bundle |
|
| 1692 | + | .objects |
|
| 1693 | + | .iter() |
|
| 1694 | + | .any(|(t, b)| *t == ObjectType::Identity && *b == body) |
|
| 1695 | + | { |
|
| 1696 | + | bundle.objects.push((ObjectType::Identity, body)); |
|
| 1697 | + | } |
|
| 1698 | + | } |
|
| 1699 | + | } |
|
| 1700 | + | } |
|
| 1661 | 1701 | let serial = attestation.as_ref().and_then(|a| match a.attest.body { |
|
| 1662 | 1702 | AttestBody::Ref { serial, .. } => Some(serial), |
|
| 1663 | 1703 | _ => None, |
|
| … | 239 unchanged lines |
modified crates/oxy-server/tests/http.rs +60 −0
| … | 689 unchanged lines |
||
| 690 | 690 | fs::set_permissions(&attest, fs::Permissions::from_mode(0o755)).unwrap(); |
|
| 691 | 691 | } |
|
| 692 | 692 | } |
|
| 693 | + | ||
| 694 | + | /// oxygen#112: a subrepo checkout whose stored login for the hub is stale |
|
| 695 | + | /// pushes with the token its parent checkout holds for the same hub, once |
|
| 696 | + | /// the stale one is refused; a token the subrepo sets itself is never |
|
| 697 | + | /// swapped for another. |
|
| 698 | + | #[test] |
|
| 699 | + | fn a_refused_stored_token_falls_back_to_the_parents() { |
|
| 700 | + | hermetic(); |
|
| 701 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 702 | + | let root = dir.path(); |
|
| 703 | + | let served = root.join("served"); |
|
| 704 | + | Repo::init(&served.join("parent")).unwrap(); |
|
| 705 | + | Repo::init(&served.join("child")).unwrap(); |
|
| 706 | + | let base = start(&served, vec!["good".into()]); |
|
| 707 | + | ||
| 708 | + | let work = root.join("work"); |
|
| 709 | + | let mut parent = Repo::clone(&format!("{base}/parent"), &work).unwrap(); |
|
| 710 | + | parent.config.set("remote.origin.token", "good").unwrap(); |
|
| 711 | + | parent.save_config().unwrap(); |
|
| 712 | + | // The subrepo's own home holds a stale login for this hub. |
|
| 713 | + | let home = root.join("home"); |
|
| 714 | + | oxy_core::credentials::Credentials::at(&home) |
|
| 715 | + | .save(&base, "stale", Some("andy"), None) |
|
| 716 | + | .unwrap(); |
|
| 717 | + | let nested = work.join("deps").join("child"); |
|
| 718 | + | let mut child = Repo::clone(&format!("{base}/child"), &nested) |
|
| 719 | + | .unwrap() |
|
| 720 | + | .with_home(&home); |
|
| 721 | + | let options = child.options_for_url(&format!("{base}/child")); |
|
| 722 | + | assert_eq!( |
|
| 723 | + | options.token.as_deref(), |
|
| 724 | + | Some("stale"), |
|
| 725 | + | "the stored login comes first" |
|
| 726 | + | ); |
|
| 727 | + | assert_eq!( |
|
| 728 | + | options.fallback_tokens, |
|
| 729 | + | vec!["good".to_string()], |
|
| 730 | + | "the parent's, to fall back on" |
|
| 731 | + | ); |
|
| 732 | + | write(&nested, "c.txt", b"child\n"); |
|
| 733 | + | child.commit("child work", &[]).unwrap(); |
|
| 734 | + | let pushed = child.push("origin", false).unwrap(); |
|
| 735 | + | assert!(pushed[0].ok, "{}", pushed[0].reason); |
|
| 736 | + | assert_eq!( |
|
| 737 | + | Repo::open(&served.join("child")) |
|
| 738 | + | .unwrap() |
|
| 739 | + | .bookmark_heads() |
|
| 740 | + | .unwrap() |
|
| 741 | + | .len(), |
|
| 742 | + | 1 |
|
| 743 | + | ); |
|
| 744 | + | ||
| 745 | + | // Its own token, if wrong, stays wrong: what a checkout sets is its choice. |
|
| 746 | + | child.config.set("remote.origin.token", "wrong").unwrap(); |
|
| 747 | + | child.save_config().unwrap(); |
|
| 748 | + | write(&nested, "d.txt", b"more\n"); |
|
| 749 | + | child.commit("more", &[]).unwrap(); |
|
| 750 | + | let err = child.push("origin", false).err().unwrap().to_string(); |
|
| 751 | + | assert!(err.contains("401"), "{err}"); |
|
| 752 | + | } |
modified docs/guide.md +5 −1
| … | 364 unchanged lines |
||
| 365 | 365 | ||
| 366 | 366 | A token is found in this order: the remote's own `remote.<name>.token`, then |
|
| 367 | 367 | `$OXY_TOKEN` (for CI), then the store, then a remote on the same hub in an |
|
| 368 | - | enclosing checkout (so a subrepo uses its parent's). oxy sends a token over |
|
| 368 | + | enclosing checkout (so a subrepo uses its parent's). When the token came |
|
| 369 | + | from the store or an enclosing checkout and the hub refuses it (a stale login, |
|
| 370 | + | or another account's), oxy tries the other tokens enclosing checkouts hold for |
|
| 371 | + | that hub before giving up, and says so once; a remote's own token and |
|
| 372 | + | `$OXY_TOKEN` are never swapped for another. oxy sends a token over |
|
| 369 | 373 | `https` only, or over plain `http` to this machine; it is never sent to |
|
| 370 | 374 | another host after a redirect, and a credentials file that others can read |
|
| 371 | 375 | is ignored. |
|
| … | 246 unchanged lines |
modified docs/spec.md +1 −1
| … | 760 unchanged lines |
||
| 761 | 761 | | Message | Encoding | |
|
| 762 | 762 | |---|---| |
|
| 763 | 763 | | Signed attestation | `[attest bytes, [Signature]]` | |
|
| 764 | - | | `Bundle` | `{0: [[change bytes, [Signature]]], 1: [signed attestation], 2: [[type name, object bytes]], 3: [signed policy]}`; changes in dependency order; objects are chunks, blobs, identities, and states only, and include the text blobs of every change in the bundle and their chunks, so a bundle can be read on its own; key 3 (optional) holds the policies between the receiver's serial and the one sent with a push, ascending | |
|
| 764 | + | | `Bundle` | `{0: [[change bytes, [Signature]]], 1: [signed attestation], 2: [[type name, object bytes]], 3: [signed policy]}`; changes in dependency order; objects are chunks, blobs, identities, and states only, and include the text blobs of every change in the bundle and their chunks, so a bundle can be read on its own; a push that moves a bookmark also carries the identity that signed the move and, for a delegate, the identity it acts for, so a receiver that has seen no change by that key can check it (oxygenhub#129); key 3 (optional) holds the policies between the receiver's serial and the one sent with a push, ascending | |
|
| 765 | 765 | | `capabilities` response | `{0: [versions], 1: repo id bytes(16), 2: [feature names]}` | |
|
| 766 | 766 | | Signed policy | `[policy bytes, [Signature]]` | |
|
| 767 | 767 | | `refs` response | `{0: [{0: name, 1: state ref, 2: [heads], 3: signed attestation or null, 4: last serial or null}], 1: repo id, 2: signed policy or null, 3: [tag], 4: [signed policy]}`; key 4 (optional) lists earlier policies ascending by serial; key 5 (optional, text or null) names the default bookmark, which a clone checks out; the attestation is present only when it describes the current state; a tag is `{0: name, 1: state ref, 2: [heads], 3: signed attestation or null}` | |
|
| … | 283 unchanged lines |
modified src/cli/auth.rs +3 −6
| … | 169 unchanged lines |
||
| 170 | 170 | let anonymous = RemoteOptions { |
|
| 171 | 171 | token: None, |
|
| 172 | 172 | ca: ca.clone(), |
|
| 173 | - | repo_id: None, |
|
| 174 | - | cache_dir: None, |
|
| 173 | + | ..RemoteOptions::default() |
|
| 175 | 174 | }; |
|
| 176 | 175 | let token = if with_token { |
|
| 177 | 176 | let mut line = String::new(); |
|
| … | 83 unchanged lines |
||
| 261 | 260 | let options = RemoteOptions { |
|
| 262 | 261 | token: Some(token.clone()), |
|
| 263 | 262 | ca: e.ca.clone(), |
|
| 264 | - | repo_id: None, |
|
| 265 | - | cache_dir: None, |
|
| 263 | + | ..RemoteOptions::default() |
|
| 266 | 264 | }; |
|
| 267 | 265 | let check = match whoami(&e.hub, &options) { |
|
| 268 | 266 | Ok(Ok(a)) => ui::ok(format!("valid, signed in as {a}")), |
|
| … | 46 unchanged lines |
||
| 315 | 313 | let options = RemoteOptions { |
|
| 316 | 314 | token: Some(t.clone()), |
|
| 317 | 315 | ca: entry.ca.clone(), |
|
| 318 | - | repo_id: None, |
|
| 319 | - | cache_dir: None, |
|
| 316 | + | ..RemoteOptions::default() |
|
| 320 | 317 | }; |
|
| 321 | 318 | matches!( |
|
| 322 | 319 | http_json( |
|
| … | 26 unchanged lines |