1use crate::acl::{self, Role};
9use crate::auth::AuthClaims;
10use crate::auth::session::now_epoch;
11use crate::config::AppConfig;
12use crate::error::AppError;
13use crate::mnemonic::{
14 generate_unique_mnemonic, is_path_available, validate_custom_path, validate_mnemonic,
15};
16use crate::server::AppState;
17
18use crate::store::KeyspaceHandle;
19use did_hosting_common::DidListEntry;
20use serde::{Deserialize, Serialize};
21use tracing::{debug, info, warn};
22
23pub use did_hosting_common::did_ops::{
26 DidRecord, LogEntryInfo, LogMetadata, content_log_key, content_witness_key, did_key,
27 extract_did_id, extract_did_web_document, extract_log_metadata, extract_service_types,
28 owner_key, parse_log_entries, watcher_sync_key,
29};
30
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct QuotaIndex {
38 pub did_count: u64,
39 pub total_size: u64,
40}
41
42pub fn quota_key(owner: &str) -> String {
43 format!("quota:{owner}")
44}
45
46pub async fn get_quota(dids_ks: &KeyspaceHandle, owner: &str) -> Result<QuotaIndex, AppError> {
48 Ok(dids_ks.get(quota_key(owner)).await?.unwrap_or_default())
49}
50
51pub async fn quota_on_create(dids_ks: &KeyspaceHandle, owner: &str) -> Result<(), AppError> {
53 let mut q = get_quota(dids_ks, owner).await?;
54 q.did_count += 1;
55 dids_ks.insert(quota_key(owner), &q).await
56}
57
58pub async fn quota_on_delete(
60 dids_ks: &KeyspaceHandle,
61 owner: &str,
62 content_size: u64,
63) -> Result<(), AppError> {
64 let mut q = get_quota(dids_ks, owner).await?;
65 q.did_count = q.did_count.saturating_sub(1);
66 q.total_size = q.total_size.saturating_sub(content_size);
67 dids_ks.insert(quota_key(owner), &q).await
68}
69
70pub async fn quota_on_size_change(
72 dids_ks: &KeyspaceHandle,
73 owner: &str,
74 old_size: u64,
75 new_size: u64,
76) -> Result<(), AppError> {
77 let mut q = get_quota(dids_ks, owner).await?;
78 q.total_size = q
79 .total_size
80 .saturating_sub(old_size)
81 .saturating_add(new_size);
82 dids_ks.insert(quota_key(owner), &q).await
83}
84
85pub async fn check_did_count_limit(
92 auth: &AuthClaims,
93 dids_ks: &KeyspaceHandle,
94 acl_ks: &KeyspaceHandle,
95 config: &AppConfig,
96) -> Result<(), AppError> {
97 if auth.role == Role::Admin {
98 return Ok(());
99 }
100 let acl_entry = acl::get_acl_entry(acl_ks, &auth.did).await?;
101 let max = acl_entry
102 .as_ref()
103 .map(|e| e.effective_max_did_count(config.limits.default_max_did_count))
104 .unwrap_or(config.limits.default_max_did_count);
105
106 let quota = get_quota(dids_ks, &auth.did).await?;
107 if quota.did_count >= max {
108 warn!(did = %auth.did, count = quota.did_count, max, "DID count quota exceeded");
109 return Err(AppError::QuotaExceeded(format!(
110 "DID count limit reached ({max})"
111 )));
112 }
113 debug!(did = %auth.did, count = quota.did_count, max, "DID count quota check passed");
114 Ok(())
115}
116
117pub async fn check_total_size_limit(
120 auth: &AuthClaims,
121 dids_ks: &KeyspaceHandle,
122 acl_ks: &KeyspaceHandle,
123 config: &AppConfig,
124 old_size: u64,
125 new_size: u64,
126) -> Result<(), AppError> {
127 if auth.role == Role::Admin {
128 return Ok(());
129 }
130 let acl_entry = acl::get_acl_entry(acl_ks, &auth.did).await?;
131 let max = acl_entry
132 .as_ref()
133 .map(|e| e.effective_max_total_size(config.limits.default_max_total_size))
134 .unwrap_or(config.limits.default_max_total_size);
135
136 let quota = get_quota(dids_ks, &auth.did).await?;
137 let proposed = quota
138 .total_size
139 .saturating_sub(old_size)
140 .saturating_add(new_size);
141 if proposed > max {
142 warn!(did = %auth.did, current = quota.total_size, old_size, new_size, max, "total size quota exceeded");
143 return Err(AppError::QuotaExceeded(format!(
144 "total DID document size would exceed limit ({max} bytes)"
145 )));
146 }
147 debug!(did = %auth.did, current = quota.total_size, new_size, max, "total size quota check passed");
148 Ok(())
149}
150
151pub async fn get_authorized_record(
157 dids_ks: &KeyspaceHandle,
158 mnemonic: &str,
159 auth: &AuthClaims,
160) -> Result<DidRecord, AppError> {
161 let record: DidRecord = dids_ks
162 .get(did_key(mnemonic))
163 .await?
164 .ok_or_else(|| AppError::NotFound(format!("DID not found: {mnemonic}")))?;
165 if record.owner != auth.did && auth.role != Role::Admin {
166 warn!(
167 caller = %auth.did,
168 role = %auth.role,
169 owner = %record.owner,
170 mnemonic = %mnemonic,
171 "access denied: not the owner of this DID"
172 );
173 return Err(AppError::Forbidden("not the owner of this DID".into()));
174 }
175 Ok(record)
176}
177
178pub async fn set_did_disabled(
184 auth: &AuthClaims,
185 state: &AppState,
186 mnemonic: &str,
187 disabled: bool,
188) -> Result<(), AppError> {
189 validate_mnemonic(mnemonic)?;
190 let mut record = get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
191 record.disabled = disabled;
192 state.dids_ks.insert(did_key(mnemonic), &record).await?;
193 info!(
194 did = %auth.did,
195 role = %auth.role,
196 mnemonic = %mnemonic,
197 disabled,
198 "DID disabled state updated"
199 );
200 Ok(())
201}
202
203pub fn validate_did_jsonl(content: &str) -> Result<(), AppError> {
209 did_hosting_common::did_ops::validate_did_jsonl(content).map_err(AppError::Validation)
210}
211
212pub struct CreateDidResult {
218 pub mnemonic: String,
219 pub did_url: String,
220}
221
222pub async fn create_did(
224 auth: &AuthClaims,
225 state: &AppState,
226 path: Option<&str>,
227) -> Result<CreateDidResult, AppError> {
228 check_did_count_limit(auth, &state.dids_ks, &state.acl_ks, &state.config).await?;
229
230 let mnemonic = match path {
231 Some(custom_path) if custom_path == ".well-known" => {
232 if auth.role != Role::Admin {
233 return Err(AppError::Forbidden(
234 "only admins can create the root DID".into(),
235 ));
236 }
237 if !is_path_available(&state.dids_ks, custom_path).await? {
238 return Err(AppError::Conflict(
239 "root DID (.well-known) already exists".into(),
240 ));
241 }
242 custom_path.to_string()
243 }
244 Some(custom_path) => {
245 validate_custom_path(custom_path)?;
246 if !is_path_available(&state.dids_ks, custom_path).await? {
247 return Err(AppError::Conflict(format!(
248 "path '{custom_path}' is already taken"
249 )));
250 }
251 custom_path.to_string()
252 }
253 None => generate_unique_mnemonic(&state.dids_ks).await?,
254 };
255
256 let now = now_epoch();
257 let record = DidRecord {
258 owner: auth.did.clone(),
259 mnemonic: mnemonic.clone(),
260 created_at: now,
261 updated_at: now,
262 version_count: 0,
263 did_id: None,
264 content_size: 0,
265 disabled: false,
266 deleted_at: None,
267
268 method: "webvh".to_string(),
270 domain: String::new(),
271
272 services: None,
274 agent_names: Vec::new(),
275 };
276
277 let mut batch = state.store.batch();
278 batch.insert(&state.dids_ks, did_key(&mnemonic), &record)?;
279 batch.insert_raw(
280 &state.dids_ks,
281 owner_key(&auth.did, &mnemonic),
282 mnemonic.as_bytes().to_vec(),
283 );
284 batch.commit().await?;
285
286 quota_on_create(&state.dids_ks, &auth.did).await?;
288
289 let did_url = format!("{}/{mnemonic}/did.jsonl", state.config.public_base_url());
290
291 if let Some(ref collector) = state.stats_collector {
292 collector.increment_total_dids();
293 }
294 info!(audit = true, did = %auth.did, role = %auth.role, mnemonic = %mnemonic, "DID URI created");
295
296 Ok(CreateDidResult { mnemonic, did_url })
297}
298
299pub struct PublishDidResult {
301 pub did_id: Option<String>,
302 pub did_url: String,
303 pub version_id: Option<String>,
304 pub version_count: u64,
305}
306
307pub async fn publish_did(
309 auth: &AuthClaims,
310 state: &AppState,
311 mnemonic: &str,
312 did_log: &str,
313) -> Result<PublishDidResult, AppError> {
314 validate_mnemonic(mnemonic)?;
315 let mut record = get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
316
317 validate_did_jsonl(did_log)?;
318
319 let new_size = did_log.len() as u64;
320 let old_size = record.content_size;
321 check_total_size_limit(
322 auth,
323 &state.dids_ks,
324 &state.acl_ks,
325 &state.config,
326 old_size,
327 new_size,
328 )
329 .await?;
330
331 let did_id = extract_did_id(did_log);
332
333 let version_id = did_log
334 .lines()
335 .last()
336 .and_then(|line| serde_json::from_str::<serde_json::Value>(line).ok())
337 .and_then(|v| {
338 v.get("versionId")
339 .and_then(|id| id.as_str())
340 .map(String::from)
341 });
342
343 record.updated_at = now_epoch();
344 record.version_count += 1;
345 record.did_id = did_id.clone();
346 record.content_size = new_size;
347 record.services = extract_service_types(did_log);
350
351 let mut batch = state.store.batch();
352 batch.insert_raw(
353 &state.dids_ks,
354 content_log_key(mnemonic),
355 did_log.as_bytes().to_vec(),
356 );
357 batch.insert(&state.dids_ks, did_key(mnemonic), &record)?;
358 batch.commit().await?;
359
360 quota_on_size_change(&state.dids_ks, &record.owner, old_size, new_size).await?;
362
363 state.did_cache.invalidate(&content_log_key(mnemonic));
365
366 if let Some(ref collector) = state.stats_collector {
367 collector.record_update(mnemonic);
368 }
369
370 let did_url = format!("{}/{mnemonic}/did.jsonl", state.config.public_base_url());
371
372 info!(
373 did = %auth.did,
374 role = %auth.role,
375 mnemonic = %mnemonic,
376 size = new_size,
377 version = record.version_count,
378 "did.jsonl published"
379 );
380
381 crate::identity_rotation::on_did_published(state, mnemonic).await;
390
391 Ok(PublishDidResult {
392 did_id,
393 did_url,
394 version_id,
395 version_count: record.version_count,
396 })
397}
398
399pub struct WitnessUploadResult {
401 pub witness_url: String,
402}
403
404pub async fn upload_witness(
406 auth: &AuthClaims,
407 state: &AppState,
408 mnemonic: &str,
409 witness_content: &str,
410) -> Result<WitnessUploadResult, AppError> {
411 validate_mnemonic(mnemonic)?;
412 get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
413
414 if witness_content.is_empty() {
415 return Err(AppError::Validation(
416 "did-witness.json content cannot be empty".into(),
417 ));
418 }
419
420 serde_json::from_str::<serde_json::Value>(witness_content)
422 .map_err(|e| AppError::Validation(format!("did-witness.json must be valid JSON: {e}")))?;
423
424 let size = witness_content.len();
425
426 state
427 .dids_ks
428 .insert_raw(
429 content_witness_key(mnemonic),
430 witness_content.as_bytes().to_vec(),
431 )
432 .await?;
433
434 let witness_url = format!(
435 "{}/{mnemonic}/did-witness.json",
436 state.config.public_base_url()
437 );
438
439 info!(did = %auth.did, role = %auth.role, mnemonic = %mnemonic, size, "did-witness.json uploaded");
440
441 Ok(WitnessUploadResult { witness_url })
442}
443
444pub struct DidInfoResult {
446 pub record: DidRecord,
447 pub log_metadata: Option<LogMetadata>,
448 pub stats: did_hosting_common::DidStats,
449 pub did_url: String,
450}
451
452pub async fn get_did_info(
454 auth: &AuthClaims,
455 state: &AppState,
456 mnemonic: &str,
457) -> Result<DidInfoResult, AppError> {
458 validate_mnemonic(mnemonic)?;
459 let record = get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
460
461 let log_metadata = match state.dids_ks.get_raw(content_log_key(mnemonic)).await? {
462 Some(bytes) => {
463 let content = String::from_utf8(bytes).unwrap_or_default();
464 Some(extract_log_metadata(&content))
465 }
466 None => None,
467 };
468
469 let did_stats = did_hosting_common::DidStats::default();
470 let did_url = format!("{}/{mnemonic}/did.jsonl", state.config.public_base_url());
471
472 info!(did = %auth.did, mnemonic = %mnemonic, "DID info retrieved");
473
474 Ok(DidInfoResult {
475 record,
476 log_metadata,
477 stats: did_stats,
478 did_url,
479 })
480}
481
482pub async fn get_did_log(
484 auth: &AuthClaims,
485 state: &AppState,
486 mnemonic: &str,
487) -> Result<Vec<LogEntryInfo>, AppError> {
488 validate_mnemonic(mnemonic)?;
489 get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
490
491 let bytes = state
492 .dids_ks
493 .get_raw(content_log_key(mnemonic))
494 .await?
495 .ok_or_else(|| AppError::NotFound("no log content for this DID".into()))?;
496
497 let content = String::from_utf8(bytes)
498 .map_err(|e| AppError::Internal(format!("invalid log bytes: {e}")))?;
499
500 let entries = parse_log_entries(&content);
501
502 debug!(mnemonic = %mnemonic, count = entries.len(), "DID log entries retrieved");
503
504 Ok(entries)
505}
506
507pub async fn list_dids(
510 auth: &AuthClaims,
511 state: &AppState,
512 requested_owner: Option<&str>,
513 limit: Option<usize>,
514 offset: Option<usize>,
515) -> Result<Vec<DidListEntry>, AppError> {
516 if auth.role == Role::Admin && requested_owner.is_none() {
518 return list_all_dids(auth, state).await;
519 }
520
521 let target_owner = if auth.role == Role::Admin {
522 requested_owner.unwrap_or(&auth.did)
523 } else {
524 &auth.did
525 };
526
527 let prefix = format!("owner:{target_owner}:");
528 let raw = state.dids_ks.prefix_iter_raw(prefix).await?;
529
530 let mut entries = Vec::with_capacity(raw.len());
531 for (_key, value) in raw {
532 let mnemonic = String::from_utf8(value)
533 .map_err(|e| AppError::Internal(format!("invalid mnemonic bytes: {e}")))?;
534 if let Some(record) = state.dids_ks.get::<DidRecord>(did_key(&mnemonic)).await? {
535 let did_stats = did_hosting_common::DidStats::default();
536 let agent_names = record.agent_names.clone();
537 entries.push(DidListEntry {
538 mnemonic: record.mnemonic,
539 owner: record.owner,
540 created_at: record.created_at,
541 updated_at: record.updated_at,
542 version_count: record.version_count,
543 did_id: record.did_id,
544 total_resolves: did_stats.total_resolves,
545 disabled: record.disabled,
546 method: (!record.method.is_empty()).then(|| record.method.clone()),
547 domain: (!record.domain.is_empty()).then(|| record.domain.clone()),
548 agent_names,
549 services: record.services,
550 });
551 }
552 }
553
554 let offset = offset.unwrap_or(0);
556 let limit = limit.unwrap_or(1000); let total = entries.len();
558 let entries: Vec<_> = entries.into_iter().skip(offset).take(limit).collect();
559
560 info!(did = %auth.did, role = %auth.role, owner = %target_owner, total, returned = entries.len(), "DIDs listed");
561
562 Ok(entries)
563}
564
565async fn list_all_dids(auth: &AuthClaims, state: &AppState) -> Result<Vec<DidListEntry>, AppError> {
567 let raw = state.dids_ks.prefix_iter_raw("did:").await?;
568
569 let mut entries = Vec::with_capacity(raw.len());
570 for (_key, value) in raw {
571 let record: DidRecord = match serde_json::from_slice(&value) {
572 Ok(r) => r,
573 Err(_) => continue,
574 };
575 let did_stats = did_hosting_common::DidStats::default();
576 let agent_names = record.agent_names.clone();
577 entries.push(DidListEntry {
578 mnemonic: record.mnemonic,
579 owner: record.owner,
580 created_at: record.created_at,
581 updated_at: record.updated_at,
582 version_count: record.version_count,
583 did_id: record.did_id,
584 total_resolves: did_stats.total_resolves,
585 disabled: record.disabled,
586 method: (!record.method.is_empty()).then(|| record.method.clone()),
587 domain: (!record.domain.is_empty()).then(|| record.domain.clone()),
588 agent_names,
589 services: record.services,
590 });
591 }
592
593 info!(did = %auth.did, role = %auth.role, count = entries.len(), "all DIDs listed (admin)");
594
595 Ok(entries)
596}
597
598pub struct DeleteDidResult {
600 pub mnemonic: String,
601 pub did_id: Option<String>,
602}
603
604pub async fn delete_did(
607 auth: &AuthClaims,
608 state: &AppState,
609 mnemonic: &str,
610) -> Result<DeleteDidResult, AppError> {
611 validate_mnemonic(mnemonic)?;
612 let mut record = get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
613
614 let did_id = record.did_id.clone();
615
616 record.deleted_at = Some(now_epoch());
618 state.dids_ks.insert(did_key(mnemonic), &record).await?;
619
620 quota_on_delete(&state.dids_ks, &record.owner, record.content_size).await?;
622
623 state.did_cache.invalidate(&content_log_key(mnemonic));
624
625 if let Some(ref collector) = state.stats_collector {
626 collector.decrement_total_dids();
627 }
628 info!(audit = true, did = %auth.did, role = %auth.role, mnemonic = %mnemonic, "DID deleted");
629
630 Ok(DeleteDidResult {
631 mnemonic: mnemonic.to_string(),
632 did_id,
633 })
634}
635
636pub async fn recover_did(state: &AppState, mnemonic: &str) -> Result<(), AppError> {
638 validate_mnemonic(mnemonic)?;
639
640 let mut record: DidRecord = state
641 .dids_ks
642 .get(did_key(mnemonic))
643 .await?
644 .ok_or_else(|| AppError::NotFound(format!("DID not found: {mnemonic}")))?;
645
646 if record.deleted_at.is_none() {
647 return Err(AppError::Validation("DID is not deleted".into()));
648 }
649
650 record.deleted_at = None;
651 state.dids_ks.insert(did_key(mnemonic), &record).await?;
652
653 quota_on_create(&state.dids_ks, &record.owner).await?;
655
656 state.did_cache.invalidate(&content_log_key(mnemonic));
657
658 info!(audit = true, mnemonic = %mnemonic, owner = %record.owner, "DID recovered from soft delete");
659
660 Ok(())
661}
662
663pub struct RollbackDidResult {
669 pub record: DidRecord,
670 pub log_metadata: Option<LogMetadata>,
671 pub did_url: String,
672}
673
674pub async fn rollback_did(
680 auth: &AuthClaims,
681 state: &AppState,
682 mnemonic: &str,
683) -> Result<RollbackDidResult, AppError> {
684 validate_mnemonic(mnemonic)?;
685 let mut record = get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
686
687 let bytes = state
688 .dids_ks
689 .get_raw(content_log_key(mnemonic))
690 .await?
691 .ok_or_else(|| AppError::NotFound("no log content for this DID".into()))?;
692
693 let content = String::from_utf8(bytes)
694 .map_err(|e| AppError::Internal(format!("invalid log bytes: {e}")))?;
695
696 let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
697 if lines.len() < 2 {
698 return Err(AppError::Validation(
699 "cannot rollback: DID log must have at least 2 entries".into(),
700 ));
701 }
702
703 let truncated_lines = &lines[..lines.len() - 1];
704 let truncated = truncated_lines.join("\n");
705
706 let new_did_id = extract_did_id(&truncated);
707 let new_size = truncated.len() as u64;
708
709 record.version_count = truncated_lines.len() as u64;
710 record.did_id = new_did_id;
711 record.content_size = new_size;
712 record.updated_at = now_epoch();
713 record.services = extract_service_types(&truncated);
715
716 let mut batch = state.store.batch();
717 batch.insert_raw(
718 &state.dids_ks,
719 content_log_key(mnemonic),
720 truncated.as_bytes().to_vec(),
721 );
722 batch.insert(&state.dids_ks, did_key(mnemonic), &record)?;
723 batch.remove(&state.dids_ks, content_witness_key(mnemonic));
724 batch.commit().await?;
725
726 state.did_cache.invalidate(&content_log_key(mnemonic));
727
728 let log_metadata = Some(extract_log_metadata(&truncated));
729 let did_url = format!("{}/{mnemonic}/did.jsonl", state.config.public_base_url());
730
731 info!(
732 did = %auth.did,
733 role = %auth.role,
734 mnemonic = %mnemonic,
735 remaining = truncated_lines.len(),
736 "DID log entry rolled back"
737 );
738
739 Ok(RollbackDidResult {
740 record,
741 log_metadata,
742 did_url,
743 })
744}
745
746pub async fn get_raw_log(
748 auth: &AuthClaims,
749 state: &AppState,
750 mnemonic: &str,
751) -> Result<String, AppError> {
752 validate_mnemonic(mnemonic)?;
753 get_authorized_record(&state.dids_ks, mnemonic, auth).await?;
754
755 let bytes = state
756 .dids_ks
757 .get_raw(content_log_key(mnemonic))
758 .await?
759 .ok_or_else(|| AppError::NotFound("no log content for this DID".into()))?;
760
761 String::from_utf8(bytes).map_err(|e| AppError::Internal(format!("invalid log bytes: {e}")))
762}
763
764pub async fn cleanup_empty_dids(
771 dids_ks: &KeyspaceHandle,
772 ttl_seconds: u64,
773) -> Result<u64, AppError> {
774 const SOFT_DELETE_RETENTION: u64 = 30 * 24 * 3600; let now = now_epoch();
776 let raw = dids_ks.prefix_iter_raw("did:").await?;
777 let mut removed = 0u64;
778
779 for (_key, value) in raw {
780 let record: DidRecord = match serde_json::from_slice(&value) {
781 Ok(r) => r,
782 Err(_) => continue,
783 };
784
785 let should_remove =
786 (record.version_count == 0 && now.saturating_sub(record.created_at) > ttl_seconds)
788 || record.deleted_at.is_some_and(|d| now.saturating_sub(d) > SOFT_DELETE_RETENTION);
790
791 if should_remove {
792 dids_ks.remove(did_key(&record.mnemonic)).await?;
793 dids_ks.remove(content_log_key(&record.mnemonic)).await?;
794 dids_ks
795 .remove(content_witness_key(&record.mnemonic))
796 .await?;
797 dids_ks
798 .remove(owner_key(&record.owner, &record.mnemonic))
799 .await?;
800 removed += 1;
801 }
802 }
803
804 Ok(removed)
805}
806
807#[cfg(test)]
812mod tests {
813 use super::*;
814
815 #[test]
818 fn validate_jsonl_empty_string_rejected() {
819 let result = validate_did_jsonl("");
820 assert!(result.is_err());
821 let err = result.unwrap_err().to_string();
822 assert!(err.contains("empty"), "expected 'empty' in: {err}");
823 }
824
825 #[test]
826 fn validate_jsonl_invalid_json_rejected() {
827 let result = validate_did_jsonl("this is not json");
828 assert!(result.is_err());
829 let err = result.unwrap_err().to_string();
830 assert!(
831 err.contains("invalid log entry at line 1"),
832 "expected line reference in: {err}"
833 );
834 }
835
836 #[test]
837 fn validate_jsonl_valid_json_but_not_log_entry() {
838 let result = validate_did_jsonl(r#"{"hello":"world"}"#);
839 assert!(result.is_err());
840 }
841
842 async fn make_valid_jsonl() -> String {
843 use did_hosting_common::did::{build_did_document, create_log_entry, encode_host};
844
845 let secret = affinidi_tdk::secrets_resolver::secrets::Secret::generate_ed25519(None, None);
846 let pk = secret.get_public_keymultibase().unwrap();
847 let host = encode_host("http://localhost:3000").unwrap();
848 let doc = build_did_document(&host, "test-validate", &pk, &Default::default());
849 let (_scid, jsonl) = create_log_entry(&doc, &secret).await.unwrap();
850 jsonl
851 }
852
853 #[tokio::test]
854 async fn validate_jsonl_blank_lines_skipped() {
855 let entry = make_valid_jsonl().await;
856 let with_blanks = format!("\n{entry}\n\n");
857 assert!(validate_did_jsonl(&with_blanks).is_ok());
858 }
859
860 #[tokio::test]
861 async fn validate_jsonl_valid_single_entry() {
862 let entry = make_valid_jsonl().await;
863 assert!(validate_did_jsonl(&entry).is_ok());
864 }
865
866 #[tokio::test]
867 async fn validate_jsonl_second_line_invalid() {
868 let entry = make_valid_jsonl().await;
869 let content = format!("{entry}\nnot valid json");
870 let result = validate_did_jsonl(&content);
871 assert!(result.is_err());
872 let err = result.unwrap_err().to_string();
873 assert!(err.contains("line 2"), "expected 'line 2' in error: {err}");
874 }
875
876 #[test]
879 fn rollback_removes_last_line() {
880 let line1 = r#"{"state":{"id":"did:webvh:first:host:path"}}"#;
881 let line2 = r#"{"state":{"id":"did:webvh:second:host:path"}}"#;
882 let jsonl = format!("{line1}\n{line2}");
883 let lines: Vec<&str> = jsonl.lines().filter(|l| !l.trim().is_empty()).collect();
884 assert_eq!(lines.len(), 2);
885 let truncated = lines[..lines.len() - 1].join("\n");
886 assert_eq!(truncated, line1);
887 assert_eq!(
888 extract_did_id(&truncated),
889 Some("did:webvh:first:host:path".to_string())
890 );
891 }
892
893 #[test]
894 fn rollback_rejects_single_entry() {
895 let jsonl = r#"{"state":{"id":"did:webvh:only:host:path"}}"#;
896 let lines: Vec<&str> = jsonl.lines().filter(|l| !l.trim().is_empty()).collect();
897 assert!(lines.len() < 2, "single entry should not be rollback-able");
898 }
899
900 #[test]
901 fn rollback_updates_did_id() {
902 let line1 = r#"{"state":{"id":"did:webvh:genesis:host:path"}}"#;
903 let line2 = r#"{"state":{"id":"did:webvh:update1:host:path"}}"#;
904 let line3 = r#"{"state":{"id":"did:webvh:update2:host:path"}}"#;
905 let jsonl = format!("{line1}\n{line2}\n{line3}");
906 let lines: Vec<&str> = jsonl.lines().filter(|l| !l.trim().is_empty()).collect();
907 let truncated = lines[..lines.len() - 1].join("\n");
908 assert_eq!(
909 extract_did_id(&truncated),
910 Some("did:webvh:update1:host:path".to_string())
911 );
912 }
913}