systemprompt_loader/bundle/source/oci/
auth.rs1use base64::Engine;
17use serde::Deserialize;
18
19use crate::bundle::error::{BundleError, BundleResult};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct BearerChallenge {
23 pub realm: String,
24 pub service: Option<String>,
25 pub scope: Option<String>,
26}
27
28#[derive(Debug, Deserialize)]
29struct TokenResponse {
30 #[serde(default)]
31 token: Option<String>,
32
33 #[serde(default)]
34 access_token: Option<String>,
35}
36
37#[must_use]
38pub fn parse_challenge(header: &str) -> Option<BearerChallenge> {
39 let rest = header
40 .strip_prefix("Bearer ")
41 .or_else(|| header.strip_prefix("bearer "))?;
42 let mut realm = None;
43 let mut service = None;
44 let mut scope = None;
45
46 for part in rest.split(',') {
47 let (key, value) = part.trim().split_once('=')?;
48 let value = value.trim().trim_matches('"').to_owned();
49 match key.trim() {
50 "realm" => realm = Some(value),
51 "service" => service = Some(value),
52 "scope" => scope = Some(value),
53 _ => {},
54 }
55 }
56 realm.map(|realm| BearerChallenge {
57 realm,
58 service,
59 scope,
60 })
61}
62
63pub fn apply_credential(
64 builder: reqwest::RequestBuilder,
65 secret: Option<&str>,
66) -> reqwest::RequestBuilder {
67 match secret {
68 None => builder,
69 Some(raw) => match raw.split_once(':') {
70 Some((user, password)) => {
71 let encoded =
72 base64::engine::general_purpose::STANDARD.encode(format!("{user}:{password}"));
73 builder.header(reqwest::header::AUTHORIZATION, format!("Basic {encoded}"))
74 },
75 None => builder.bearer_auth(raw),
76 },
77 }
78}
79
80pub async fn fetch_token(
81 client: &reqwest::Client,
82 challenge: &BearerChallenge,
83 secret: Option<&str>,
84 source_name: &str,
85) -> BundleResult<String> {
86 let mut request = client.get(&challenge.realm);
87 if let Some(service) = challenge.service.as_ref() {
88 request = request.query(&[("service", service)]);
89 }
90 if let Some(scope) = challenge.scope.as_ref() {
91 request = request.query(&[("scope", scope)]);
92 }
93 request = apply_credential(request, secret);
94
95 let response = request
96 .send()
97 .await
98 .map_err(|e| BundleError::fetch(source_name, e))?;
99 if !response.status().is_success() {
100 return Err(BundleError::Auth {
101 source_name: source_name.to_owned(),
102 });
103 }
104
105 let body: TokenResponse = response
106 .json()
107 .await
108 .map_err(|e| BundleError::fetch(source_name, format!("token response: {e}")))?;
109 body.token
110 .or(body.access_token)
111 .ok_or_else(|| BundleError::Auth {
112 source_name: source_name.to_owned(),
113 })
114}