+20
-21
crates/tranquil-oauth/src/client.rs
+20
-21
crates/tranquil-oauth/src/client.rs
···
529
let signature_bytes = URL_SAFE_NO_PAD
530
.decode(parts[2])
531
.map_err(|_| OAuthError::InvalidClient("Invalid signature encoding".to_string()))?;
532
-
for key in matching_keys {
533
-
let key_alg = key.get("alg").and_then(|a| a.as_str());
534
-
if key_alg.is_some() && key_alg != Some(alg) {
535
-
continue;
536
-
}
537
-
let kty = key.get("kty").and_then(|k| k.as_str()).unwrap_or("");
538
-
let verified = match (alg, kty) {
539
-
("ES256", "EC") => verify_es256(key, &signing_input, &signature_bytes),
540
-
("ES384", "EC") => verify_es384(key, &signing_input, &signature_bytes),
541
-
("RS256" | "RS384" | "RS512", "RSA") => {
542
-
verify_rsa(alg, key, &signing_input, &signature_bytes)
543
}
544
-
("EdDSA", "OKP") => verify_eddsa(key, &signing_input, &signature_bytes),
545
-
_ => continue,
546
-
};
547
-
if verified.is_ok() {
548
-
return Ok(());
549
-
}
550
-
}
551
-
Err(OAuthError::InvalidClient(
552
-
"client_assertion signature verification failed".to_string(),
553
-
))
554
}
555
556
fn verify_es256(
···
529
let signature_bytes = URL_SAFE_NO_PAD
530
.decode(parts[2])
531
.map_err(|_| OAuthError::InvalidClient("Invalid signature encoding".to_string()))?;
532
+
matching_keys
533
+
.into_iter()
534
+
.filter(|key| {
535
+
let key_alg = key.get("alg").and_then(|a| a.as_str());
536
+
key_alg.is_none() || key_alg == Some(alg)
537
+
})
538
+
.find_map(|key| {
539
+
let kty = key.get("kty").and_then(|k| k.as_str()).unwrap_or("");
540
+
match (alg, kty) {
541
+
("ES256", "EC") => verify_es256(key, &signing_input, &signature_bytes).ok(),
542
+
("ES384", "EC") => verify_es384(key, &signing_input, &signature_bytes).ok(),
543
+
("RS256" | "RS384" | "RS512", "RSA") => {
544
+
verify_rsa(alg, key, &signing_input, &signature_bytes).ok()
545
+
}
546
+
("EdDSA", "OKP") => verify_eddsa(key, &signing_input, &signature_bytes).ok(),
547
+
_ => None,
548
}
549
+
})
550
+
.ok_or_else(|| {
551
+
OAuthError::InvalidClient("client_assertion signature verification failed".to_string())
552
+
})
553
}
554
555
fn verify_es256(
+6
-7
crates/tranquil-pds/src/api/identity/account.rs
+6
-7
crates/tranquil-pds/src/api/identity/account.rs
···
189
if input.handle.contains(' ') || input.handle.contains('\t') {
190
return ApiError::InvalidRequest("Handle cannot contain spaces".into()).into_response();
191
}
192
-
for c in input.handle.chars() {
193
-
if !c.is_ascii_alphanumeric() && c != '.' && c != '-' {
194
-
return ApiError::InvalidRequest(format!(
195
-
"Handle contains invalid character: {}",
196
-
c
197
-
))
198
.into_response();
199
-
}
200
}
201
let handle_lower = input.handle.to_lowercase();
202
if crate::moderation::has_explicit_slur(&handle_lower) {
···
189
if input.handle.contains(' ') || input.handle.contains('\t') {
190
return ApiError::InvalidRequest("Handle cannot contain spaces".into()).into_response();
191
}
192
+
if let Some(c) = input
193
+
.handle
194
+
.chars()
195
+
.find(|c| !c.is_ascii_alphanumeric() && *c != '.' && *c != '-')
196
+
{
197
+
return ApiError::InvalidRequest(format!("Handle contains invalid character: {}", c))
198
.into_response();
199
}
200
let handle_lower = input.handle.to_lowercase();
201
if crate::moderation::has_explicit_slur(&handle_lower) {
+11
-10
crates/tranquil-pds/src/api/identity/did.rs
+11
-10
crates/tranquil-pds/src/api/identity/did.rs
···
639
return ApiError::InvalidHandle(Some("Handle contains invalid characters".into()))
640
.into_response();
641
}
642
-
for segment in new_handle.split('.') {
643
-
if segment.is_empty() {
644
-
return ApiError::InvalidHandle(Some("Handle contains empty segment".into()))
645
-
.into_response();
646
-
}
647
-
if segment.starts_with('-') || segment.ends_with('-') {
648
-
return ApiError::InvalidHandle(Some(
649
-
"Handle segment cannot start or end with hyphen".into(),
650
-
))
651
.into_response();
652
-
}
653
}
654
if crate::moderation::has_explicit_slur(&new_handle) {
655
return ApiError::InvalidHandle(Some("Inappropriate language in handle".into()))
···
639
return ApiError::InvalidHandle(Some("Handle contains invalid characters".into()))
640
.into_response();
641
}
642
+
if new_handle.split('.').any(|segment| segment.is_empty()) {
643
+
return ApiError::InvalidHandle(Some("Handle contains empty segment".into()))
644
.into_response();
645
+
}
646
+
if new_handle
647
+
.split('.')
648
+
.any(|segment| segment.starts_with('-') || segment.ends_with('-'))
649
+
{
650
+
return ApiError::InvalidHandle(Some(
651
+
"Handle segment cannot start or end with hyphen".into(),
652
+
))
653
+
.into_response();
654
}
655
if crate::moderation::has_explicit_slur(&new_handle) {
656
return ApiError::InvalidHandle(Some("Inappropriate language in handle".into()))
+3
-1
crates/tranquil-pds/src/api/server/account_status.rs
+3
-1
crates/tranquil-pds/src/api/server/account_status.rs
+5
-5
crates/tranquil-pds/src/api/server/migration.rs
+5
-5
crates/tranquil-pds/src/api/server/migration.rs
···
115
}
116
}
117
118
-
if let Some(ref handles) = input.also_known_as {
119
-
if handles.iter().any(|h| !h.starts_with("at://")) {
120
-
return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into())
121
-
.into_response();
122
-
}
123
}
124
125
if let Some(ref endpoint) = input.service_endpoint {
···
115
}
116
}
117
118
+
if let Some(ref handles) = input.also_known_as
119
+
&& handles.iter().any(|h| !h.starts_with("at://"))
120
+
{
121
+
return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into())
122
+
.into_response();
123
}
124
125
if let Some(ref endpoint) = input.service_endpoint {
+4
-4
crates/tranquil-pds/src/api/server/session.rs
+4
-4
crates/tranquil-pds/src/api/server/session.rs
···
949
}
950
};
951
952
-
let jwt_sessions = jwt_rows.into_iter().map(|(id, access_jti, created_at, expires_at)| {
953
-
SessionInfo {
954
id: format!("jwt:{}", id),
955
session_type: "legacy".to_string(),
956
client_name: None,
957
created_at: created_at.to_rfc3339(),
958
expires_at: expires_at.to_rfc3339(),
959
is_current: current_jti.as_ref() == Some(&access_jti),
960
-
}
961
-
});
962
963
let is_oauth = auth.0.is_oauth;
964
let oauth_sessions =
···
949
}
950
};
951
952
+
let jwt_sessions = jwt_rows
953
+
.into_iter()
954
+
.map(|(id, access_jti, created_at, expires_at)| SessionInfo {
955
id: format!("jwt:{}", id),
956
session_type: "legacy".to_string(),
957
client_name: None,
958
created_at: created_at.to_rfc3339(),
959
expires_at: expires_at.to_rfc3339(),
960
is_current: current_jti.as_ref() == Some(&access_jti),
961
+
});
962
963
let is_oauth = auth.0.is_oauth;
964
let oauth_sessions =
+4
-2
crates/tranquil-pds/src/api/server/totp.rs
+4
-2
crates/tranquil-pds/src/api/server/totp.rs
···
195
return ApiError::InternalError(None).into_response();
196
}
197
198
-
let backup_hashes: Result<Vec<_>, _> = backup_codes.iter().map(|c| hash_backup_code(c)).collect();
199
let backup_hashes = match backup_hashes {
200
Ok(hashes) => hashes,
201
Err(e) => {
···
484
return ApiError::InternalError(None).into_response();
485
}
486
487
-
let backup_hashes: Result<Vec<_>, _> = backup_codes.iter().map(|c| hash_backup_code(c)).collect();
488
let backup_hashes = match backup_hashes {
489
Ok(hashes) => hashes,
490
Err(e) => {
···
195
return ApiError::InternalError(None).into_response();
196
}
197
198
+
let backup_hashes: Result<Vec<_>, _> =
199
+
backup_codes.iter().map(|c| hash_backup_code(c)).collect();
200
let backup_hashes = match backup_hashes {
201
Ok(hashes) => hashes,
202
Err(e) => {
···
485
return ApiError::InternalError(None).into_response();
486
}
487
488
+
let backup_hashes: Result<Vec<_>, _> =
489
+
backup_codes.iter().map(|c| hash_backup_code(c)).collect();
490
let backup_hashes = match backup_hashes {
491
Ok(hashes) => hashes,
492
Err(e) => {
+8
-9
crates/tranquil-pds/src/auth/verification_token.rs
+8
-9
crates/tranquil-pds/src/auth/verification_token.rs
···
296
}
297
298
pub fn format_token_for_display(token: &str) -> String {
299
-
let clean = token.replace(['-', ' '], "");
300
-
let mut result = String::new();
301
-
for (i, c) in clean.chars().enumerate() {
302
-
if i > 0 && i % 4 == 0 {
303
-
result.push('-');
304
-
}
305
-
result.push(c);
306
-
}
307
-
result
308
}
309
310
pub fn normalize_token_input(input: &str) -> String {
···
296
}
297
298
pub fn format_token_for_display(token: &str) -> String {
299
+
token
300
+
.replace(['-', ' '], "")
301
+
.chars()
302
+
.collect::<Vec<_>>()
303
+
.chunks(4)
304
+
.map(|chunk| chunk.iter().collect::<String>())
305
+
.collect::<Vec<_>>()
306
+
.join("-")
307
}
308
309
pub fn normalize_token_input(input: &str) -> String {
+3
-7
crates/tranquil-pds/src/oauth/db/scope_preference.rs
+3
-7
crates/tranquil-pds/src/oauth/db/scope_preference.rs
···
75
let stored_scopes: std::collections::HashSet<&str> =
76
stored_prefs.iter().map(|p| p.scope.as_str()).collect();
77
78
-
for scope in requested_scopes {
79
-
if !stored_scopes.contains(scope.as_str()) {
80
-
return Ok(true);
81
-
}
82
-
}
83
-
84
-
Ok(false)
85
}
86
87
pub async fn delete_scope_preferences(
+20
-20
crates/tranquil-pds/src/oauth/db/token.rs
+20
-20
crates/tranquil-pds/src/oauth/db/token.rs
···
315
)
316
.fetch_all(pool)
317
.await?;
318
-
let mut tokens = Vec::with_capacity(rows.len());
319
-
for r in rows {
320
-
tokens.push(TokenData {
321
-
did: r.did,
322
-
token_id: r.token_id,
323
-
created_at: r.created_at,
324
-
updated_at: r.updated_at,
325
-
expires_at: r.expires_at,
326
-
client_id: r.client_id,
327
-
client_auth: from_json(r.client_auth)?,
328
-
device_id: r.device_id,
329
-
parameters: from_json(r.parameters)?,
330
-
details: r.details,
331
-
code: r.code,
332
-
current_refresh_token: r.current_refresh_token,
333
-
scope: r.scope,
334
-
controller_did: r.controller_did,
335
-
});
336
-
}
337
-
Ok(tokens)
338
}
339
340
pub async fn count_tokens_for_user(pool: &PgPool, did: &str) -> Result<i64, OAuthError> {
···
315
)
316
.fetch_all(pool)
317
.await?;
318
+
rows.into_iter()
319
+
.map(|r| {
320
+
Ok(TokenData {
321
+
did: r.did,
322
+
token_id: r.token_id,
323
+
created_at: r.created_at,
324
+
updated_at: r.updated_at,
325
+
expires_at: r.expires_at,
326
+
client_id: r.client_id,
327
+
client_auth: from_json(r.client_auth)?,
328
+
device_id: r.device_id,
329
+
parameters: from_json(r.parameters)?,
330
+
details: r.details,
331
+
code: r.code,
332
+
current_refresh_token: r.current_refresh_token,
333
+
scope: r.scope,
334
+
controller_did: r.controller_did,
335
+
})
336
+
})
337
+
.collect()
338
}
339
340
pub async fn count_tokens_for_user(pool: &PgPool, did: &str) -> Result<i64, OAuthError> {
+34
-32
crates/tranquil-pds/src/oauth/endpoints/par.rs
+34
-32
crates/tranquil-pds/src/oauth/endpoints/par.rs
···
182
if requested_scopes.is_empty() {
183
return Ok(Some("atproto".to_string()));
184
}
185
-
let mut has_transition = false;
186
-
let mut has_granular = false;
187
188
-
for scope in &requested_scopes {
189
-
let parsed = parse_scope(scope);
190
-
match &parsed {
191
-
ParsedScope::Unknown(_) => {
192
-
return Err(OAuthError::InvalidScope(format!(
193
-
"Unsupported scope: {}",
194
-
scope
195
-
)));
196
-
}
197
ParsedScope::TransitionGeneric
198
-
| ParsedScope::TransitionChat
199
-
| ParsedScope::TransitionEmail => {
200
-
has_transition = true;
201
-
}
202
ParsedScope::Repo(_)
203
-
| ParsedScope::Blob(_)
204
-
| ParsedScope::Rpc(_)
205
-
| ParsedScope::Account(_)
206
-
| ParsedScope::Identity(_)
207
-
| ParsedScope::Include(_) => {
208
-
has_granular = true;
209
-
}
210
-
ParsedScope::Atproto => {}
211
-
}
212
-
}
213
214
if has_transition && has_granular {
215
return Err(OAuthError::InvalidScope(
···
219
220
if let Some(client_scope) = &client_metadata.scope {
221
let client_scopes: Vec<&str> = client_scope.split_whitespace().collect();
222
-
for scope in &requested_scopes {
223
-
if !client_scopes.iter().any(|cs| scope_matches(cs, scope)) {
224
-
return Err(OAuthError::InvalidScope(format!(
225
-
"Scope '{}' not registered for this client",
226
-
scope
227
-
)));
228
-
}
229
}
230
}
231
Ok(Some(requested_scopes.join(" ")))
···
182
if requested_scopes.is_empty() {
183
return Ok(Some("atproto".to_string()));
184
}
185
+
if let Some(unknown) = requested_scopes
186
+
.iter()
187
+
.find(|s| matches!(parse_scope(s), ParsedScope::Unknown(_)))
188
+
{
189
+
return Err(OAuthError::InvalidScope(format!(
190
+
"Unsupported scope: {}",
191
+
unknown
192
+
)));
193
+
}
194
195
+
let has_transition = requested_scopes.iter().any(|s| {
196
+
matches!(
197
+
parse_scope(s),
198
ParsedScope::TransitionGeneric
199
+
| ParsedScope::TransitionChat
200
+
| ParsedScope::TransitionEmail
201
+
)
202
+
});
203
+
let has_granular = requested_scopes.iter().any(|s| {
204
+
matches!(
205
+
parse_scope(s),
206
ParsedScope::Repo(_)
207
+
| ParsedScope::Blob(_)
208
+
| ParsedScope::Rpc(_)
209
+
| ParsedScope::Account(_)
210
+
| ParsedScope::Identity(_)
211
+
| ParsedScope::Include(_)
212
+
)
213
+
});
214
215
if has_transition && has_granular {
216
return Err(OAuthError::InvalidScope(
···
220
221
if let Some(client_scope) = &client_metadata.scope {
222
let client_scopes: Vec<&str> = client_scope.split_whitespace().collect();
223
+
if let Some(unregistered) = requested_scopes
224
+
.iter()
225
+
.find(|scope| !client_scopes.iter().any(|cs| scope_matches(cs, scope)))
226
+
{
227
+
return Err(OAuthError::InvalidScope(format!(
228
+
"Scope '{}' not registered for this client",
229
+
unregistered
230
+
)));
231
}
232
}
233
Ok(Some(requested_scopes.join(" ")))
+1
-7
crates/tranquil-pds/src/oauth/endpoints/token/grants.rs
+1
-7
crates/tranquil-pds/src/oauth/endpoints/token/grants.rs
···
334
REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL
335
};
336
let new_expires_at = Utc::now() + Duration::days(refresh_expiry_days);
337
-
db::rotate_token(
338
-
&state.db,
339
-
db_id,
340
-
&new_refresh_token.0,
341
-
new_expires_at,
342
-
)
343
-
.await?;
344
tracing::info!(
345
did = %token_data.did,
346
new_expires_at = %new_expires_at,
···
334
REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL
335
};
336
let new_expires_at = Utc::now() + Duration::days(refresh_expiry_days);
337
+
db::rotate_token(&state.db, db_id, &new_refresh_token.0, new_expires_at).await?;
338
tracing::info!(
339
did = %token_data.did,
340
new_expires_at = %new_expires_at,
+13
-5
crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs
+13
-5
crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs
···
11
12
pub struct TokenClaims {
13
pub jti: String,
14
pub exp: i64,
15
pub iat: i64,
16
}
···
33
}
34
35
pub fn create_access_token(
36
-
token_id: &str,
37
sub: &str,
38
dpop_jkt: Option<&str>,
39
scope: Option<&str>,
40
) -> Result<String, OAuthError> {
41
-
create_access_token_with_delegation(token_id, sub, dpop_jkt, scope, None)
42
}
43
44
pub fn create_access_token_with_delegation(
45
-
token_id: &str,
46
sub: &str,
47
dpop_jkt: Option<&str>,
48
scope: Option<&str>,
49
controller_did: Option<&str>,
50
) -> Result<String, OAuthError> {
51
use serde_json::json;
52
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
53
let issuer = format!("https://{}", pds_hostname);
54
let now = Utc::now().timestamp();
···
60
"aud": issuer,
61
"iat": now,
62
"exp": exp,
63
-
"jti": token_id,
64
"scope": actual_scope
65
});
66
if let Some(jkt) = dpop_jkt {
···
132
.and_then(|j| j.as_str())
133
.ok_or_else(|| OAuthError::InvalidToken("Missing jti claim".to_string()))?
134
.to_string();
135
let exp = payload
136
.get("exp")
137
.and_then(|e| e.as_i64())
···
140
.get("iat")
141
.and_then(|i| i.as_i64())
142
.ok_or_else(|| OAuthError::InvalidToken("Missing iat claim".to_string()))?;
143
-
Ok(TokenClaims { jti, exp, iat })
144
}
···
11
12
pub struct TokenClaims {
13
pub jti: String,
14
+
pub sid: String,
15
pub exp: i64,
16
pub iat: i64,
17
}
···
34
}
35
36
pub fn create_access_token(
37
+
session_id: &str,
38
sub: &str,
39
dpop_jkt: Option<&str>,
40
scope: Option<&str>,
41
) -> Result<String, OAuthError> {
42
+
create_access_token_with_delegation(session_id, sub, dpop_jkt, scope, None)
43
}
44
45
pub fn create_access_token_with_delegation(
46
+
session_id: &str,
47
sub: &str,
48
dpop_jkt: Option<&str>,
49
scope: Option<&str>,
50
controller_did: Option<&str>,
51
) -> Result<String, OAuthError> {
52
use serde_json::json;
53
+
let jti = uuid::Uuid::new_v4().to_string();
54
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
55
let issuer = format!("https://{}", pds_hostname);
56
let now = Utc::now().timestamp();
···
62
"aud": issuer,
63
"iat": now,
64
"exp": exp,
65
+
"jti": jti,
66
+
"sid": session_id,
67
"scope": actual_scope
68
});
69
if let Some(jkt) = dpop_jkt {
···
135
.and_then(|j| j.as_str())
136
.ok_or_else(|| OAuthError::InvalidToken("Missing jti claim".to_string()))?
137
.to_string();
138
+
let sid = payload
139
+
.get("sid")
140
+
.and_then(|s| s.as_str())
141
+
.ok_or_else(|| OAuthError::InvalidToken("Missing sid claim".to_string()))?
142
+
.to_string();
143
let exp = payload
144
.get("exp")
145
.and_then(|e| e.as_i64())
···
148
.get("iat")
149
.and_then(|i| i.as_i64())
150
.ok_or_else(|| OAuthError::InvalidToken("Missing iat claim".to_string()))?;
151
+
Ok(TokenClaims { jti, sid, exp, iat })
152
}
+1
-1
crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs
+1
-1
crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs
+2
-2
crates/tranquil-pds/src/oauth/verify.rs
+2
-2
crates/tranquil-pds/src/oauth/verify.rs
+2
-6
crates/tranquil-pds/src/sync/deprecated.rs
+2
-6
crates/tranquil-pds/src/sync/deprecated.rs
+2
-6
crates/tranquil-pds/src/sync/import.rs
+2
-6
crates/tranquil-pds/src/sync/import.rs
+22
-20
crates/tranquil-pds/src/sync/repo.rs
+22
-20
crates/tranquil-pds/src/sync/repo.rs
···
181
}
182
};
183
184
-
let mut block_cids: Vec<Cid> = Vec::new();
185
-
for event in &events {
186
-
if let Some(cids) = &event.blocks_cids {
187
-
for cid_str in cids {
188
-
if let Ok(cid) = Cid::from_str(cid_str)
189
-
&& !block_cids.contains(&cid)
190
-
{
191
-
block_cids.push(cid);
192
-
}
193
}
194
-
}
195
-
if let Some(commit_cid_str) = &event.commit_cid
196
-
&& let Ok(cid) = Cid::from_str(commit_cid_str)
197
-
&& !block_cids.contains(&cid)
198
-
{
199
-
block_cids.push(cid);
200
-
}
201
-
}
202
203
let mut car_bytes = match encode_car_header(head_cid) {
204
Ok(h) => h,
···
334
car.extend_from_slice(&writer);
335
};
336
write_block(&mut car_bytes, &commit_cid, &commit_bytes);
337
-
for (cid, data) in &proof_blocks {
338
-
write_block(&mut car_bytes, cid, data);
339
-
}
340
write_block(&mut car_bytes, &record_cid, &record_block);
341
(
342
StatusCode::OK,
···
181
}
182
};
183
184
+
let block_cids: Vec<Cid> = events
185
+
.iter()
186
+
.flat_map(|event| {
187
+
let block_cids = event
188
+
.blocks_cids
189
+
.as_ref()
190
+
.map(|cids| cids.iter().filter_map(|s| Cid::from_str(s).ok()).collect())
191
+
.unwrap_or_else(Vec::new);
192
+
let commit_cid = event
193
+
.commit_cid
194
+
.as_ref()
195
+
.and_then(|s| Cid::from_str(s).ok());
196
+
block_cids.into_iter().chain(commit_cid)
197
+
})
198
+
.fold(Vec::new(), |mut acc, cid| {
199
+
if !acc.contains(&cid) {
200
+
acc.push(cid);
201
}
202
+
acc
203
+
});
204
205
let mut car_bytes = match encode_car_header(head_cid) {
206
Ok(h) => h,
···
336
car.extend_from_slice(&writer);
337
};
338
write_block(&mut car_bytes, &commit_cid, &commit_bytes);
339
+
proof_blocks
340
+
.iter()
341
+
.for_each(|(cid, data)| write_block(&mut car_bytes, cid, data));
342
write_block(&mut car_bytes, &record_cid, &record_block);
343
(
344
StatusCode::OK,
+43
-57
crates/tranquil-pds/src/sync/util.rs
+43
-57
crates/tranquil-pds/src/sync/util.rs
···
210
let mut buffer = Cursor::new(Vec::new());
211
let header = CarHeader::new_v1(vec![commit_cid]);
212
let mut writer = CarWriter::new(header, &mut buffer);
213
-
for (cid, data) in other_blocks {
214
-
if cid != commit_cid {
215
-
writer
216
-
.write(cid, data.as_ref())
217
-
.await
218
-
.map_err(|e| anyhow::anyhow!("writing block {}: {}", cid, e))?;
219
-
}
220
}
221
if let Some(data) = commit_bytes {
222
writer
···
360
}
361
let car_bytes = if !all_cids.is_empty() {
362
let fetched = state.block_store.get_many(&all_cids).await?;
363
-
let mut blocks = std::collections::BTreeMap::new();
364
-
let mut commit_bytes: Option<Bytes> = None;
365
-
for (cid, data_opt) in all_cids.iter().zip(fetched.iter()) {
366
-
if let Some(data) = data_opt {
367
-
if *cid == commit_cid {
368
-
commit_bytes = Some(data.clone());
369
-
if let Some(rev) = extract_rev_from_commit_bytes(data) {
370
-
frame.rev = rev;
371
-
}
372
-
} else {
373
-
blocks.insert(*cid, data.clone());
374
-
}
375
-
}
376
}
377
write_car_blocks(commit_cid, commit_bytes, blocks).await?
378
} else {
379
Vec::new()
···
393
state: &AppState,
394
events: &[SequencedEvent],
395
) -> Result<HashMap<Cid, Bytes>, anyhow::Error> {
396
-
let mut all_cids: Vec<Cid> = Vec::new();
397
-
for event in events {
398
-
if let Some(ref commit_cid_str) = event.commit_cid
399
-
&& let Ok(cid) = Cid::from_str(commit_cid_str)
400
-
{
401
-
all_cids.push(cid);
402
-
}
403
-
if let Some(ref prev_cid_str) = event.prev_cid
404
-
&& let Ok(cid) = Cid::from_str(prev_cid_str)
405
-
{
406
-
all_cids.push(cid);
407
-
}
408
-
if let Some(ref block_cids_str) = event.blocks_cids {
409
-
for s in block_cids_str {
410
-
if let Ok(cid) = Cid::from_str(s) {
411
-
all_cids.push(cid);
412
-
}
413
-
}
414
-
}
415
-
}
416
all_cids.sort();
417
all_cids.dedup();
418
if all_cids.is_empty() {
419
return Ok(HashMap::new());
420
}
421
let fetched = state.block_store.get_many(&all_cids).await?;
422
-
let mut blocks_map = HashMap::with_capacity(all_cids.len());
423
-
for (cid, data_opt) in all_cids.into_iter().zip(fetched.into_iter()) {
424
-
if let Some(data) = data_opt {
425
-
blocks_map.insert(cid, data);
426
-
}
427
-
}
428
Ok(blocks_map)
429
}
430
···
511
frame.since = Some(rev);
512
}
513
let car_bytes = if !all_cids.is_empty() {
514
-
let mut blocks = BTreeMap::new();
515
-
let mut commit_bytes_for_car: Option<Bytes> = None;
516
-
for cid in all_cids {
517
-
if let Some(data) = prefetched.get(&cid) {
518
-
if cid == commit_cid {
519
-
commit_bytes_for_car = Some(data.clone());
520
-
} else {
521
-
blocks.insert(cid, data.clone());
522
-
}
523
-
}
524
-
}
525
write_car_blocks(commit_cid, commit_bytes_for_car, blocks).await?
526
} else {
527
Vec::new()
···
210
let mut buffer = Cursor::new(Vec::new());
211
let header = CarHeader::new_v1(vec![commit_cid]);
212
let mut writer = CarWriter::new(header, &mut buffer);
213
+
for (cid, data) in other_blocks.iter().filter(|(c, _)| **c != commit_cid) {
214
+
writer
215
+
.write(*cid, data.as_ref())
216
+
.await
217
+
.map_err(|e| anyhow::anyhow!("writing block {}: {}", cid, e))?;
218
}
219
if let Some(data) = commit_bytes {
220
writer
···
358
}
359
let car_bytes = if !all_cids.is_empty() {
360
let fetched = state.block_store.get_many(&all_cids).await?;
361
+
let (commit_data, other_blocks): (Vec<_>, Vec<_>) = all_cids
362
+
.iter()
363
+
.zip(fetched.iter())
364
+
.filter_map(|(cid, data_opt)| data_opt.as_ref().map(|data| (*cid, data.clone())))
365
+
.partition(|(cid, _)| *cid == commit_cid);
366
+
let commit_bytes = commit_data.into_iter().next().map(|(_, data)| data);
367
+
if let Some(ref cb) = commit_bytes
368
+
&& let Some(rev) = extract_rev_from_commit_bytes(cb)
369
+
{
370
+
frame.rev = rev;
371
}
372
+
let blocks: std::collections::BTreeMap<Cid, Bytes> = other_blocks.into_iter().collect();
373
write_car_blocks(commit_cid, commit_bytes, blocks).await?
374
} else {
375
Vec::new()
···
389
state: &AppState,
390
events: &[SequencedEvent],
391
) -> Result<HashMap<Cid, Bytes>, anyhow::Error> {
392
+
let mut all_cids: Vec<Cid> = events
393
+
.iter()
394
+
.flat_map(|event| {
395
+
let commit_cid = event
396
+
.commit_cid
397
+
.as_ref()
398
+
.and_then(|s| Cid::from_str(s).ok());
399
+
let prev_cid = event.prev_cid.as_ref().and_then(|s| Cid::from_str(s).ok());
400
+
let block_cids = event
401
+
.blocks_cids
402
+
.as_ref()
403
+
.map(|cids| cids.iter().filter_map(|s| Cid::from_str(s).ok()).collect())
404
+
.unwrap_or_else(Vec::new);
405
+
commit_cid.into_iter().chain(prev_cid).chain(block_cids)
406
+
})
407
+
.collect();
408
all_cids.sort();
409
all_cids.dedup();
410
if all_cids.is_empty() {
411
return Ok(HashMap::new());
412
}
413
let fetched = state.block_store.get_many(&all_cids).await?;
414
+
let blocks_map: HashMap<Cid, Bytes> = all_cids
415
+
.into_iter()
416
+
.zip(fetched)
417
+
.filter_map(|(cid, data_opt)| data_opt.map(|data| (cid, data)))
418
+
.collect();
419
Ok(blocks_map)
420
}
421
···
502
frame.since = Some(rev);
503
}
504
let car_bytes = if !all_cids.is_empty() {
505
+
let (commit_data, other_blocks): (Vec<_>, Vec<_>) = all_cids
506
+
.into_iter()
507
+
.filter_map(|cid| prefetched.get(&cid).map(|data| (cid, data.clone())))
508
+
.partition(|(cid, _)| *cid == commit_cid);
509
+
let commit_bytes_for_car = commit_data.into_iter().next().map(|(_, data)| data);
510
+
let blocks: BTreeMap<Cid, Bytes> = other_blocks.into_iter().collect();
511
write_car_blocks(commit_cid, commit_bytes_for_car, blocks).await?
512
} else {
513
Vec::new()
+14
-16
crates/tranquil-pds/tests/common/mod.rs
+14
-16
crates/tranquil-pds/tests/common/mod.rs
···
256
.unwrap_or_default()
257
.to_string();
258
259
-
if let Ok(body) = serde_json::from_slice::<Value>(request.body.as_slice()) {
260
-
if let Ok(mut store) = self.store.write() {
261
-
store.insert(did, body);
262
-
}
263
}
264
ResponseTemplate::new(200)
265
}
···
298
299
match endpoint {
300
"/log/last" => {
301
-
let response = operation
302
-
.cloned()
303
-
.unwrap_or_else(|| {
304
-
json!({
305
-
"type": "plc_operation",
306
-
"rotationKeys": [],
307
-
"verificationMethods": {},
308
-
"alsoKnownAs": [],
309
-
"services": {},
310
-
"prev": null
311
-
})
312
-
});
313
ResponseTemplate::new(200).set_body_json(response)
314
}
315
"/log/audit" => ResponseTemplate::new(200).set_body_json(json!([])),
···
256
.unwrap_or_default()
257
.to_string();
258
259
+
if let Ok(body) = serde_json::from_slice::<Value>(request.body.as_slice())
260
+
&& let Ok(mut store) = self.store.write()
261
+
{
262
+
store.insert(did, body);
263
}
264
ResponseTemplate::new(200)
265
}
···
298
299
match endpoint {
300
"/log/last" => {
301
+
let response = operation.cloned().unwrap_or_else(|| {
302
+
json!({
303
+
"type": "plc_operation",
304
+
"rotationKeys": [],
305
+
"verificationMethods": {},
306
+
"alsoKnownAs": [],
307
+
"services": {},
308
+
"prev": null
309
+
})
310
+
});
311
ResponseTemplate::new(200).set_body_json(response)
312
}
313
"/log/audit" => ResponseTemplate::new(200).set_body_json(json!([])),
+1
-4
crates/tranquil-pds/tests/import_verification.rs
+1
-4
crates/tranquil-pds/tests/import_verification.rs