Skip to main content

didwebvh_rs/
create.rs

1/*!
2*   Library API for creating a new webvh DID programmatically.
3*   Encapsulates the DID creation flow (log entry creation, validation, witness signing)
4*   without any interactive prompts.
5*/
6
7use crate::{
8    DIDWebVHError, DIDWebVHState, Signer, ensure_object_mut,
9    log_entry::{LogEntry, LogEntryMethods},
10    log_entry_state::LogEntryState,
11    parameters::Parameters,
12    url::WebVHURL,
13    witness::{Witnesses, proofs::WitnessProofCollection},
14};
15use affinidi_data_integrity::{DataIntegrityProof, SignOptions};
16use affinidi_secrets_resolver::secrets::Secret;
17use ahash::HashMap;
18use serde_json::{Value, json};
19use std::sync::Arc;
20use url::Url;
21
22/// Configuration for creating a new DID.
23///
24/// Generic over `A` (authorization key signer) and `W` (witness signer).
25/// Both default to [`Secret`] for backward compatibility.
26pub struct CreateDIDConfig<A: Signer = Secret, W: Signer = Secret> {
27    /// Address: URL (e.g. `https://example.com/`) or DID (e.g. `did:webvh:{SCID}:example.com`)
28    pub address: String,
29    /// At least one signer for signing the log entry
30    pub authorization_keys: Vec<A>,
31    /// The DID Document (JSON Value). Must contain `id` matching the DID.
32    pub did_document: Value,
33    /// Parameters (update_keys, portable, witnesses, watchers, ttl, etc.)
34    pub parameters: Parameters,
35    /// Witness signers keyed by witness DID — required if witnesses configured
36    pub witness_secrets: HashMap<String, W>,
37    /// Add did:web to alsoKnownAs
38    pub also_known_as_web: bool,
39    /// Add did:scid:vh to alsoKnownAs
40    pub also_known_as_scid: bool,
41    /// Explicit versionTime for the genesis log entry. `None` (default) stamps
42    /// `now`. Set this to control the entry timestamp — e.g. to backdate it so a
43    /// rapid create-then-update sequence stays strictly increasing and not in the
44    /// future (versionTime serializes at second granularity).
45    pub version_time: Option<chrono::DateTime<chrono::FixedOffset>>,
46}
47
48/// Builder for constructing a [`CreateDIDConfig`].
49///
50/// Only `address`, `authorization_keys`, `did_document`, and `parameters` are required.
51/// All other fields have sensible defaults.
52///
53/// # Example
54/// ```no_run
55/// use didwebvh_rs::prelude::*;
56/// # use serde_json::Value;
57///
58/// # fn run(signing_key: Secret, doc: Value, params: Parameters)
59/// #     -> Result<(), DIDWebVHError> {
60/// let config = CreateDIDConfig::builder()
61///     .address("https://example.com/")
62///     .authorization_key(signing_key)
63///     .did_document(doc)
64///     .parameters(params)
65///     .also_known_as_web(true)
66///     .build()?;
67/// # Ok(())
68/// # }
69/// ```
70pub struct CreateDIDConfigBuilder<A: Signer = Secret, W: Signer = Secret> {
71    address: Option<String>,
72    authorization_keys: Vec<A>,
73    did_document: Option<Value>,
74    parameters: Option<Parameters>,
75    witness_secrets: HashMap<String, W>,
76    also_known_as_web: bool,
77    also_known_as_scid: bool,
78    version_time: Option<chrono::DateTime<chrono::FixedOffset>>,
79}
80
81impl<A: Signer, W: Signer> CreateDIDConfigBuilder<A, W> {
82    fn new() -> Self {
83        Self {
84            address: None,
85            authorization_keys: Vec::new(),
86            did_document: None,
87            parameters: None,
88            witness_secrets: HashMap::default(),
89            also_known_as_web: false,
90            also_known_as_scid: false,
91            version_time: None,
92        }
93    }
94
95    /// Set the address (URL or DID format). Required.
96    pub fn address(mut self, address: impl Into<String>) -> Self {
97        self.address = Some(address.into());
98        self
99    }
100
101    /// Add a single authorization key. At least one is required.
102    pub fn authorization_key(mut self, key: A) -> Self {
103        self.authorization_keys.push(key);
104        self
105    }
106
107    /// Set all authorization keys at once, replacing any previously added.
108    pub fn authorization_keys(mut self, keys: Vec<A>) -> Self {
109        self.authorization_keys = keys;
110        self
111    }
112
113    /// Set the DID Document. Required.
114    pub fn did_document(mut self, doc: Value) -> Self {
115        self.did_document = Some(doc);
116        self
117    }
118
119    /// Set the Parameters. Required.
120    pub fn parameters(mut self, params: Parameters) -> Self {
121        self.parameters = Some(params);
122        self
123    }
124
125    /// Add a single witness signer keyed by witness DID.
126    pub fn witness_secret(mut self, did: impl Into<String>, secret: W) -> Self {
127        self.witness_secrets.insert(did.into(), secret);
128        self
129    }
130
131    /// Set all witness signers at once, replacing any previously added.
132    pub fn witness_secrets(mut self, secrets: HashMap<String, W>) -> Self {
133        self.witness_secrets = secrets;
134        self
135    }
136
137    /// Whether to add `did:web` to `alsoKnownAs`. Defaults to `false`.
138    pub fn also_known_as_web(mut self, enabled: bool) -> Self {
139        self.also_known_as_web = enabled;
140        self
141    }
142
143    /// Whether to add `did:scid:vh` to `alsoKnownAs`. Defaults to `false`.
144    pub fn also_known_as_scid(mut self, enabled: bool) -> Self {
145        self.also_known_as_scid = enabled;
146        self
147    }
148
149    /// Set an explicit versionTime for the genesis log entry (default: `now`).
150    pub fn version_time(mut self, version_time: chrono::DateTime<chrono::FixedOffset>) -> Self {
151        self.version_time = Some(version_time);
152        self
153    }
154
155    /// Build the [`CreateDIDConfig`], returning an error if required fields are missing.
156    pub fn build(self) -> Result<CreateDIDConfig<A, W>, DIDWebVHError> {
157        let address = self
158            .address
159            .ok_or_else(|| DIDWebVHError::DIDError("address is required".to_string()))?;
160        if self.authorization_keys.is_empty() {
161            return Err(DIDWebVHError::LogEntryError(
162                "At least one authorization key is required".to_string(),
163            ));
164        }
165        let did_document = self
166            .did_document
167            .ok_or_else(|| DIDWebVHError::DIDError("did_document is required".to_string()))?;
168        let parameters = self
169            .parameters
170            .ok_or_else(|| DIDWebVHError::ParametersError("parameters is required".to_string()))?;
171
172        // Validate that the DID document has a top-level "id" field
173        match did_document.get("id") {
174            Some(Value::String(id)) => {
175                // For a new DID (no existing log), the document must contain {SCID} or {DID} placeholder
176                if !id.contains("{SCID}") && !id.contains("{DID}") {
177                    return Err(DIDWebVHError::DIDError(
178                        "DID document 'id' must contain a '{SCID}' or '{DID}' placeholder \
179                         (e.g. \"did:webvh:{SCID}:example.com\" or \"{DID}\"). \
180                         The placeholder is replaced with the actual identifier during creation."
181                            .to_string(),
182                    ));
183                }
184            }
185            Some(_) => {
186                return Err(DIDWebVHError::DIDError(
187                    "DID document 'id' field must be a string".to_string(),
188                ));
189            }
190            None => {
191                return Err(DIDWebVHError::DIDError(
192                    "DID document must have a top-level 'id' field".to_string(),
193                ));
194            }
195        }
196
197        Ok(CreateDIDConfig {
198            address,
199            authorization_keys: self.authorization_keys,
200            did_document,
201            parameters,
202            witness_secrets: self.witness_secrets,
203            also_known_as_web: self.also_known_as_web,
204            also_known_as_scid: self.also_known_as_scid,
205            version_time: self.version_time,
206        })
207    }
208}
209
210impl CreateDIDConfig {
211    /// Create a new builder for `CreateDIDConfig` using default signer types (`Secret`).
212    ///
213    /// For custom signer types, use [`Self::builder_generic()`].
214    pub fn builder() -> CreateDIDConfigBuilder {
215        CreateDIDConfigBuilder::new()
216    }
217}
218
219impl<A: Signer, W: Signer> CreateDIDConfig<A, W> {
220    /// Create a new builder for `CreateDIDConfig` with custom signer types.
221    pub fn builder_generic() -> CreateDIDConfigBuilder<A, W> {
222        CreateDIDConfigBuilder::new()
223    }
224}
225
226/// Result of creating a new DID
227#[derive(Clone, Debug)]
228pub struct CreateDIDResult {
229    /// The resolved DID identifier (with SCID)
230    pub(crate) did: String,
231    /// The signed first log entry (serialize to JSON for did.jsonl)
232    pub(crate) log_entry: LogEntry,
233    /// Witness proofs (serialize to JSON for witness.json). Empty if no witnesses.
234    pub(crate) witness_proofs: WitnessProofCollection,
235}
236
237impl CreateDIDResult {
238    /// Returns the resolved DID identifier (with SCID).
239    pub fn did(&self) -> &str {
240        &self.did
241    }
242
243    /// Returns a reference to the signed first log entry.
244    pub fn log_entry(&self) -> &LogEntry {
245        &self.log_entry
246    }
247
248    /// Returns a reference to the witness proof collection.
249    pub fn witness_proofs(&self) -> &WitnessProofCollection {
250        &self.witness_proofs
251    }
252}
253
254/// Validate that a signer's verification method is in the expected `did:key:{mb}#{mb}` format.
255fn validate_did_key_vm(vm: &str) -> Result<(), DIDWebVHError> {
256    if !vm.starts_with("did:key:") || !vm.contains('#') {
257        return Err(DIDWebVHError::LogEntryError(format!(
258            "Signer verification_method '{vm}' must be in 'did:key:{{mb}}#{{mb}}' format"
259        )));
260    }
261    Ok(())
262}
263
264/// Create a new DID using the provided configuration.
265///
266/// This is the main library entry point for DID creation. It:
267/// 1. Parses the address (URL or DID format)
268/// 2. Optionally adds `did:web` and `did:scid:vh` to `alsoKnownAs`
269/// 3. Creates and signs the first log entry
270/// 4. Validates the log entry
271/// 5. Signs witness proofs using provided witness secrets
272///
273/// Returns the resolved DID, signed LogEntry, and WitnessProofCollection.
274pub async fn create_did<A: Signer, W: Signer>(
275    mut config: CreateDIDConfig<A, W>,
276) -> Result<CreateDIDResult, DIDWebVHError> {
277    // Parse the address
278    let did_url = if config.address.starts_with("did:") {
279        WebVHURL::parse_did_url(&config.address)?
280    } else {
281        let url = Url::parse(&config.address).map_err(|e| {
282            DIDWebVHError::DIDError(format!("Invalid URL ({}): {e}", config.address))
283        })?;
284        WebVHURL::parse_url(&url)?
285    };
286
287    let webvh_did = did_url.to_string();
288
289    // Optionally add did:web to alsoKnownAs
290    if config.also_known_as_web {
291        add_web_also_known_as(&mut config.did_document, &webvh_did)?;
292    }
293
294    // Optionally add did:scid:vh to alsoKnownAs
295    if config.also_known_as_scid {
296        add_scid_also_known_as(&mut config.did_document, &webvh_did)?;
297    }
298
299    replace_did_placeholder(&mut config.did_document, &webvh_did);
300
301    // Validate authorization keys have proper did:key verification methods
302    for key in &config.authorization_keys {
303        validate_did_key_vm(key.verification_method())?;
304    }
305
306    // Create the log entry
307    let mut didwebvh = DIDWebVHState::default();
308    let signing_key = config.authorization_keys.first().ok_or_else(|| {
309        DIDWebVHError::LogEntryError("At least one authorization key is required".to_string())
310    })?;
311
312    let log_entry_state = didwebvh
313        .create_log_entry(
314            config.version_time,
315            &config.did_document,
316            &config.parameters,
317            signing_key,
318        )
319        .await?;
320
321    // Validate the log entry
322    log_entry_state.log_entry.verify_log_entry(None, None)?;
323
324    // Get the resolved DID (with SCID)
325    let resolved_did =
326        if let Some(Value::String(id)) = log_entry_state.log_entry.get_state().get("id") {
327            id.clone()
328        } else {
329            webvh_did
330        };
331
332    // Clone the log entry since we borrow from didwebvh
333    let log_entry = log_entry_state.log_entry.clone();
334    let active_witnesses = log_entry_state.get_active_witnesses();
335
336    // Sign witness proofs
337    let mut witness_proofs = WitnessProofCollection::default();
338    sign_witness_proofs(
339        &mut witness_proofs,
340        log_entry_state,
341        &active_witnesses,
342        &config.witness_secrets,
343    )
344    .await?;
345
346    Ok(CreateDIDResult {
347        did: resolved_did,
348        log_entry,
349        witness_proofs,
350    })
351}
352
353/// Recursively replaces all occurrences of the string "{DID}" in leaf string values of a JSON document.
354///
355/// Traverses the provided `did_document` (serde_json::Value), and for every string value found,
356/// replaces all instances of "{DID}" with the provided `did` value. This is useful for templating
357/// DID documents where placeholders need to be replaced with the actual DID.
358///
359/// # Arguments
360/// * `did_document` - A mutable reference to a serde_json::Value representing the DID document.
361/// * `did` - The DID string to substitute for the "{DID}" placeholder.
362fn replace_did_placeholder(did_document: &mut Value, did: &str) {
363    match did_document {
364        Value::Object(map) => {
365            for value in map.values_mut() {
366                replace_did_placeholder(value, did);
367            }
368        }
369        Value::Array(arr) => {
370            for value in arr.iter_mut() {
371                replace_did_placeholder(value, did);
372            }
373        }
374        Value::String(s) if s.contains("{DID}") => {
375            *s = s.replace("{DID}", did);
376        }
377        _ => {}
378    }
379}
380
381/// Add a `did:web` alias to `alsoKnownAs` in the DID document (non-interactive).
382///
383/// Converts the `did:webvh` identifier to `did:web` format and inserts it into
384/// the `alsoKnownAs` array. If the alias already exists, it is not duplicated.
385pub fn add_web_also_known_as(did_document: &mut Value, did: &str) -> Result<(), DIDWebVHError> {
386    let did_web_id = DIDWebVHState::convert_webvh_id_to_web_id(did);
387
388    let also_known_as = did_document.get_mut("alsoKnownAs");
389
390    let Some(also_known_as) = also_known_as else {
391        // There is no alsoKnownAs, add the did:web
392        ensure_object_mut(did_document)?.insert(
393            "alsoKnownAs".to_string(),
394            Value::Array(vec![Value::String(did_web_id.to_string())]),
395        );
396        return Ok(());
397    };
398
399    let new_aliases = build_alias_list(also_known_as, &did_web_id)?;
400
401    ensure_object_mut(did_document)?.insert("alsoKnownAs".to_string(), Value::Array(new_aliases));
402
403    Ok(())
404}
405
406/// Add a `did:scid:vh` alias to `alsoKnownAs` in the DID document (non-interactive).
407///
408/// Converts the `did:webvh` identifier to `did:scid:vh` format and inserts it into
409/// the `alsoKnownAs` array. If the alias already exists, it is not duplicated.
410pub fn add_scid_also_known_as(did_document: &mut Value, did: &str) -> Result<(), DIDWebVHError> {
411    let did_scid_id = DIDWebVHState::convert_webvh_id_to_scid_id(did);
412
413    let also_known_as = did_document.get_mut("alsoKnownAs");
414
415    let Some(also_known_as) = also_known_as else {
416        // There is no alsoKnownAs, add the did:scid
417        ensure_object_mut(did_document)?.insert(
418            "alsoKnownAs".to_string(),
419            Value::Array(vec![Value::String(did_scid_id.to_string())]),
420        );
421        return Ok(());
422    };
423
424    let new_aliases = build_alias_list(also_known_as, &did_scid_id)?;
425
426    ensure_object_mut(did_document)?.insert("alsoKnownAs".to_string(), Value::Array(new_aliases));
427
428    Ok(())
429}
430
431/// Shared helper: collects existing aliases, appending `new_alias` if not already present.
432fn build_alias_list(also_known_as: &Value, new_alias: &str) -> Result<Vec<Value>, DIDWebVHError> {
433    let mut new_aliases = vec![];
434    let mut already_exists = false;
435
436    if let Some(aliases) = also_known_as.as_array() {
437        for alias in aliases {
438            if let Some(alias_str) = alias.as_str() {
439                if alias_str == new_alias {
440                    already_exists = true;
441                }
442                new_aliases.push(alias.clone());
443            }
444        }
445    } else {
446        return Err(DIDWebVHError::DIDError(
447            "alsoKnownAs is not an array".to_string(),
448        ));
449    }
450
451    if !already_exists {
452        new_aliases.push(Value::String(new_alias.to_string()));
453    }
454
455    Ok(new_aliases)
456}
457
458/// Sign witness proofs for a log entry using provided witness secrets (non-interactive).
459///
460/// For each witness node in the active witnesses configuration, looks up the corresponding
461/// secret in `witness_secrets` (keyed by witness DID) and signs a proof.
462///
463/// Returns `Ok(true)` if witness proofs were signed, `Ok(false)` if no witnesses configured.
464///
465/// `witness_secrets` takes `ahash::HashMap` (the crate's default hasher
466/// everywhere) rather than the generic `HashMap<K, V, S>` because downstream
467/// callers uniformly use the same hasher; the pedantic `implicit_hasher`
468/// lint is allowed here at the call site instead of pushing the generic
469/// through a widely-used public signature.
470#[allow(
471    clippy::implicit_hasher,
472    reason = "didwebvh-rs uses ahash::HashMap uniformly; adding an S-generic \
473              is churn without a real interop need."
474)]
475pub async fn sign_witness_proofs<W: Signer>(
476    witness_proofs: &mut WitnessProofCollection,
477    log_entry: &LogEntryState,
478    witnesses: &Option<Arc<Witnesses>>,
479    witness_secrets: &HashMap<String, W>,
480) -> Result<bool, DIDWebVHError> {
481    let Some(witnesses) = witnesses else {
482        return Ok(false);
483    };
484
485    let (_, witness_nodes) = match &**witnesses {
486        Witnesses::Value {
487            threshold,
488            witnesses,
489        } => (threshold, witnesses),
490        _ => {
491            return Err(DIDWebVHError::WitnessProofError(
492                "No valid witness parameter config found".to_string(),
493            ));
494        }
495    };
496
497    for witness in witness_nodes {
498        // Get signer for Witness
499        let Some(secret) = witness_secrets.get(witness.id.as_str()) else {
500            return Err(DIDWebVHError::WitnessProofError(format!(
501                "Couldn't find secret for witness ({})",
502                witness.id
503            )));
504        };
505
506        // Validate the witness signer has a proper did:key verification method
507        validate_did_key_vm(secret.verification_method())?;
508
509        // Generate Signature
510        let proof = DataIntegrityProof::sign(
511            &json!({"versionId": log_entry.get_version_id()}),
512            secret,
513            SignOptions::new(),
514        )
515        .await
516        .map_err(|e| {
517            DIDWebVHError::SCIDError(format!(
518                "Couldn't generate Data Integrity Proof for LogEntry. Reason: {e}",
519            ))
520        })?;
521
522        // Save proof to collection
523        witness_proofs
524            .add_proof(log_entry.get_version_id(), &proof, false)
525            .map_err(|e| DIDWebVHError::WitnessProofError(format!("Error adding proof: {e}")))?;
526    }
527
528    // Strip out any duplicate records where we can
529    witness_proofs.write_optimise_records()?;
530
531    Ok(true)
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use crate::{DIDWebVHState, Multibase, witness::Witness};
538    use affinidi_secrets_resolver::secrets::Secret;
539    use serde_json::json;
540    use std::sync::Arc;
541
542    use crate::test_utils::{did_doc_with_key, key_and_params};
543
544    /// Helper: create a first log entry and its LogEntryState (for witness tests).
545    async fn create_log_entry_state(key: &Secret, params: &Parameters) -> (DIDWebVHState, String) {
546        let mut state = DIDWebVHState::default();
547        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", key);
548        state
549            .create_log_entry(None, &doc, params, key)
550            .await
551            .expect("Failed to create log entry");
552        let version_id = state
553            .log_entries
554            .last()
555            .unwrap()
556            .get_version_id()
557            .to_string();
558        (state, version_id)
559    }
560
561    // -----------------------------------------------------------------------
562    // Builder tests
563    // -----------------------------------------------------------------------
564
565    #[test]
566    fn builder_missing_address() {
567        let (key, params) = key_and_params();
568        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
569        let result = CreateDIDConfig::builder()
570            .authorization_key(key)
571            .did_document(doc)
572            .parameters(params)
573            .build();
574
575        assert!(result.is_err());
576    }
577
578    #[test]
579    fn builder_missing_authorization_keys() {
580        let (key, params) = key_and_params();
581        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
582        let result = CreateDIDConfig::builder()
583            .address("https://example.com/")
584            .did_document(doc)
585            .parameters(params)
586            .build();
587
588        assert!(result.is_err());
589    }
590
591    #[test]
592    fn builder_missing_did_document() {
593        let (key, params) = key_and_params();
594        let result = CreateDIDConfig::builder()
595            .address("https://example.com/")
596            .authorization_key(key)
597            .parameters(params)
598            .build();
599
600        assert!(result.is_err());
601    }
602
603    #[test]
604    fn builder_missing_parameters() {
605        let (key, _) = key_and_params();
606        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
607        let result = CreateDIDConfig::builder()
608            .address("https://example.com/")
609            .authorization_key(key)
610            .did_document(doc)
611            .build();
612
613        assert!(result.is_err());
614    }
615
616    #[test]
617    fn builder_all_required_fields() {
618        let (key, params) = key_and_params();
619        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
620        let result = CreateDIDConfig::builder()
621            .address("https://example.com/")
622            .authorization_key(key)
623            .did_document(doc)
624            .parameters(params)
625            .build();
626
627        assert!(result.is_ok());
628    }
629
630    #[test]
631    fn builder_authorization_keys_replaces() {
632        let key1 = crate::test_utils::generate_signing_key();
633        let key2 = crate::test_utils::generate_signing_key();
634        let (_, params) = key_and_params();
635        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key2);
636
637        let config = CreateDIDConfig::builder()
638            .address("https://example.com/")
639            .authorization_key(key1)
640            .authorization_keys(vec![key2])
641            .did_document(doc)
642            .parameters(params)
643            .build()
644            .unwrap();
645
646        assert_eq!(config.authorization_keys.len(), 1);
647    }
648
649    #[test]
650    fn builder_witness_secrets() {
651        let (key, params) = key_and_params();
652        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
653        let witness_key = crate::test_utils::generate_signing_key();
654
655        let config = CreateDIDConfig::builder()
656            .address("https://example.com/")
657            .authorization_key(key)
658            .did_document(doc)
659            .parameters(params)
660            .witness_secret("did:key:z6Mk1", witness_key)
661            .build()
662            .unwrap();
663
664        assert_eq!(config.witness_secrets.len(), 1);
665        assert!(config.witness_secrets.contains_key("did:key:z6Mk1"));
666    }
667
668    #[test]
669    fn builder_defaults() {
670        let (key, params) = key_and_params();
671        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
672        let config = CreateDIDConfig::builder()
673            .address("https://example.com/")
674            .authorization_key(key)
675            .did_document(doc)
676            .parameters(params)
677            .build()
678            .unwrap();
679
680        assert!(!config.also_known_as_web);
681        assert!(!config.also_known_as_scid);
682        assert!(config.witness_secrets.is_empty());
683    }
684
685    // -----------------------------------------------------------------------
686    // create_did tests
687    // -----------------------------------------------------------------------
688
689    #[tokio::test]
690    async fn create_did_with_url_address() {
691        let (key, params) = key_and_params();
692        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
693        let config = CreateDIDConfig::builder()
694            .address("https://example.com/")
695            .authorization_key(key)
696            .did_document(doc)
697            .parameters(params)
698            .build()
699            .unwrap();
700
701        let result = create_did(config).await;
702        assert!(result.is_ok());
703        let result = result.unwrap();
704        assert!(result.did.starts_with("did:webvh:"));
705        assert!(result.did.contains("example.com"));
706        assert!(!result.did.contains("{SCID}"));
707    }
708
709    #[tokio::test]
710    async fn create_did_with_did_address() {
711        let (key, params) = key_and_params();
712        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
713        let config = CreateDIDConfig::builder()
714            .address("did:webvh:{SCID}:example.com")
715            .authorization_key(key)
716            .did_document(doc)
717            .parameters(params)
718            .build()
719            .unwrap();
720
721        let result = create_did(config).await;
722        assert!(result.is_ok());
723        let result = result.unwrap();
724        assert!(result.did.starts_with("did:webvh:"));
725        assert!(!result.did.contains("{SCID}"));
726    }
727
728    #[tokio::test]
729    async fn create_did_invalid_address() {
730        let (key, _params) = key_and_params();
731        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
732        let config: CreateDIDConfig = CreateDIDConfig {
733            address: "not a valid url or did".to_string(),
734            authorization_keys: vec![key],
735            did_document: doc,
736            parameters: _params,
737            witness_secrets: HashMap::default(),
738            also_known_as_web: false,
739            also_known_as_scid: false,
740            version_time: None,
741        };
742
743        assert!(create_did(config).await.is_err());
744    }
745
746    #[tokio::test]
747    async fn create_did_no_update_keys() {
748        let key = crate::test_utils::generate_signing_key();
749        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
750        let params = Parameters::default(); // no update_keys
751
752        let config = CreateDIDConfig::builder()
753            .address("https://example.com/")
754            .authorization_key(key)
755            .did_document(doc)
756            .parameters(params)
757            .build()
758            .unwrap();
759
760        assert!(create_did(config).await.is_err());
761    }
762
763    #[tokio::test]
764    async fn create_did_with_also_known_as_web() {
765        let (key, params) = key_and_params();
766        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
767        let config = CreateDIDConfig::builder()
768            .address("https://example.com/")
769            .authorization_key(key)
770            .did_document(doc)
771            .parameters(params)
772            .also_known_as_web(true)
773            .build()
774            .unwrap();
775
776        let result = create_did(config).await.unwrap();
777        let state = result.log_entry.get_state();
778        let also_known_as = state.get("alsoKnownAs").unwrap().as_array().unwrap();
779        assert!(
780            also_known_as
781                .iter()
782                .any(|v| { v.as_str().is_some_and(|s| s.starts_with("did:web:")) })
783        );
784    }
785
786    #[tokio::test]
787    async fn create_did_with_also_known_as_scid() {
788        let (key, params) = key_and_params();
789        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
790        let config = CreateDIDConfig::builder()
791            .address("https://example.com/")
792            .authorization_key(key)
793            .did_document(doc)
794            .parameters(params)
795            .also_known_as_scid(true)
796            .build()
797            .unwrap();
798
799        let result = create_did(config).await.unwrap();
800        let state = result.log_entry.get_state();
801        let also_known_as = state.get("alsoKnownAs").unwrap().as_array().unwrap();
802        assert!(
803            also_known_as
804                .iter()
805                .any(|v| { v.as_str().is_some_and(|s| s.starts_with("did:scid:vh:")) })
806        );
807    }
808
809    #[tokio::test]
810    async fn create_did_with_both_aliases() {
811        let (key, params) = key_and_params();
812        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
813        let config = CreateDIDConfig::builder()
814            .address("https://example.com/")
815            .authorization_key(key)
816            .did_document(doc)
817            .parameters(params)
818            .also_known_as_web(true)
819            .also_known_as_scid(true)
820            .build()
821            .unwrap();
822
823        let result = create_did(config).await.unwrap();
824        let state = result.log_entry.get_state();
825        let also_known_as = state.get("alsoKnownAs").unwrap().as_array().unwrap();
826        let has_web = also_known_as
827            .iter()
828            .any(|v| v.as_str().is_some_and(|s| s.starts_with("did:web:")));
829        let has_scid = also_known_as
830            .iter()
831            .any(|v| v.as_str().is_some_and(|s| s.starts_with("did:scid:vh:")));
832        assert!(has_web);
833        assert!(has_scid);
834    }
835
836    #[tokio::test]
837    async fn create_did_no_witnesses_returns_empty_proofs() {
838        let (key, params) = key_and_params();
839        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
840        let config = CreateDIDConfig::builder()
841            .address("https://example.com/")
842            .authorization_key(key)
843            .did_document(doc)
844            .parameters(params)
845            .build()
846            .unwrap();
847
848        let result = create_did(config).await.unwrap();
849        assert_eq!(result.witness_proofs.get_total_count(), 0);
850    }
851
852    #[tokio::test]
853    async fn create_did_with_witnesses() {
854        let (key, _) = key_and_params();
855        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
856        let witness1 = crate::test_utils::generate_signing_key();
857        let witness2 = crate::test_utils::generate_signing_key();
858        let w1_id = witness1.get_public_keymultibase().unwrap();
859        let w2_id = witness2.get_public_keymultibase().unwrap();
860
861        let params = Parameters {
862            update_keys: Some(Arc::new(vec![Multibase::new(
863                key.get_public_keymultibase().unwrap(),
864            )])),
865            witness: Some(Arc::new(Witnesses::Value {
866                threshold: 1,
867                witnesses: vec![
868                    Witness {
869                        id: Multibase::new(w1_id.clone()),
870                    },
871                    Witness {
872                        id: Multibase::new(w2_id.clone()),
873                    },
874                ],
875            })),
876            ..Default::default()
877        };
878
879        let config = CreateDIDConfig::builder()
880            .address("https://example.com/")
881            .authorization_key(key)
882            .did_document(doc)
883            .parameters(params)
884            // Witness ids are canonicalized to `did:key:` form (issue #42), so
885            // the witness-secret lookup map must be keyed the same way.
886            .witness_secret(format!("did:key:{w1_id}"), witness1)
887            .witness_secret(format!("did:key:{w2_id}"), witness2)
888            .build()
889            .unwrap();
890
891        let result = create_did(config).await.unwrap();
892        assert_eq!(result.witness_proofs.get_total_count(), 2);
893    }
894
895    #[tokio::test]
896    async fn create_did_witnesses_missing_secret() {
897        let (key, _) = key_and_params();
898        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
899        let witness1 = crate::test_utils::generate_signing_key();
900        let w1_id = witness1.get_public_keymultibase().unwrap();
901
902        let params = Parameters {
903            update_keys: Some(Arc::new(vec![Multibase::new(
904                key.get_public_keymultibase().unwrap(),
905            )])),
906            witness: Some(Arc::new(Witnesses::Value {
907                threshold: 1,
908                witnesses: vec![Witness {
909                    id: Multibase::new(w1_id),
910                }],
911            })),
912            ..Default::default()
913        };
914
915        // Don't provide the witness secret
916        let config = CreateDIDConfig::builder()
917            .address("https://example.com/")
918            .authorization_key(key)
919            .did_document(doc)
920            .parameters(params)
921            .build()
922            .unwrap();
923
924        assert!(create_did(config).await.is_err());
925    }
926
927    #[tokio::test]
928    async fn create_did_portable() {
929        let (key, _) = key_and_params();
930        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
931        let params = Parameters {
932            update_keys: Some(Arc::new(vec![Multibase::new(
933                key.get_public_keymultibase().unwrap(),
934            )])),
935            portable: Some(true),
936            ..Default::default()
937        };
938
939        let config = CreateDIDConfig::builder()
940            .address("https://example.com/")
941            .authorization_key(key)
942            .did_document(doc)
943            .parameters(params)
944            .build()
945            .unwrap();
946
947        let result = create_did(config).await.unwrap();
948        assert!(result.did.starts_with("did:webvh:"));
949    }
950
951    #[tokio::test]
952    async fn create_did_log_entry_serializable() {
953        let (key, params) = key_and_params();
954        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
955        let config = CreateDIDConfig::builder()
956            .address("https://example.com/")
957            .authorization_key(key)
958            .did_document(doc)
959            .parameters(params)
960            .build()
961            .unwrap();
962
963        let result = create_did(config).await.unwrap();
964        let json = serde_json::to_string(&result.log_entry);
965        assert!(json.is_ok());
966        assert!(!json.unwrap().is_empty());
967    }
968
969    // -----------------------------------------------------------------------
970    // add_web_also_known_as tests
971    // -----------------------------------------------------------------------
972
973    #[test]
974    fn add_web_also_known_as_no_existing() {
975        let mut doc = json!({"id": "did:webvh:abc123:example.com"});
976        add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
977
978        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
979        assert_eq!(aliases.len(), 1);
980        assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
981    }
982
983    #[test]
984    fn add_web_also_known_as_with_existing() {
985        let mut doc = json!({
986            "id": "did:webvh:abc123:example.com",
987            "alsoKnownAs": ["did:example:other"]
988        });
989        add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
990
991        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
992        assert_eq!(aliases.len(), 2);
993        assert!(
994            aliases
995                .iter()
996                .any(|v| v.as_str() == Some("did:example:other"))
997        );
998        assert!(
999            aliases
1000                .iter()
1001                .any(|v| v.as_str() == Some("did:web:example.com"))
1002        );
1003    }
1004
1005    #[test]
1006    fn add_web_also_known_as_already_present() {
1007        let mut doc = json!({
1008            "id": "did:webvh:abc123:example.com",
1009            "alsoKnownAs": ["did:web:example.com"]
1010        });
1011        add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1012
1013        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1014        assert_eq!(aliases.len(), 1);
1015        assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
1016    }
1017
1018    #[test]
1019    fn add_web_also_known_as_not_array() {
1020        let mut doc = json!({
1021            "id": "did:webvh:abc123:example.com",
1022            "alsoKnownAs": "not an array"
1023        });
1024        assert!(add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").is_err());
1025    }
1026
1027    // -----------------------------------------------------------------------
1028    // add_scid_also_known_as tests
1029    // -----------------------------------------------------------------------
1030
1031    #[test]
1032    fn add_scid_also_known_as_no_existing() {
1033        let mut doc = json!({"id": "did:webvh:abc123:example.com"});
1034        add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1035
1036        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1037        assert_eq!(aliases.len(), 1);
1038        assert!(aliases[0].as_str().unwrap().starts_with("did:scid:vh:1:"));
1039    }
1040
1041    #[test]
1042    fn add_scid_also_known_as_with_existing() {
1043        let mut doc = json!({
1044            "id": "did:webvh:abc123:example.com",
1045            "alsoKnownAs": ["did:example:other"]
1046        });
1047        add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1048
1049        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1050        assert_eq!(aliases.len(), 2);
1051        assert!(
1052            aliases
1053                .iter()
1054                .any(|v| v.as_str() == Some("did:example:other"))
1055        );
1056        assert!(
1057            aliases
1058                .iter()
1059                .any(|v| { v.as_str().is_some_and(|s| s.starts_with("did:scid:vh:1:")) })
1060        );
1061    }
1062
1063    #[test]
1064    fn add_scid_also_known_as_already_present() {
1065        let scid_id = DIDWebVHState::convert_webvh_id_to_scid_id("did:webvh:abc123:example.com");
1066        let mut doc = json!({
1067            "id": "did:webvh:abc123:example.com",
1068            "alsoKnownAs": [scid_id]
1069        });
1070        add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1071
1072        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1073        assert_eq!(aliases.len(), 1);
1074    }
1075
1076    #[test]
1077    fn add_scid_also_known_as_not_array() {
1078        let mut doc = json!({
1079            "id": "did:webvh:abc123:example.com",
1080            "alsoKnownAs": 42
1081        });
1082        assert!(add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").is_err());
1083    }
1084
1085    // -----------------------------------------------------------------------
1086    // sign_witness_proofs tests
1087    // -----------------------------------------------------------------------
1088
1089    #[tokio::test]
1090    async fn sign_witness_proofs_no_witnesses() {
1091        let (key, params) = key_and_params();
1092        let (state, _) = create_log_entry_state(&key, &params).await;
1093        let log_entry = state.log_entries.last().unwrap();
1094
1095        let mut proofs = WitnessProofCollection::default();
1096        let result = sign_witness_proofs(
1097            &mut proofs,
1098            log_entry,
1099            &None,
1100            &HashMap::<String, Secret>::default(),
1101        )
1102        .await;
1103        assert!(result.is_ok());
1104        assert!(!result.unwrap()); // false = no witnesses
1105        assert_eq!(proofs.get_total_count(), 0);
1106    }
1107
1108    #[tokio::test]
1109    async fn sign_witness_proofs_with_witnesses() {
1110        let (key, _) = key_and_params();
1111        let witness1 = crate::test_utils::generate_signing_key();
1112        let witness2 = crate::test_utils::generate_signing_key();
1113        let w1_id = witness1.get_public_keymultibase().unwrap();
1114        let w2_id = witness2.get_public_keymultibase().unwrap();
1115
1116        let params = Parameters {
1117            update_keys: Some(Arc::new(vec![Multibase::new(
1118                key.get_public_keymultibase().unwrap(),
1119            )])),
1120            witness: Some(Arc::new(Witnesses::Value {
1121                threshold: 1,
1122                witnesses: vec![
1123                    Witness {
1124                        id: Multibase::new(w1_id.clone()),
1125                    },
1126                    Witness {
1127                        id: Multibase::new(w2_id.clone()),
1128                    },
1129                ],
1130            })),
1131            ..Default::default()
1132        };
1133
1134        let (state, version_id) = create_log_entry_state(&key, &params).await;
1135        let log_entry = state.log_entries.last().unwrap();
1136
1137        let mut secrets = HashMap::default();
1138        // Witness ids are canonicalized to `did:key:` form (issue #42).
1139        secrets.insert(format!("did:key:{w1_id}"), witness1);
1140        secrets.insert(format!("did:key:{w2_id}"), witness2);
1141
1142        let witnesses = log_entry.get_active_witnesses();
1143        let mut proofs = WitnessProofCollection::default();
1144        let result = sign_witness_proofs(&mut proofs, log_entry, &witnesses, &secrets).await;
1145        assert!(result.is_ok());
1146        assert!(result.unwrap()); // true = witnesses signed
1147        assert_eq!(proofs.get_proof_count(&version_id), 2);
1148    }
1149
1150    #[tokio::test]
1151    async fn sign_witness_proofs_missing_secret() {
1152        let (key, _) = key_and_params();
1153        let witness1 = crate::test_utils::generate_signing_key();
1154        let w1_id = witness1.get_public_keymultibase().unwrap();
1155
1156        let params = Parameters {
1157            update_keys: Some(Arc::new(vec![Multibase::new(
1158                key.get_public_keymultibase().unwrap(),
1159            )])),
1160            witness: Some(Arc::new(Witnesses::Value {
1161                threshold: 1,
1162                witnesses: vec![Witness {
1163                    id: Multibase::new(w1_id),
1164                }],
1165            })),
1166            ..Default::default()
1167        };
1168
1169        let (state, _) = create_log_entry_state(&key, &params).await;
1170        let log_entry = state.log_entries.last().unwrap();
1171
1172        let witnesses = log_entry.get_active_witnesses();
1173        let mut proofs = WitnessProofCollection::default();
1174        // Empty secrets map — secret for witness not provided
1175        let result = sign_witness_proofs(
1176            &mut proofs,
1177            log_entry,
1178            &witnesses,
1179            &HashMap::<String, Secret>::default(),
1180        )
1181        .await;
1182        assert!(result.is_err());
1183    }
1184
1185    #[tokio::test]
1186    async fn sign_witness_proofs_empty_witnesses_config() {
1187        let (key, params) = key_and_params();
1188        let (state, _) = create_log_entry_state(&key, &params).await;
1189        let log_entry = state.log_entries.last().unwrap();
1190
1191        let witnesses = Some(Arc::new(Witnesses::Empty {}));
1192        let mut proofs = WitnessProofCollection::default();
1193        let result = sign_witness_proofs(
1194            &mut proofs,
1195            log_entry,
1196            &witnesses,
1197            &HashMap::<String, Secret>::default(),
1198        )
1199        .await;
1200        assert!(result.is_err());
1201    }
1202
1203    // -----------------------------------------------------------------------
1204    // validate_did_key_vm tests
1205    // -----------------------------------------------------------------------
1206
1207    #[test]
1208    fn validate_did_key_vm_accepts_valid() {
1209        let vm = "did:key:z6MkTest#z6MkTest";
1210        assert!(validate_did_key_vm(vm).is_ok());
1211    }
1212
1213    #[test]
1214    fn validate_did_key_vm_rejects_missing_hash() {
1215        let vm = "did:key:z6MkTest";
1216        assert!(validate_did_key_vm(vm).is_err());
1217    }
1218
1219    #[test]
1220    fn validate_did_key_vm_rejects_wrong_prefix() {
1221        let vm = "did:web:example.com#key-0";
1222        assert!(validate_did_key_vm(vm).is_err());
1223    }
1224
1225    #[test]
1226    fn validate_did_key_vm_rejects_empty() {
1227        assert!(validate_did_key_vm("").is_err());
1228    }
1229
1230    // -----------------------------------------------------------------------
1231    // Additional builder tests
1232    // -----------------------------------------------------------------------
1233
1234    #[test]
1235    fn builder_also_known_as_flags() {
1236        let (key, params) = key_and_params();
1237        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
1238        let config = CreateDIDConfig::builder()
1239            .address("https://example.com/")
1240            .authorization_key(key)
1241            .did_document(doc)
1242            .parameters(params)
1243            .also_known_as_web(true)
1244            .also_known_as_scid(true)
1245            .build()
1246            .unwrap();
1247
1248        assert!(config.also_known_as_web);
1249        assert!(config.also_known_as_scid);
1250    }
1251
1252    #[test]
1253    fn builder_multiple_authorization_keys_accumulate() {
1254        let key1 = crate::test_utils::generate_signing_key();
1255        let key2 = crate::test_utils::generate_signing_key();
1256        let (_, params) = key_and_params();
1257        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key1);
1258
1259        let config = CreateDIDConfig::builder()
1260            .address("https://example.com/")
1261            .authorization_key(key1)
1262            .authorization_key(key2)
1263            .did_document(doc)
1264            .parameters(params)
1265            .build()
1266            .unwrap();
1267
1268        assert_eq!(config.authorization_keys.len(), 2);
1269    }
1270
1271    #[test]
1272    fn builder_witness_secrets_bulk_replaces() {
1273        let (key, params) = key_and_params();
1274        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
1275        let w1 = crate::test_utils::generate_signing_key();
1276        let w2 = crate::test_utils::generate_signing_key();
1277
1278        let mut bulk = HashMap::default();
1279        bulk.insert("did:key:z6MkBulk".to_string(), w2);
1280
1281        let config = CreateDIDConfig::builder()
1282            .address("https://example.com/")
1283            .authorization_key(key)
1284            .did_document(doc)
1285            .parameters(params)
1286            .witness_secret("did:key:z6MkSingle", w1)
1287            .witness_secrets(bulk)
1288            .build()
1289            .unwrap();
1290
1291        // Bulk setter replaces the individual one
1292        assert_eq!(config.witness_secrets.len(), 1);
1293        assert!(config.witness_secrets.contains_key("did:key:z6MkBulk"));
1294        assert!(!config.witness_secrets.contains_key("did:key:z6MkSingle"));
1295    }
1296
1297    // -----------------------------------------------------------------------
1298    // Additional create_did tests
1299    // -----------------------------------------------------------------------
1300
1301    #[tokio::test]
1302    async fn create_did_key_with_existing_did_key_id() {
1303        let mut key = crate::test_utils::generate_signing_key();
1304        let pub_mb = key.get_public_keymultibase().unwrap();
1305        key.id = format!("did:key:{pub_mb}#{pub_mb}");
1306
1307        let params = Parameters {
1308            update_keys: Some(Arc::new(vec![Multibase::new(pub_mb)])),
1309            ..Default::default()
1310        };
1311        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
1312
1313        let config = CreateDIDConfig::builder()
1314            .address("https://example.com/")
1315            .authorization_key(key)
1316            .did_document(doc)
1317            .parameters(params)
1318            .build()
1319            .unwrap();
1320
1321        let result = create_did(config).await;
1322        assert!(result.is_ok());
1323    }
1324
1325    #[tokio::test]
1326    async fn create_did_state_has_no_scid_placeholder() {
1327        let (key, params) = key_and_params();
1328        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
1329        let config = CreateDIDConfig::builder()
1330            .address("https://example.com/")
1331            .authorization_key(key)
1332            .did_document(doc)
1333            .parameters(params)
1334            .build()
1335            .unwrap();
1336
1337        let result = create_did(config).await.unwrap();
1338
1339        // Verify SCID placeholder is replaced everywhere
1340        let state_str = serde_json::to_string(result.log_entry.get_state()).unwrap();
1341        assert!(!state_str.contains("{SCID}"));
1342        assert!(!result.did.contains("{SCID}"));
1343    }
1344
1345    #[tokio::test]
1346    async fn create_did_log_entry_has_proof() {
1347        let (key, params) = key_and_params();
1348        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
1349        let config = CreateDIDConfig::builder()
1350            .address("https://example.com/")
1351            .authorization_key(key)
1352            .did_document(doc)
1353            .parameters(params)
1354            .build()
1355            .unwrap();
1356
1357        let result = create_did(config).await.unwrap();
1358        assert!(!result.log_entry.get_proofs().is_empty());
1359    }
1360
1361    #[tokio::test]
1362    async fn create_did_version_id_starts_with_one() {
1363        let (key, params) = key_and_params();
1364        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
1365        let config = CreateDIDConfig::builder()
1366            .address("https://example.com/")
1367            .authorization_key(key)
1368            .did_document(doc)
1369            .parameters(params)
1370            .build()
1371            .unwrap();
1372
1373        let result = create_did(config).await.unwrap();
1374        let version_id = result.log_entry.get_version_id();
1375        assert!(version_id.starts_with("1-"));
1376    }
1377
1378    #[tokio::test]
1379    async fn create_did_with_url_path() {
1380        let (key, params) = key_and_params();
1381        let doc = did_doc_with_key("did:webvh:{SCID}:example.com:dids:alice", &key);
1382        let config = CreateDIDConfig::builder()
1383            .address("https://example.com/dids/alice/")
1384            .authorization_key(key)
1385            .did_document(doc)
1386            .parameters(params)
1387            .build()
1388            .unwrap();
1389
1390        let result = create_did(config).await.unwrap();
1391        assert!(result.did.starts_with("did:webvh:"));
1392        assert!(result.did.contains("example.com"));
1393    }
1394
1395    #[tokio::test]
1396    async fn create_did_result_did_matches_state_id() {
1397        let (key, params) = key_and_params();
1398        let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
1399        let config = CreateDIDConfig::builder()
1400            .address("https://example.com/")
1401            .authorization_key(key)
1402            .did_document(doc)
1403            .parameters(params)
1404            .build()
1405            .unwrap();
1406
1407        let result = create_did(config).await.unwrap();
1408        let state_id = result
1409            .log_entry
1410            .get_state()
1411            .get("id")
1412            .unwrap()
1413            .as_str()
1414            .unwrap();
1415        assert_eq!(result.did, state_id);
1416    }
1417
1418    // -----------------------------------------------------------------------
1419    // Additional add_web_also_known_as tests
1420    // -----------------------------------------------------------------------
1421
1422    #[test]
1423    fn add_web_also_known_as_empty_array() {
1424        let mut doc = json!({
1425            "id": "did:webvh:abc123:example.com",
1426            "alsoKnownAs": []
1427        });
1428        add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1429
1430        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1431        assert_eq!(aliases.len(), 1);
1432        assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
1433    }
1434
1435    #[test]
1436    fn add_web_also_known_as_idempotent() {
1437        let mut doc = json!({"id": "did:webvh:abc123:example.com"});
1438        add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1439        add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1440
1441        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1442        assert_eq!(aliases.len(), 1);
1443        assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
1444    }
1445
1446    #[test]
1447    fn add_web_also_known_as_preserves_all_existing() {
1448        let mut doc = json!({
1449            "id": "did:webvh:abc123:example.com",
1450            "alsoKnownAs": ["did:example:a", "did:example:b", "did:web:example.com"]
1451        });
1452        add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1453
1454        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1455        assert_eq!(aliases.len(), 3);
1456        assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:a")));
1457        assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:b")));
1458        assert!(
1459            aliases
1460                .iter()
1461                .any(|v| v.as_str() == Some("did:web:example.com"))
1462        );
1463    }
1464
1465    // -----------------------------------------------------------------------
1466    // Additional add_scid_also_known_as tests
1467    // -----------------------------------------------------------------------
1468
1469    #[test]
1470    fn add_scid_also_known_as_empty_array() {
1471        let mut doc = json!({
1472            "id": "did:webvh:abc123:example.com",
1473            "alsoKnownAs": []
1474        });
1475        add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1476
1477        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1478        assert_eq!(aliases.len(), 1);
1479        assert!(aliases[0].as_str().unwrap().starts_with("did:scid:vh:1:"));
1480    }
1481
1482    #[test]
1483    fn add_scid_also_known_as_idempotent() {
1484        let mut doc = json!({"id": "did:webvh:abc123:example.com"});
1485        add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1486        add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1487
1488        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1489        assert_eq!(aliases.len(), 1);
1490    }
1491
1492    #[test]
1493    fn add_scid_also_known_as_preserves_all_existing() {
1494        let scid_id = DIDWebVHState::convert_webvh_id_to_scid_id("did:webvh:abc123:example.com");
1495        let mut doc = json!({
1496            "id": "did:webvh:abc123:example.com",
1497            "alsoKnownAs": ["did:example:a", "did:example:b", scid_id]
1498        });
1499        add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
1500
1501        let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
1502        assert_eq!(aliases.len(), 3);
1503        assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:a")));
1504        assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:b")));
1505    }
1506
1507    // -----------------------------------------------------------------------
1508    // Additional sign_witness_proofs tests
1509    // -----------------------------------------------------------------------
1510
1511    #[tokio::test]
1512    async fn sign_witness_proofs_are_verifiable() {
1513        let (key, _) = key_and_params();
1514        let witness1 = crate::test_utils::generate_signing_key();
1515        let w1_id = witness1.get_public_keymultibase().unwrap();
1516
1517        let params = Parameters {
1518            update_keys: Some(Arc::new(vec![Multibase::new(
1519                key.get_public_keymultibase().unwrap(),
1520            )])),
1521            witness: Some(Arc::new(Witnesses::Value {
1522                threshold: 1,
1523                witnesses: vec![Witness {
1524                    id: Multibase::new(w1_id.clone()),
1525                }],
1526            })),
1527            ..Default::default()
1528        };
1529
1530        let (state, version_id) = create_log_entry_state(&key, &params).await;
1531        let log_entry_state = state.log_entries.last().unwrap();
1532
1533        let mut secrets = HashMap::default();
1534        // Witness id is canonicalized to `did:key:` form (issue #42).
1535        secrets.insert(format!("did:key:{w1_id}"), witness1);
1536
1537        let witnesses = log_entry_state.get_active_witnesses();
1538        let mut proofs = WitnessProofCollection::default();
1539        sign_witness_proofs(&mut proofs, log_entry_state, &witnesses, &secrets)
1540            .await
1541            .unwrap();
1542
1543        // Verify the proof can be validated by the log entry
1544        let witness_proof = proofs.get_proofs(&version_id).unwrap();
1545        let validation = log_entry_state.log_entry.validate_witness_proof(
1546            witness_proof.proof.first().unwrap(),
1547            &crate::witness::WitnessVerifyOptions::new(),
1548        );
1549        assert!(validation.is_ok());
1550    }
1551
1552    #[tokio::test]
1553    async fn sign_witness_proofs_returns_true_with_witnesses() {
1554        let (key, _) = key_and_params();
1555        let witness1 = crate::test_utils::generate_signing_key();
1556        let w1_id = witness1.get_public_keymultibase().unwrap();
1557
1558        let params = Parameters {
1559            update_keys: Some(Arc::new(vec![Multibase::new(
1560                key.get_public_keymultibase().unwrap(),
1561            )])),
1562            witness: Some(Arc::new(Witnesses::Value {
1563                threshold: 1,
1564                witnesses: vec![Witness {
1565                    id: Multibase::new(w1_id.clone()),
1566                }],
1567            })),
1568            ..Default::default()
1569        };
1570
1571        let (state, _) = create_log_entry_state(&key, &params).await;
1572        let log_entry_state = state.log_entries.last().unwrap();
1573
1574        let mut secrets = HashMap::default();
1575        // Witness id is canonicalized to `did:key:` form (issue #42).
1576        secrets.insert(format!("did:key:{w1_id}"), witness1);
1577
1578        let witnesses = log_entry_state.get_active_witnesses();
1579        let mut proofs = WitnessProofCollection::default();
1580        let signed = sign_witness_proofs(&mut proofs, log_entry_state, &witnesses, &secrets)
1581            .await
1582            .unwrap();
1583        assert!(signed);
1584    }
1585
1586    #[tokio::test]
1587    async fn sign_witness_proofs_returns_false_no_witnesses() {
1588        let (key, params) = key_and_params();
1589        let (state, _) = create_log_entry_state(&key, &params).await;
1590        let log_entry = state.log_entries.last().unwrap();
1591
1592        let mut proofs = WitnessProofCollection::default();
1593        let signed = sign_witness_proofs(
1594            &mut proofs,
1595            log_entry,
1596            &None,
1597            &HashMap::<String, Secret>::default(),
1598        )
1599        .await
1600        .unwrap();
1601        assert!(!signed);
1602    }
1603
1604    #[test]
1605    fn replace_did_placeholder_replaces_all_occurrences() {
1606        let did = "did:webvh:abc:example.com".to_string();
1607
1608        let mut did_document = json!({
1609            "id": "{DID}",
1610            "@context": ["https://www.w3.org/ns/did/v1"],
1611            "verificationMethod": [{
1612                "id": "{DID}#key-0",
1613                "type": "Multikey",
1614                "publicKeyMultibase": "abcd",
1615                "controller": "{DID}"
1616            }],
1617            "authentication": ["{DID}#key-0"],
1618            "assertionMethod": ["{DID}#key-0"],
1619        });
1620
1621        let expected_document = json!({
1622            "id": "did:webvh:abc:example.com",
1623            "@context": ["https://www.w3.org/ns/did/v1"],
1624            "verificationMethod": [{
1625                "id": "did:webvh:abc:example.com#key-0",
1626                "type": "Multikey",
1627                "publicKeyMultibase": "abcd",
1628                "controller": did
1629            }],
1630            "authentication": ["did:webvh:abc:example.com#key-0"],
1631            "assertionMethod": ["did:webvh:abc:example.com#key-0"],
1632        });
1633
1634        replace_did_placeholder(&mut did_document, &did);
1635
1636        assert_eq!(did_document, expected_document);
1637    }
1638
1639    #[test]
1640    fn replace_did_placeholder_no_op() {
1641        let did = "did:webvh:abc:example.com".to_string();
1642
1643        let mut did_document = json!({
1644            "a": 1,
1645            "b": {
1646                "c": null
1647            }
1648        });
1649
1650        let expected_document = json!({
1651            "a": 1,
1652            "b": {
1653                "c": null
1654            }
1655        });
1656
1657        replace_did_placeholder(&mut did_document, &did);
1658
1659        assert_eq!(did_document, expected_document);
1660    }
1661}