1use affinidi_tdk::secrets_resolver::secrets::Secret;
7use did_hosting_common::did::{
8 DidDocumentOptions, build_did_document, create_log_entry, encode_host,
9};
10use tracing::info;
11
12use crate::auth::session::now_epoch;
13use crate::did_ops::{
14 DidRecord, content_log_key, content_witness_key, did_key, extract_did_id,
15 extract_service_types, owner_key, validate_did_jsonl,
16};
17use crate::error::AppError;
18use crate::store::{KeyspaceHandle, Store};
19
20pub struct BootstrapResult {
22 pub scid: String,
23 pub did_id: String,
24 pub jsonl: String,
25 pub mnemonic: String,
26}
27
28pub async fn root_did_exists(dids_ks: &KeyspaceHandle) -> Result<bool, AppError> {
30 dids_ks.contains_key(did_key(".well-known")).await
31}
32
33pub async fn bootstrap_root_did(
37 store: &Store,
38 dids_ks: &KeyspaceHandle,
39 signing_secret: &Secret,
40 ka_secret: Option<&Secret>,
41 mediator_did: Option<&str>,
42 public_url: &str,
43) -> Result<BootstrapResult, AppError> {
44 bootstrap_did(
45 store,
46 dids_ks,
47 signing_secret,
48 ka_secret,
49 mediator_did,
50 true,
53 true,
54 public_url,
55 ".well-known",
56 )
57 .await
58}
59
60#[allow(clippy::too_many_arguments)]
71pub async fn bootstrap_did(
72 store: &Store,
73 dids_ks: &KeyspaceHandle,
74 signing_secret: &Secret,
75 ka_secret: Option<&Secret>,
76 mediator_did: Option<&str>,
77 advertise_didcomm: bool,
78 advertise_tsp: bool,
79 public_url: &str,
80 mnemonic: &str,
81) -> Result<BootstrapResult, AppError> {
82 if dids_ks.contains_key(did_key(mnemonic)).await? {
84 return Err(AppError::Conflict(format!(
85 "DID at path '{mnemonic}' already exists"
86 )));
87 }
88
89 let host = encode_host(public_url)
90 .map_err(|e| AppError::Config(format!("failed to encode host from public_url: {e}")))?;
91
92 let public_key = signing_secret
93 .get_public_keymultibase()
94 .map_err(|e| AppError::Internal(format!("failed to get public key multibase: {e}")))?;
95
96 let ka_public_key = ka_secret
97 .map(|s| s.get_public_keymultibase())
98 .transpose()
99 .map_err(|e| AppError::Internal(format!("failed to get KA public key multibase: {e}")))?;
100
101 let doc = build_did_document(
102 &host,
103 mnemonic,
104 &public_key,
105 &DidDocumentOptions {
106 key_agreement_multibase: ka_public_key.as_deref(),
107 mediator_endpoint: if advertise_didcomm {
110 mediator_did
111 } else {
112 None
113 },
114 tsp_endpoint: if advertise_tsp { mediator_did } else { None },
115 },
116 );
117
118 let (scid, jsonl) = create_log_entry(&doc, signing_secret)
119 .await
120 .map_err(|e| AppError::Internal(format!("failed to create log entry: {e}")))?;
121
122 let did_id = extract_did_id(&jsonl)
123 .ok_or_else(|| AppError::Internal("failed to extract DID id from log entry".into()))?;
124
125 let mnemonic = mnemonic.to_string();
126 let now = now_epoch();
127
128 let record = DidRecord {
129 owner: "system".to_string(),
130 mnemonic: mnemonic.clone(),
131 created_at: now,
132 updated_at: now,
133 version_count: 1,
134 did_id: Some(did_id.clone()),
135 content_size: jsonl.len() as u64,
136 disabled: false,
137 deleted_at: None,
138
139 method: "webvh".to_string(),
141 domain: String::new(),
142
143 services: extract_service_types(&jsonl),
147 agent_names: Vec::new(),
148 };
149
150 let mut batch = store.batch();
151 batch.insert(dids_ks, did_key(&mnemonic), &record)?;
152 batch.insert_raw(
153 dids_ks,
154 content_log_key(&mnemonic),
155 jsonl.as_bytes().to_vec(),
156 );
157 batch.insert_raw(
158 dids_ks,
159 owner_key("system", &mnemonic),
160 mnemonic.as_bytes().to_vec(),
161 );
162 batch.commit().await?;
163
164 info!(did = %did_id, scid = %scid, path = %mnemonic, "DID bootstrapped");
165
166 Ok(BootstrapResult {
167 scid,
168 did_id,
169 jsonl,
170 mnemonic,
171 })
172}
173
174pub async fn import_root_did(
179 store: &Store,
180 dids_ks: &KeyspaceHandle,
181 jsonl: &str,
182 witness_content: Option<&str>,
183) -> Result<BootstrapResult, AppError> {
184 if root_did_exists(dids_ks).await? {
186 return Err(AppError::Conflict(
187 "root DID (.well-known) already exists".into(),
188 ));
189 }
190
191 import_did_at_path(store, dids_ks, ".well-known", jsonl, witness_content).await
192}
193
194pub async fn import_did_at_path(
200 store: &Store,
201 dids_ks: &KeyspaceHandle,
202 mnemonic: &str,
203 jsonl: &str,
204 witness_content: Option<&str>,
205) -> Result<BootstrapResult, AppError> {
206 if dids_ks.contains_key(did_key(mnemonic)).await? {
208 return Err(AppError::Conflict(format!(
209 "DID at path '{mnemonic}' already exists"
210 )));
211 }
212
213 validate_did_jsonl(jsonl)?;
215
216 let did_id = extract_did_id(jsonl)
217 .ok_or_else(|| AppError::Validation("could not extract DID id from did.jsonl".into()))?;
218
219 let scid = did_id
221 .strip_prefix("did:webvh:")
222 .and_then(|rest| rest.split(':').next())
223 .ok_or_else(|| AppError::Validation("could not extract SCID from DID id".into()))?
224 .to_string();
225
226 if let Some(witness) = witness_content {
228 serde_json::from_str::<serde_json::Value>(witness).map_err(|e| {
229 AppError::Validation(format!("did-witness.json must be valid JSON: {e}"))
230 })?;
231 }
232
233 let mnemonic_str = mnemonic.to_string();
234 let now = now_epoch();
235 let version_count = jsonl.lines().filter(|l| !l.trim().is_empty()).count() as u64;
236
237 let record = DidRecord {
238 owner: "system".to_string(),
239 mnemonic: mnemonic_str.clone(),
240 created_at: now,
241 updated_at: now,
242 version_count,
243 did_id: Some(did_id.clone()),
244 content_size: jsonl.len() as u64,
245 disabled: false,
246 deleted_at: None,
247
248 method: "webvh".to_string(),
250 domain: String::new(),
251
252 services: extract_service_types(jsonl),
253
254 agent_names: Vec::new(),
255 };
256
257 let mut batch = store.batch();
258 batch.insert(dids_ks, did_key(&mnemonic_str), &record)?;
259 batch.insert_raw(
260 dids_ks,
261 content_log_key(&mnemonic_str),
262 jsonl.as_bytes().to_vec(),
263 );
264 batch.insert_raw(
265 dids_ks,
266 owner_key("system", &mnemonic_str),
267 mnemonic_str.as_bytes().to_vec(),
268 );
269 if let Some(witness) = witness_content {
270 batch.insert_raw(
271 dids_ks,
272 content_witness_key(&mnemonic_str),
273 witness.as_bytes().to_vec(),
274 );
275 }
276 batch.commit().await?;
277
278 info!(did = %did_id, scid = %scid, path = %mnemonic_str, "DID imported from files");
279
280 Ok(BootstrapResult {
281 scid,
282 did_id,
283 jsonl: jsonl.to_string(),
284 mnemonic: mnemonic_str,
285 })
286}