app_store_connect/
api_token.rs1use {
10 crate::Result,
11 jsonwebtoken::{Algorithm, EncodingKey, Header},
12 serde::{Deserialize, Serialize},
13 std::{path::Path, time::SystemTime},
14 thiserror::Error,
15};
16
17#[derive(Clone, Debug, Deserialize, Serialize)]
18struct ConnectTokenRequest {
19 iss: String,
20 iat: u64,
21 exp: u64,
22 aud: String,
23}
24
25pub type AppStoreConnectToken = String;
27
28#[derive(Clone)]
45pub struct ConnectTokenEncoder {
46 key_id: String,
47 issuer_id: String,
48 encoding_key: EncodingKey,
49}
50
51impl ConnectTokenEncoder {
52 pub fn from_jwt_encoding_key(
56 key_id: String,
57 issuer_id: String,
58 encoding_key: EncodingKey,
59 ) -> Self {
60 Self {
61 key_id,
62 issuer_id,
63 encoding_key,
64 }
65 }
66
67 pub fn from_ecdsa_der(key_id: String, issuer_id: String, der_data: &[u8]) -> Result<Self> {
69 let encoding_key = EncodingKey::from_ec_der(der_data);
70
71 Ok(Self::from_jwt_encoding_key(key_id, issuer_id, encoding_key))
72 }
73
74 pub fn from_ecdsa_pem(key_id: String, issuer_id: String, pem_data: &[u8]) -> Result<Self> {
76 let encoding_key = EncodingKey::from_ec_pem(pem_data)?;
77
78 Ok(Self::from_jwt_encoding_key(key_id, issuer_id, encoding_key))
79 }
80
81 pub fn from_ecdsa_pem_path(
83 key_id: String,
84 issuer_id: String,
85 path: impl AsRef<Path>,
86 ) -> Result<Self> {
87 let data = std::fs::read(path.as_ref())?;
88
89 Self::from_ecdsa_pem(key_id, issuer_id, &data)
90 }
91
92 pub fn from_api_key_id(key_id: String, issuer_id: String) -> Result<Self> {
97 let mut search_paths = vec![std::env::current_dir()?.join("private_keys")];
98
99 if let Some(home) = dirs::home_dir() {
100 search_paths.extend([
101 home.join("private_keys"),
102 home.join(".private_keys"),
103 home.join(".appstoreconnect").join("private_keys"),
104 ]);
105 }
106
107 let filename = format!("AuthKey_{key_id}.p8");
109
110 for path in search_paths {
111 let candidate = path.join(filename.as_str());
112
113 if candidate.exists() {
114 return Self::from_ecdsa_pem_path(key_id, issuer_id, candidate);
115 }
116 }
117
118 Err(MissingApiKey.into())
119 }
120
121 pub fn new_token(&self, duration: u64) -> Result<AppStoreConnectToken> {
126 let header = Header {
127 kid: Some(self.key_id.clone()),
128 alg: Algorithm::ES256,
129 ..Default::default()
130 };
131
132 let now = SystemTime::now()
133 .duration_since(SystemTime::UNIX_EPOCH)
134 .expect("calculating UNIX time should never fail")
135 .as_secs();
136
137 let claims = ConnectTokenRequest {
138 iss: self.issuer_id.clone(),
139 iat: now,
140 exp: now + duration,
141 aud: "appstoreconnect-v1".to_string(),
142 };
143
144 let token = jsonwebtoken::encode(&header, &claims, &self.encoding_key)?;
145
146 Ok(token)
147 }
148}
149
150#[derive(Clone, Copy, Debug, Error)]
151#[error("no app store connect api key found")]
152pub struct MissingApiKey;