Skip to content

Commit 39f0c08

Browse files
committed
Merge branch 'km/cose' of github.com:bitwarden/sdk-internal into km/cose
2 parents 077e18f + 8a72734 commit 39f0c08

File tree

22 files changed

+499
-285
lines changed

22 files changed

+499
-285
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bitwarden_license/bitwarden-sm/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ keywords.workspace = true
1515

1616
[dependencies]
1717
bitwarden-api-api = { workspace = true }
18-
bitwarden-core = { workspace = true }
18+
bitwarden-core = { workspace = true, features = ["secrets"] }
1919
bitwarden-crypto = { workspace = true }
2020
chrono = { workspace = true }
2121
log = { workspace = true }
@@ -28,6 +28,7 @@ validator = { workspace = true }
2828

2929
[dev-dependencies]
3030
tokio = { workspace = true, features = ["rt"] }
31+
wiremock = "0.6.0"
3132

3233
[lints]
3334
workspace = true

bitwarden_license/bitwarden-sm/src/client_secrets.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,121 @@ impl SecretsClientExt for Client {
101101
SecretsClient::new(self.clone())
102102
}
103103
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
use bitwarden_core::{
108+
auth::login::AccessTokenLoginRequest, Client, ClientSettings, DeviceType,
109+
};
110+
111+
use crate::{
112+
secrets::{SecretGetRequest, SecretIdentifiersRequest},
113+
ClientSecretsExt,
114+
};
115+
116+
async fn start_mock(mocks: Vec<wiremock::Mock>) -> (wiremock::MockServer, Client) {
117+
let server = wiremock::MockServer::start().await;
118+
119+
for mock in mocks {
120+
server.register(mock).await;
121+
}
122+
123+
let settings = ClientSettings {
124+
identity_url: format!("http://{}/identity", server.address()),
125+
api_url: format!("http://{}/api", server.address()),
126+
user_agent: "Bitwarden Rust-SDK [TEST]".into(),
127+
device_type: DeviceType::SDK,
128+
};
129+
130+
(server, Client::new(Some(settings)))
131+
}
132+
133+
#[tokio::test]
134+
async fn test_access_token_login() {
135+
use wiremock::{matchers, Mock, ResponseTemplate};
136+
137+
// Create the mock server with the necessary routes for this test
138+
let (_server, client) = start_mock(vec![
139+
Mock::given(matchers::path("/identity/connect/token"))
140+
.respond_with(ResponseTemplate::new(200).set_body_json(
141+
serde_json::json!({
142+
"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQzg1REJEOUFSUzI1NiIsInR5cCI6\
143+
ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJuYmYiOjE2NzUxMDM3ODEsImV4cCI6MTY3NTEwNzM4MSwiaXNzIjo\
144+
iaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI6ImVjMmMxZDQ2LTZhNGItNDc1MS1hMzEwLWFmOTYwMTMxN2YyZCIsInN1YiI6ImQzNDgwNGNhLTR\
145+
mNmMtNDM5Mi04NmI3LWFmOTYwMTMxNzVkMCIsIm9yZ2FuaXphdGlvbiI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImp0aSI\
146+
6IjU3QUU0NzQ0MzIwNzk1RThGQkQ4MUIxNDA2RDQyNTQyIiwiaWF0IjoxNjc1MTAzNzgxLCJzY29wZSI6WyJhcGkuc2VjcmV0cyJdfQ.GRKYzqgJZHEE\
147+
ZHsJkhVZH8zjYhY3hUvM4rhdV3FU10WlCteZdKHrPIadCUh-Oz9DxIAA2HfALLhj1chL4JgwPmZgPcVS2G8gk8XeBmZXowpVWJ11TXS1gYrM9syXbv9j\
148+
0JUCdpeshH7e56WnlpVynyUwIum9hmYGZ_XJUfmGtlKLuNjYnawTwLEeR005uEjxq3qI1kti-WFnw8ciL4a6HLNulgiFw1dAvs4c7J0souShMfrnFO3g\
149+
SOHff5kKD3hBB9ynDBnJQSFYJ7dFWHIjhqs0Vj-9h0yXXCcHvu7dVGpaiNjNPxbh6YeXnY6UWcmHLDtFYsG2BWcNvVD4-VgGxXt3cMhrn7l3fSYuo32Z\
150+
Yk4Wop73XuxqF2fmfmBdZqGI1BafhENCcZw_bpPSfK2uHipfztrgYnrzwvzedz0rjFKbhDyrjzuRauX5dqVJ4ntPeT9g_I5n71gLxiP7eClyAx5RxdF6\
151+
He87NwC8i-hLBhugIvLTiDj-Sk9HvMth6zaD0ebxd56wDjq8-CMG_WcgusDqNzKFHqWNDHBXt8MLeTgZAR2rQMIMFZqFgsJlRflbig8YewmNUA9wAU74\
152+
TfxLY1foO7Xpg49vceB7C-PlvGi1VtX6F2i0tc_67lA5kWXnnKBPBUyspoIrmAUCwfms5nTTqA9xXAojMhRHAos_OdM",
153+
"expires_in":3600,
154+
"token_type":"Bearer",
155+
"scope":"api.secrets",
156+
"encrypted_payload":"2.E9fE8+M/VWMfhhim1KlCbQ==|eLsHR484S/tJbIkM6spnG/HP65tj9A6Tba7kAAvUp+rYuQmGLixiOCfMsqt5OvBctDfvvr/Aes\
157+
Bu7cZimPLyOEhqEAjn52jF0eaI38XZfeOG2VJl0LOf60Wkfh3ryAMvfvLj3G4ZCNYU8sNgoC2+IQ==|lNApuCQ4Pyakfo/wwuuajWNaEX/2MW8/3rjXB/V7n+k="})
158+
)),
159+
Mock::given(matchers::path("/api/organizations/f4e44a7f-1190-432a-9d4a-af96013127cb/secrets"))
160+
.respond_with(ResponseTemplate::new(200).set_body_json(
161+
serde_json::json!({
162+
"secrets":[{
163+
"id":"15744a66-341a-4c62-af50-af960166b6bc",
164+
"organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb",
165+
"key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=",
166+
"creationDate":"2023-01-26T21:46:02.2182556Z",
167+
"revisionDate":"2023-01-26T21:46:02.2182557Z"
168+
}],
169+
"projects":[],
170+
"object":"SecretsWithProjectsList"
171+
})
172+
)),
173+
Mock::given(matchers::path("/api/secrets/15744a66-341a-4c62-af50-af960166b6bc"))
174+
.respond_with(ResponseTemplate::new(200).set_body_json(
175+
serde_json::json!({
176+
"id":"15744a66-341a-4c62-af50-af960166b6bc",
177+
"organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb",
178+
"key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=",
179+
"value":"2.Gl34n9JYABC7V21qHcBzHg==|c1Ds244pob7i+8+MXe4++w==|Shimz/qKMYZmzSFWdeBzFb9dFz7oF6Uv9oqkws7rEe0=",
180+
"note":"2.Cn9ABJy7+WfR4uUHwdYepg==|+nbJyU/6hSknoa5dcEJEUg==|1DTp/ZbwGO3L3RN+VMsCHz8XDr8egn/M5iSitGGysPA=",
181+
"creationDate":"2023-01-26T21:46:02.2182556Z",
182+
"revisionDate":"2023-01-26T21:46:02.2182557Z",
183+
"object":"secret"
184+
})
185+
))
186+
]).await;
187+
188+
// Test the login is correct and we store the returned organization ID correctly
189+
let res = client
190+
.auth()
191+
.login_access_token(&AccessTokenLoginRequest {
192+
access_token: "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==".into(),
193+
state_file: None,
194+
})
195+
.await
196+
.unwrap();
197+
assert!(res.authenticated);
198+
199+
let organization_id = "f4e44a7f-1190-432a-9d4a-af96013127cb".try_into().unwrap();
200+
201+
// Test that we can retrieve the list of secrets correctly
202+
let mut res = client
203+
.secrets()
204+
.list(&SecretIdentifiersRequest { organization_id })
205+
.await
206+
.unwrap();
207+
assert_eq!(res.data.len(), 1);
208+
209+
// Test that given a secret ID we can get it's data
210+
let res = client
211+
.secrets()
212+
.get(&SecretGetRequest {
213+
id: res.data.remove(0).id,
214+
})
215+
.await
216+
.unwrap();
217+
assert_eq!(res.key, "TEST");
218+
assert_eq!(res.note, "TEST");
219+
assert_eq!(res.value, "TEST");
220+
}
221+
}

crates/bitwarden-core/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ rustls-platform-verifier = "0.5.0"
6262
[dev-dependencies]
6363
rand_chacha = "0.3.1"
6464
tokio = { workspace = true, features = ["rt"] }
65-
wiremock = "0.6.0"
6665
zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] }
6766

