vta_sdk/did_templates/mod.rs
1//! DID document templates.
2//!
3//! A template is a JSON file (or embedded built-in) describing the shape of
4//! a DID document with `{TOKEN}` placeholders. Callers render the template
5//! by supplying variable values; the renderer returns a concrete
6//! `serde_json::Value` ready to hand to a DID-method-specific create
7//! operation (e.g. `create_did_webvh`).
8//!
9//! The format is deliberately declarative — no conditionals, no loops, no
10//! includes. Templates that need branching ship as two templates. See the
11//! `format` module docs for the full schema.
12//!
13//! # Scopes
14//!
15//! Templates live in one of three scopes:
16//!
17//! - **Built-in** — embedded in this crate at compile time. Always available.
18//! Load via [`builtin::load_embedded`].
19//! - **Global** (VTA-stored) — super-admin-managed, visible across all
20//! contexts on a given VTA. Managed via REST routes in Phase 2.
21//! - **Context** (VTA-stored) — context-admin-managed, visible only within
22//! one context. Phase 3.
23//!
24//! Resolution order when a caller names a template without explicit scope:
25//! context → global → builtin. Callers can disambiguate with [`Scope`].
26//!
27//! # Example
28//!
29//! ```ignore
30//! use vta_sdk::did_templates::{DidTemplate, TemplateVars};
31//!
32//! let tpl = DidTemplate::load_embedded("didcomm-mediator")?;
33//! let mut vars = TemplateVars::new();
34//! vars.insert_string("DID", "did:webvh:...");
35//! vars.insert_string("SIGNING_KEY_MB", "z6Mk...");
36//! vars.insert_string("KA_KEY_MB", "z6LS...");
37//! vars.insert_string("URL", "https://mediator.example.com");
38//! let doc = tpl.render(&vars)?;
39//! ```
40
41mod builtin;
42mod render;
43mod validate;
44
45#[cfg(test)]
46mod tests;
47
48use std::collections::HashMap;
49use std::path::Path;
50
51use serde::{Deserialize, Serialize};
52use serde_json::Value;
53use thiserror::Error;
54
55pub use builtin::{BUILTIN_NAMES, load_embedded};
56
57/// Minimum supported template `schemaVersion`.
58pub const SCHEMA_VERSION_MIN: u32 = 1;
59/// Maximum supported template `schemaVersion`.
60pub const SCHEMA_VERSION_MAX: u32 = 1;
61
62/// Placeholder names supplied automatically by the renderer. They cannot
63/// appear in a template's `requiredVars` or `optionalVars` — callers and
64/// templates declare only the things the renderer doesn't already know.
65pub const RESERVED_VARS: &[&str] = &[
66 "DID",
67 "SIGNING_KEY_MB",
68 "KA_KEY_MB",
69 "VTA_DID",
70 "VTA_URL",
71 "CONTEXT_ID",
72 "CONTEXT_DID",
73 "NOW",
74];
75
76/// Storage scope for a template. `Builtin` is in-memory only (never written
77/// to the VTA); `Global` and `Context` are persisted by the VTA in Phase 2+.
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(tag = "type", rename_all = "snake_case")]
80#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
81pub enum Scope {
82 Builtin,
83 Global,
84 Context {
85 #[serde(rename = "contextId")]
86 context_id: String,
87 },
88}
89
90/// A parsed DID template. Serialized shape matches the on-disk JSON file.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
93pub struct DidTemplate {
94 #[serde(rename = "schemaVersion")]
95 pub schema_version: u32,
96
97 pub name: String,
98
99 /// Classification hint: `"mediator"`, `"webvh-hosting"`, `"custom"`, …
100 /// Not interpreted by the renderer; consumed by UX (icons, default
101 /// behaviours in setup wizards).
102 pub kind: String,
103
104 #[serde(default)]
105 pub description: Option<String>,
106
107 /// DID methods this template is designed for (e.g. `["webvh", "web"]`).
108 /// Advisory only — not enforced by the renderer.
109 #[serde(default)]
110 pub methods: Vec<String>,
111
112 /// Variables the caller MUST supply. Reserved ambient names are not
113 /// allowed here (see [`RESERVED_VARS`]).
114 #[serde(default, rename = "requiredVars")]
115 pub required_vars: Vec<String>,
116
117 /// Variables with default values. Caller-supplied values override.
118 #[serde(default, rename = "optionalVars")]
119 pub optional_vars: serde_json::Map<String, Value>,
120
121 /// Hints for the CLI / setup wizards (e.g. `preRotationCount`, `portable`).
122 /// Not consumed by the renderer itself.
123 #[serde(default)]
124 pub defaults: serde_json::Map<String, Value>,
125
126 /// The DID document with `{TOKEN}` placeholders.
127 pub document: Value,
128}
129
130impl DidTemplate {
131 /// Parse a template from its JSON representation.
132 pub fn from_json(value: Value) -> Result<Self, TemplateError> {
133 let tpl: DidTemplate = serde_json::from_value(value)?;
134 tpl.validate()?;
135 Ok(tpl)
136 }
137
138 /// Load and parse a template from a JSON file on disk.
139 pub fn load_file(path: impl AsRef<Path>) -> Result<Self, TemplateError> {
140 let path = path.as_ref();
141 let bytes = std::fs::read(path).map_err(|e| TemplateError::Io {
142 path: path.display().to_string(),
143 source: e,
144 })?;
145 let value: Value = serde_json::from_slice(&bytes)?;
146 Self::from_json(value)
147 }
148
149 /// Render the template with the supplied variables, returning a concrete
150 /// DID document ready to hand to a DID-method create operation.
151 ///
152 /// Ambient variables the renderer knows about are picked up from `vars`
153 /// if set (e.g. by the server before handing the vars map to this
154 /// function). Missing required vars, unknown placeholders, or reserved
155 /// names in the wrong place all produce errors.
156 pub fn render(&self, vars: &TemplateVars) -> Result<Value, TemplateError> {
157 render::render(self, vars)
158 }
159
160 /// Structural + semantic lint. Called automatically by [`Self::from_json`].
161 pub fn validate(&self) -> Result<(), TemplateError> {
162 validate::validate(self)
163 }
164}
165
166/// Caller + ambient variables supplied to [`DidTemplate::render`].
167///
168/// Insertion order is preserved for error messages but not semantically
169/// meaningful. Later `insert` calls overwrite earlier ones — this is how
170/// caller-supplied values override ambient defaults populated by the server.
171#[derive(Debug, Clone, Default)]
172pub struct TemplateVars {
173 vars: HashMap<String, Value>,
174}
175
176impl TemplateVars {
177 pub fn new() -> Self {
178 Self::default()
179 }
180
181 /// Insert a variable with any JSON-serializable value.
182 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
183 self.vars.insert(key.into(), value.into());
184 self
185 }
186
187 /// Convenience for string variables (the common case from CLI `--var` flags).
188 pub fn insert_string(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
189 self.vars.insert(key.into(), Value::String(value.into()));
190 self
191 }
192
193 /// Merge another map into this one; values in `other` override existing.
194 pub fn extend(&mut self, other: TemplateVars) {
195 self.vars.extend(other.vars);
196 }
197
198 pub fn get(&self, key: &str) -> Option<&Value> {
199 self.vars.get(key)
200 }
201
202 pub fn contains(&self, key: &str) -> bool {
203 self.vars.contains_key(key)
204 }
205
206 pub fn keys(&self) -> impl Iterator<Item = &String> {
207 self.vars.keys()
208 }
209}
210
211/// A DID template as persisted by the VTA (Phase 2+). The [`DidTemplate`] is
212/// the raw authored shape; this wrapper adds provenance metadata the server
213/// maintains (scope, timestamps, author DID).
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
216#[serde(rename_all = "camelCase")]
217pub struct DidTemplateRecord {
218 #[serde(flatten)]
219 pub template: DidTemplate,
220 pub scope: Scope,
221 /// UTC unix-epoch seconds. Displayed in the operator's local timezone.
222 pub created_at: u64,
223 /// UTC unix-epoch seconds. Displayed in the operator's local timezone.
224 pub updated_at: u64,
225 /// DID of the admin who last wrote this template.
226 pub created_by: String,
227}
228
229/// Errors from template parsing, validation, and rendering.
230#[derive(Debug, Error)]
231pub enum TemplateError {
232 #[error("JSON parse error: {0}")]
233 Json(#[from] serde_json::Error),
234
235 #[error("failed to read template file '{path}': {source}")]
236 Io {
237 path: String,
238 #[source]
239 source: std::io::Error,
240 },
241
242 #[error(
243 "unsupported schemaVersion {found} (this SDK supports {min}..={max}). Upgrade the SDK or downgrade the template."
244 )]
245 UnsupportedSchema { found: u32, min: u32, max: u32 },
246
247 #[error("invalid template: {0}")]
248 Invalid(String),
249
250 #[error("missing required variable(s): {0}. Supply with --var NAME=VALUE.")]
251 MissingVars(String),
252
253 #[error(
254 "unresolved placeholder(s) in rendered document: {0}. This is a bug in the template, not a missing --var."
255 )]
256 Unresolved(String),
257
258 #[error(
259 "reserved variable name '{0}' cannot appear in requiredVars/optionalVars — it is supplied automatically by the renderer"
260 )]
261 ReservedVar(String),
262
263 #[error(
264 "builtin template '{0}' not found (available: ai-agent, ai-agent-peer, did-host-didcomm, did-host-http, did-host-http-didcomm, did-host-http-tsp, did-host-tsp, didcomm-mediator, vta-admin, vtc-host)"
265 )]
266 BuiltinNotFound(String),
267}