Skip to main content

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