6867
[lints]

crates/bitwarden-core/src/auth/auth_client.rs

Lines changed: 5 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@ use bitwarden_crypto::{
55

66
#[cfg(feature = "secrets")]
77
use crate::auth::login::{login_access_token, AccessTokenLoginRequest, AccessTokenLoginResponse};
8-
use crate::{auth::renew::renew_token, Client};
98
#[cfg(feature = "internal")]
109
use crate::{
1110
auth::{
1211
auth_request::{approve_auth_request, new_auth_request, ApproveAuthRequestError},
1312
key_connector::{make_key_connector_keys, KeyConnectorResponse},
1413
login::{
1514
login_api_key, login_password, send_two_factor_email, ApiKeyLoginRequest,
16-
ApiKeyLoginResponse, LoginError, NewAuthRequestResponse, PasswordLoginRequest,
15+
ApiKeyLoginResponse, NewAuthRequestResponse, PasswordLoginRequest,
1716
PasswordLoginResponse, PreloginError, TwoFactorEmailError, TwoFactorEmailRequest,
1817
},
1918
password::{
@@ -28,6 +27,10 @@ use crate::{
2827
},
2928
client::encryption_settings::EncryptionSettingsError,
3029
};
30+
use crate::{
31+
auth::{login::LoginError, renew::renew_token},
32+
Client,
33+
};
3134

3235
pub struct AuthClient {
3336
pub(crate) client: crate::Client,
@@ -212,104 +215,3 @@ impl Client {
212215
}
213216
}
214217
}
215-
216-
/*
217-
#[cfg(test)]
218-
mod tests {
219-
220-
#[cfg(feature = "secrets")]
221-
#[tokio::test]
222-
async fn test_access_token_login() {
223-
use wiremock::{matchers, Mock, ResponseTemplate};
224-
225-
use crate::{auth::login::AccessTokenLoginRequest, secrets_manager::secrets::*};
226-
227-
// Create the mock server with the necessary routes for this test
228-
let (_server, client) = crate::util::start_mock(vec![
229-
Mock::given(matchers::path("/identity/connect/token"))
230-
.respond_with(ResponseTemplate::new(200).set_body_json(
231-
serde_json::json!({
232-
"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQzg1REJEOUFSUzI1NiIsInR5cCI6\
233-
ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJuYmYiOjE2NzUxMDM3ODEsImV4cCI6MTY3NTEwNzM4MSwiaXNzIjo\
234-
iaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI6ImVjMmMxZDQ2LTZhNGItNDc1MS1hMzEwLWFmOTYwMTMxN2YyZCIsInN1YiI6ImQzNDgwNGNhLTR\
235-
mNmMtNDM5Mi04NmI3LWFmOTYwMTMxNzVkMCIsIm9yZ2FuaXphdGlvbiI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImp0aSI\
236-
6IjU3QUU0NzQ0MzIwNzk1RThGQkQ4MUIxNDA2RDQyNTQyIiwiaWF0IjoxNjc1MTAzNzgxLCJzY29wZSI6WyJhcGkuc2VjcmV0cyJdfQ.GRKYzqgJZHEE\
237-
ZHsJkhVZH8zjYhY3hUvM4rhdV3FU10WlCteZdKHrPIadCUh-Oz9DxIAA2HfALLhj1chL4JgwPmZgPcVS2G8gk8XeBmZXowpVWJ11TXS1gYrM9syXbv9j\
238-
0JUCdpeshH7e56WnlpVynyUwIum9hmYGZ_XJUfmGtlKLuNjYnawTwLEeR005uEjxq3qI1kti-WFnw8ciL4a6HLNulgiFw1dAvs4c7J0souShMfrnFO3g\
239-
SOHff5kKD3hBB9ynDBnJQSFYJ7dFWHIjhqs0Vj-9h0yXXCcHvu7dVGpaiNjNPxbh6YeXnY6UWcmHLDtFYsG2BWcNvVD4-VgGxXt3cMhrn7l3fSYuo32Z\
240-
Yk4Wop73XuxqF2fmfmBdZqGI1BafhENCcZw_bpPSfK2uHipfztrgYnrzwvzedz0rjFKbhDyrjzuRauX5dqVJ4ntPeT9g_I5n71gLxiP7eClyAx5RxdF6\
241-
He87NwC8i-hLBhugIvLTiDj-Sk9HvMth6zaD0ebxd56wDjq8-CMG_WcgusDqNzKFHqWNDHBXt8MLeTgZAR2rQMIMFZqFgsJlRflbig8YewmNUA9wAU74\
242-
TfxLY1foO7Xpg49vceB7C-PlvGi1VtX6F2i0tc_67lA5kWXnnKBPBUyspoIrmAUCwfms5nTTqA9xXAojMhRHAos_OdM",
243-
"expires_in":3600,
244-
"token_type":"Bearer",
245-
"scope":"api.secrets",
246-
"encrypted_payload":"2.E9fE8+M/VWMfhhim1KlCbQ==|eLsHR484S/tJbIkM6spnG/HP65tj9A6Tba7kAAvUp+rYuQmGLixiOCfMsqt5OvBctDfvvr/Aes\
247-
Bu7cZimPLyOEhqEAjn52jF0eaI38XZfeOG2VJl0LOf60Wkfh3ryAMvfvLj3G4ZCNYU8sNgoC2+IQ==|lNApuCQ4Pyakfo/wwuuajWNaEX/2MW8/3rjXB/V7n+k="})
248-
)),
249-
Mock::given(matchers::path("/api/organizations/f4e44a7f-1190-432a-9d4a-af96013127cb/secrets"))
250-
.respond_with(ResponseTemplate::new(200).set_body_json(
251-
serde_json::json!({
252-
"secrets":[{
253-
"id":"15744a66-341a-4c62-af50-af960166b6bc",
254-
"organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb",
255-
"key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=",
256-
"creationDate":"2023-01-26T21:46:02.2182556Z",
257-
"revisionDate":"2023-01-26T21:46:02.2182557Z"
258-
}],
259-
"projects":[],
260-
"object":"SecretsWithProjectsList"
261-
})
262-
)),
263-
Mock::given(matchers::path("/api/secrets/15744a66-341a-4c62-af50-af960166b6bc"))
264-
.respond_with(ResponseTemplate::new(200).set_body_json(
265-
serde_json::json!({
266-
"id":"15744a66-341a-4c62-af50-af960166b6bc",
267-
"organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb",
268-
"key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=",
269-
"value":"2.Gl34n9JYABC7V21qHcBzHg==|c1Ds244pob7i+8+MXe4++w==|Shimz/qKMYZmzSFWdeBzFb9dFz7oF6Uv9oqkws7rEe0=",
270-
"note":"2.Cn9ABJy7+WfR4uUHwdYepg==|+nbJyU/6hSknoa5dcEJEUg==|1DTp/ZbwGO3L3RN+VMsCHz8XDr8egn/M5iSitGGysPA=",
271-
"creationDate":"2023-01-26T21:46:02.2182556Z",
272-
"revisionDate":"2023-01-26T21:46:02.2182557Z",
273-
"object":"secret"
274-
})
275-
))
276-
]).await;
277-
278-
// Test the login is correct and we store the returned organization ID correctly
279-
let res = client
280-
.auth()
281-
.login_access_token(&AccessTokenLoginRequest {
282-
access_token: "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==".into(),
283-
state_file: None,
284-
})
285-
.await
286-
.unwrap();
287-
assert!(res.authenticated);
288-
let organization_id = client.get_access_token_organization().unwrap();
289-
assert_eq!(
290-
organization_id.to_string(),
291-
"f4e44a7f-1190-432a-9d4a-af96013127cb"
292-
);
293-
294-
// Test that we can retrieve the list of secrets correctly
295-
let mut res = client
296-
.secrets()
297-
.list(&SecretIdentifiersRequest { organization_id })
298-
.await
299-
.unwrap();
300-
assert_eq!(res.data.len(), 1);
301-
302-
// Test that given a secret ID we can get it's data
303-
let res = client
304-
.secrets()
305-
.get(&SecretGetRequest {
306-
id: res.data.remove(0).id,
307-
})
308-
.await
309-
.unwrap();
310-
assert_eq!(res.key, "TEST");
311-
assert_eq!(res.note, "TEST");
312-
assert_eq!(res.value, "TEST");
313-
}
314-
}
315-
*/

crates/bitwarden-core/src/auth/login/access_token.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ pub(crate) async fn login_access_token(
9696
},
9797
));
9898

99-
client.internal.initialize_crypto_single_key(encryption_key);
99+
client
100+
.internal
101+
.initialize_crypto_single_org_key(organization_id, encryption_key);
100102
}
101103

102104
AccessTokenLoginResponse::process_response(response)
@@ -133,7 +135,9 @@ fn load_tokens_from_state(
133135
client
134136
.internal
135137
.set_tokens(client_state.token, None, time_till_expiration as u64);
136-
client.internal.initialize_crypto_single_key(encryption_key);
138+
client
139+
.internal
140+
.initialize_crypto_single_org_key(organization_id, encryption_key);
137141

138142
return Ok(organization_id);
139143
}

crates/bitwarden-core/src/auth/login/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub use prelogin::*;
77

88
#[cfg(any(feature = "internal", feature = "secrets"))]
99
mod password;
10-
#[cfg(feature = "internal")]
10+
#[cfg(any(feature = "internal", feature = "secrets"))]
1111
pub use password::*;
1212

1313
#[cfg(feature = "internal")]
@@ -49,6 +49,7 @@ pub enum LoginError {
4949
#[error("JWT token is missing email")]
5050
JwtTokenMissingEmail,
5151

52+
#[cfg(feature = "internal")]
5253
#[error(transparent)]
5354
Prelogin(#[from] PreloginError),
5455
#[error(transparent)]

crates/bitwarden-core/src/client/encryption_settings.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,19 @@ impl EncryptionSettings {
7777
Ok(())
7878
}
7979

80-
/// Initialize the encryption settings with only a single decrypted key.
80+
/// Initialize the encryption settings with only a single decrypted organization key.
8181
/// This is used only for logging in Secrets Manager with an access token
8282
#[cfg(feature = "secrets")]
83-
pub(crate) fn new_single_key(key: SymmetricCryptoKey, store: &KeyStore<KeyIds>) {
83+
pub(crate) fn new_single_org_key(
84+
organization_id: Uuid,
85+
key: SymmetricCryptoKey,
86+
store: &KeyStore<KeyIds>,
87+
) {
8488
// FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods
8589
#[allow(deprecated)]
8690
store
8791
.context_mut()
88-
.set_symmetric_key(SymmetricKeyId::User, key)
92+
.set_symmetric_key(SymmetricKeyId::Organization(organization_id), key)
8993
.expect("Mutable context");
9094
}
9195

crates/bitwarden-core/src/client/internal.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,12 @@ impl InternalClient {
208208
}
209209

210210
#[cfg(feature = "secrets")]
211-
pub(crate) fn initialize_crypto_single_key(&self, key: SymmetricCryptoKey) {
212-
EncryptionSettings::new_single_key(key, &self.key_store);
211+
pub(crate) fn initialize_crypto_single_org_key(
212+
&self,
213+
organization_id: Uuid,
214+
key: SymmetricCryptoKey,
215+
) {
216+
EncryptionSettings::new_single_org_key(organization_id, key, &self.key_store);
213217
}
214218

215219
#[cfg(feature = "internal")]

crates/bitwarden-core/src/util.rs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,3 @@ const INDIFFERENT: GeneralPurposeConfig =
1010
/// padding.
1111
pub const STANDARD_INDIFFERENT: GeneralPurpose =
1212
GeneralPurpose::new(&alphabet::STANDARD, INDIFFERENT);
13-
14-
#[allow(dead_code)]
15-
#[cfg(test)]
16-
async fn start_mock(mocks: Vec<wiremock::Mock>) -> (wiremock::MockServer, crate::Client) {
17-
let server = wiremock::MockServer::start().await;
18-
19-
for mock in mocks {
20-
server.register(mock).await;
21-
}
22-
23-
let settings = crate::ClientSettings {
24-
identity_url: format!("http://{}/identity", server.address()),
25-
api_url: format!("http://{}/api", server.address()),
26-
user_agent: "Bitwarden Rust-SDK [TEST]".into(),
27-
device_type: crate::DeviceType::SDK,
28-
};
29-
30-
(server, crate::Client::new(Some(settings)))
31-
}

0 commit comments

Comments
 (0)