The ledger: hash-chained events in the frozen envelope, signed checkpoints, admin verify, check and rebuild (#3) 36537112fcac
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 19ae205fcebb
- When
- · 2026-11-02 21:53 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
36537112fcacca5a7d148db09061afc9b8875ec6df78827c4fa7fee83c67f26b- Depends on
- 258bb3824813 ad258dc8cf5e
- Depended on by
- 862f0db224aa fe6b29c66cd4 87f601ccce6d 8d803d858c8c c7161ec1a607 b044d90930d9 98a0fb9f0948 67bfa06a894f eca5af92279b 77df591d5a2f b576d05cf862 98a16830a3d3 da84e98f104e e071e91a14ef 39f5c913f617 eb675dfa5e18 037c45d8093c 571572f5fb2c 5eb57578f4ef 2fc88a7b3a5c 5d695d869fbf 09752034b5a8
- In bookmarks
- main
- In tags
- v0.1.0
- Edits
- 137 atoms, 13 tree operations, 19 files
Browse the tree at this change
added crates/spectra-core/migrations/0001_ledger.sql +38 −0
| 1 | + | -- The ledger (spec §9.1, §9.5): every event, in order, chained by hash. |
|
| 2 | + | -- Columns are the frozen envelope; `pruned` marks a stub whose payload and |
|
| 3 | + | -- proof were removed by `spectra admin prune` (its hash and prev stay). |
|
| 4 | + | CREATE TABLE events ( |
|
| 5 | + | seq INTEGER PRIMARY KEY, |
|
| 6 | + | id TEXT NOT NULL UNIQUE, |
|
| 7 | + | at_ms INTEGER NOT NULL, |
|
| 8 | + | hlc_wall INTEGER NOT NULL, |
|
| 9 | + | hlc_logical INTEGER NOT NULL, |
|
| 10 | + | prev BLOB NOT NULL, |
|
| 11 | + | hash BLOB NOT NULL, |
|
| 12 | + | auth_key BLOB, |
|
| 13 | + | auth_method INTEGER NOT NULL, |
|
| 14 | + | auth_assurance INTEGER NOT NULL, |
|
| 15 | + | auth_proof BLOB, |
|
| 16 | + | kind TEXT NOT NULL, |
|
| 17 | + | actor TEXT NOT NULL, |
|
| 18 | + | session TEXT, |
|
| 19 | + | project TEXT, |
|
| 20 | + | subject TEXT, |
|
| 21 | + | payload BLOB, |
|
| 22 | + | trace TEXT, |
|
| 23 | + | pruned INTEGER NOT NULL DEFAULT 0 |
|
| 24 | + | ); |
|
| 25 | + | CREATE INDEX events_kind ON events(kind); |
|
| 26 | + | CREATE INDEX events_subject ON events(subject); |
|
| 27 | + | ||
| 28 | + | -- Facts about this database: the ledger format it was written with. |
|
| 29 | + | CREATE TABLE meta ( |
|
| 30 | + | key TEXT PRIMARY KEY, |
|
| 31 | + | value TEXT NOT NULL |
|
| 32 | + | ); |
|
| 33 | + | ||
| 34 | + | -- Projection: how many events of each kind (Radar's Log header, `admin check`). |
|
| 35 | + | CREATE TABLE event_counts ( |
|
| 36 | + | kind TEXT PRIMARY KEY, |
|
| 37 | + | n INTEGER NOT NULL |
|
| 38 | + | ); |
added crates/spectra-core/src/event.rs +282 −0
| 1 | + | //! The event envelope (spec §9.1) and how it is encoded and hashed (§9.5). |
|
| 2 | + | //! |
|
| 3 | + | //! An event's body is deterministic CBOR with integer keys, through |
|
| 4 | + | //! `oxy-core`'s encoder, exactly as Oxygen encodes its objects (oxygen spec |
|
| 5 | + | //! §5.1); its hash is `BLAKE3("spectra1 event " || len || 0x00 || body)`, the |
|
| 6 | + | //! domain-separated form of oxygen spec §5.2. The hash and the proof are not |
|
| 7 | + | //! in the body: signatures sit outside what they sign (oxygen spec §5.4). |
|
| 8 | + | //! |
|
| 9 | + | //! The envelope is frozen from the first release: fields may be added at new |
|
| 10 | + | //! keys, never changed or removed, so a hub merges logs from any version. |
|
| 11 | + | ||
| 12 | + | use crate::hlc::Hlc; |
|
| 13 | + | use oxy_core::cbor::{self, Value}; |
|
| 14 | + | ||
| 15 | + | /// The envelope's version, key 0 of the body. |
|
| 16 | + | pub const ENVELOPE_VERSION: u64 = 1; |
|
| 17 | + | ||
| 18 | + | /// The field names of the envelope, in order: the frozen set (spec §9.1). |
|
| 19 | + | pub const ENVELOPE_FIELDS: &[&str] = |
|
| 20 | + | &["seq", "id", "at_ms", "hlc", "prev", "hash", "auth", "kind", "actor", "session", "project", "subject", "payload", "trace"]; |
|
| 21 | + | ||
| 22 | + | /// How the event's cause was authenticated (spec §8.5). |
|
| 23 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
| 24 | + | pub enum Method { |
|
| 25 | + | /// Made by Spectra itself (presence timers, checkpoints of its own); |
|
| 26 | + | /// covered by the next checkpoint's signature. |
|
| 27 | + | System = 0, |
|
| 28 | + | /// A bearer token: possession only. |
|
| 29 | + | Token = 1, |
|
| 30 | + | /// A request signed with a key on disk. |
|
| 31 | + | Signature = 2, |
|
| 32 | + | /// A passkey assertion with user presence. |
|
| 33 | + | Passkey = 3, |
|
| 34 | + | } |
|
| 35 | + | ||
| 36 | + | impl Method { |
|
| 37 | + | pub fn from_u8(n: u8) -> Option<Method> { |
|
| 38 | + | Some(match n { |
|
| 39 | + | 0 => Method::System, |
|
| 40 | + | 1 => Method::Token, |
|
| 41 | + | 2 => Method::Signature, |
|
| 42 | + | 3 => Method::Passkey, |
|
| 43 | + | _ => return None, |
|
| 44 | + | }) |
|
| 45 | + | } |
|
| 46 | + | pub fn name(self) -> &'static str { |
|
| 47 | + | match self { |
|
| 48 | + | Method::System => "system", |
|
| 49 | + | Method::Token => "token", |
|
| 50 | + | Method::Signature => "signature", |
|
| 51 | + | Method::Passkey => "passkey", |
|
| 52 | + | } |
|
| 53 | + | } |
|
| 54 | + | } |
|
| 55 | + | ||
| 56 | + | /// Who caused an event and the proof of it. `key` and the assurance level |
|
| 57 | + | /// are hashed; `proof` is carried beside the hash. |
|
| 58 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 59 | + | pub struct Auth { |
|
| 60 | + | pub key: Option<[u8; 32]>, |
|
| 61 | + | pub method: Method, |
|
| 62 | + | /// 0 for system events, else 1 to 3 (spec §8.5). |
|
| 63 | + | pub assurance: u8, |
|
| 64 | + | pub proof: Option<Vec<u8>>, |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | impl Auth { |
|
| 68 | + | /// An event Spectra makes itself. |
|
| 69 | + | pub fn system() -> Auth { |
|
| 70 | + | Auth { key: None, method: Method::System, assurance: 0, proof: None } |
|
| 71 | + | } |
|
| 72 | + | } |
|
| 73 | + | ||
| 74 | + | /// An event before it is appended: what the caller decides. |
|
| 75 | + | #[derive(Clone, Debug)] |
|
| 76 | + | pub struct NewEvent { |
|
| 77 | + | pub kind: String, |
|
| 78 | + | pub actor: String, |
|
| 79 | + | pub session: Option<String>, |
|
| 80 | + | pub project: Option<String>, |
|
| 81 | + | pub subject: Option<String>, |
|
| 82 | + | pub payload: serde_json::Value, |
|
| 83 | + | pub trace: Option<String>, |
|
| 84 | + | pub auth: Auth, |
|
| 85 | + | } |
|
| 86 | + | ||
| 87 | + | impl NewEvent { |
|
| 88 | + | /// A system event of `kind` with `payload`. |
|
| 89 | + | pub fn system(kind: &str, payload: serde_json::Value) -> NewEvent { |
|
| 90 | + | NewEvent { |
|
| 91 | + | kind: kind.into(), |
|
| 92 | + | actor: "system".into(), |
|
| 93 | + | session: None, |
|
| 94 | + | project: None, |
|
| 95 | + | subject: None, |
|
| 96 | + | payload, |
|
| 97 | + | trace: None, |
|
| 98 | + | auth: Auth::system(), |
|
| 99 | + | } |
|
| 100 | + | } |
|
| 101 | + | pub fn subject(mut self, s: impl Into<String>) -> NewEvent { |
|
| 102 | + | self.subject = Some(s.into()); |
|
| 103 | + | self |
|
| 104 | + | } |
|
| 105 | + | pub fn project(mut self, p: impl Into<String>) -> NewEvent { |
|
| 106 | + | self.project = Some(p.into()); |
|
| 107 | + | self |
|
| 108 | + | } |
|
| 109 | + | pub fn session(mut self, s: impl Into<String>) -> NewEvent { |
|
| 110 | + | self.session = Some(s.into()); |
|
| 111 | + | self |
|
| 112 | + | } |
|
| 113 | + | pub fn by(mut self, actor: impl Into<String>, auth: Auth) -> NewEvent { |
|
| 114 | + | self.actor = actor.into(); |
|
| 115 | + | self.auth = auth; |
|
| 116 | + | self |
|
| 117 | + | } |
|
| 118 | + | } |
|
| 119 | + | ||
| 120 | + | /// An event in the ledger. |
|
| 121 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 122 | + | pub struct Event { |
|
| 123 | + | pub seq: u64, |
|
| 124 | + | pub id: String, |
|
| 125 | + | pub at_ms: u64, |
|
| 126 | + | pub hlc: Hlc, |
|
| 127 | + | pub prev: [u8; 32], |
|
| 128 | + | pub hash: [u8; 32], |
|
| 129 | + | pub auth: Auth, |
|
| 130 | + | pub kind: String, |
|
| 131 | + | pub actor: String, |
|
| 132 | + | pub session: Option<String>, |
|
| 133 | + | pub project: Option<String>, |
|
| 134 | + | pub subject: Option<String>, |
|
| 135 | + | /// Canonical JSON (sorted keys, no whitespace), hashed as stored; |
|
| 136 | + | /// `None` once pruned to a stub. |
|
| 137 | + | pub payload: Option<Vec<u8>>, |
|
| 138 | + | pub trace: Option<String>, |
|
| 139 | + | } |
|
| 140 | + | ||
| 141 | + | impl Event { |
|
| 142 | + | /// The payload as JSON (`null` for a stub). |
|
| 143 | + | pub fn json(&self) -> serde_json::Value { |
|
| 144 | + | self.payload.as_deref().and_then(|b| serde_json::from_slice(b).ok()).unwrap_or(serde_json::Value::Null) |
|
| 145 | + | } |
|
| 146 | + | ||
| 147 | + | /// The canonical body: every envelope field but `hash` and the proof. |
|
| 148 | + | pub fn body(&self) -> Vec<u8> { |
|
| 149 | + | let opt = |s: &Option<String>| s.as_ref().map(|t| Value::Text(t.clone())).unwrap_or(Value::Null); |
|
| 150 | + | cbor::encode(&Value::map([ |
|
| 151 | + | (0, Value::Uint(ENVELOPE_VERSION)), |
|
| 152 | + | (1, Value::Uint(self.seq)), |
|
| 153 | + | (2, Value::Text(self.id.clone())), |
|
| 154 | + | (3, Value::Uint(self.at_ms)), |
|
| 155 | + | (4, Value::Array(vec![Value::Uint(self.hlc.wall_ms), Value::Uint(self.hlc.logical as u64)])), |
|
| 156 | + | (5, Value::Bytes(self.prev.to_vec())), |
|
| 157 | + | ( |
|
| 158 | + | 6, |
|
| 159 | + | Value::map([ |
|
| 160 | + | (0, self.auth.key.map(|k| Value::Bytes(k.to_vec())).unwrap_or(Value::Null)), |
|
| 161 | + | (1, Value::Uint(self.auth.method as u64)), |
|
| 162 | + | (2, Value::Uint(self.auth.assurance as u64)), |
|
| 163 | + | ]), |
|
| 164 | + | ), |
|
| 165 | + | (7, Value::Text(self.kind.clone())), |
|
| 166 | + | (8, Value::Text(self.actor.clone())), |
|
| 167 | + | (9, opt(&self.session)), |
|
| 168 | + | (10, opt(&self.project)), |
|
| 169 | + | (11, opt(&self.subject)), |
|
| 170 | + | (12, self.payload.as_ref().map(|p| Value::Bytes(p.clone())).unwrap_or(Value::Null)), |
|
| 171 | + | (13, opt(&self.trace)), |
|
| 172 | + | ])) |
|
| 173 | + | } |
|
| 174 | + | ||
| 175 | + | /// The hash this event's fields give; equal to `hash` unless edited. |
|
| 176 | + | pub fn computed_hash(&self) -> [u8; 32] { |
|
| 177 | + | hash_body(&self.body()) |
|
| 178 | + | } |
|
| 179 | + | } |
|
| 180 | + | ||
| 181 | + | /// `BLAKE3("spectra1 event " || decimal(len) || 0x00 || body)`. |
|
| 182 | + | pub fn hash_body(body: &[u8]) -> [u8; 32] { |
|
| 183 | + | let mut h = blake3::Hasher::new(); |
|
| 184 | + | h.update(b"spectra1 event "); |
|
| 185 | + | h.update(body.len().to_string().as_bytes()); |
|
| 186 | + | h.update(&[0]); |
|
| 187 | + | h.update(body); |
|
| 188 | + | *h.finalize().as_bytes() |
|
| 189 | + | } |
|
| 190 | + | ||
| 191 | + | /// JSON with object keys sorted at every level and no whitespace, so equal |
|
| 192 | + | /// payloads are equal bytes whatever built them. |
|
| 193 | + | pub fn canonical_json(v: &serde_json::Value) -> Vec<u8> { |
|
| 194 | + | fn sort(v: &serde_json::Value) -> serde_json::Value { |
|
| 195 | + | match v { |
|
| 196 | + | serde_json::Value::Object(m) => { |
|
| 197 | + | let mut keys: Vec<&String> = m.keys().collect(); |
|
| 198 | + | keys.sort(); |
|
| 199 | + | let mut out = serde_json::Map::new(); |
|
| 200 | + | for k in keys { |
|
| 201 | + | out.insert(k.clone(), sort(&m[k])); |
|
| 202 | + | } |
|
| 203 | + | serde_json::Value::Object(out) |
|
| 204 | + | } |
|
| 205 | + | serde_json::Value::Array(a) => serde_json::Value::Array(a.iter().map(sort).collect()), |
|
| 206 | + | other => other.clone(), |
|
| 207 | + | } |
|
| 208 | + | } |
|
| 209 | + | serde_json::to_vec(&sort(v)).expect("JSON values always serialise") |
|
| 210 | + | } |
|
| 211 | + | ||
| 212 | + | #[cfg(test)] |
|
| 213 | + | mod tests { |
|
| 214 | + | use super::*; |
|
| 215 | + | ||
| 216 | + | fn sample() -> Event { |
|
| 217 | + | Event { |
|
| 218 | + | seq: 7, |
|
| 219 | + | id: "01J0000000000000000000000Z".into(), |
|
| 220 | + | at_ms: 1_700_000_000_000, |
|
| 221 | + | hlc: Hlc { wall_ms: 1_700_000_000_000, logical: 2 }, |
|
| 222 | + | prev: [9; 32], |
|
| 223 | + | hash: [0; 32], |
|
| 224 | + | auth: Auth { key: Some([1; 32]), method: Method::Signature, assurance: 2, proof: Some(vec![5, 5]) }, |
|
| 225 | + | kind: "message.posted".into(), |
|
| 226 | + | actor: "agent:claude-oxygen".into(), |
|
| 227 | + | session: None, |
|
| 228 | + | project: Some("oxygen".into()), |
|
| 229 | + | subject: Some("m1".into()), |
|
| 230 | + | payload: Some(canonical_json(&serde_json::json!({"b": 1, "a": [{"z": 1, "y": 2}]}))), |
|
| 231 | + | trace: None, |
|
| 232 | + | } |
|
| 233 | + | } |
|
| 234 | + | ||
| 235 | + | #[test] |
|
| 236 | + | fn every_field_but_the_proof_changes_the_hash() { |
|
| 237 | + | let e = sample(); |
|
| 238 | + | let h = e.computed_hash(); |
|
| 239 | + | let mut p = e.clone(); |
|
| 240 | + | p.auth.proof = None; |
|
| 241 | + | assert_eq!(p.computed_hash(), h, "the proof is outside the hash"); |
|
| 242 | + | type Edit = Box<dyn Fn(&mut Event)>; |
|
| 243 | + | let edits: Vec<Edit> = vec![ |
|
| 244 | + | Box::new(|e| e.seq += 1), |
|
| 245 | + | Box::new(|e| e.id.push('X')), |
|
| 246 | + | Box::new(|e| e.at_ms += 1), |
|
| 247 | + | Box::new(|e| e.hlc.logical += 1), |
|
| 248 | + | Box::new(|e| e.prev[0] ^= 1), |
|
| 249 | + | Box::new(|e| e.auth.key = None), |
|
| 250 | + | Box::new(|e| e.auth.assurance = 3), |
|
| 251 | + | Box::new(|e| e.auth.method = Method::Token), |
|
| 252 | + | Box::new(|e| e.kind.push('x')), |
|
| 253 | + | Box::new(|e| e.actor.push('x')), |
|
| 254 | + | Box::new(|e| e.session = Some("s".into())), |
|
| 255 | + | Box::new(|e| e.project = None), |
|
| 256 | + | Box::new(|e| e.subject = None), |
|
| 257 | + | Box::new(|e| e.payload = Some(b"{}".to_vec())), |
|
| 258 | + | Box::new(|e| e.trace = Some("t".into())), |
|
| 259 | + | ]; |
|
| 260 | + | for edit in edits { |
|
| 261 | + | let mut x = e.clone(); |
|
| 262 | + | edit(&mut x); |
|
| 263 | + | assert_ne!(x.computed_hash(), h); |
|
| 264 | + | } |
|
| 265 | + | } |
|
| 266 | + | ||
| 267 | + | #[test] |
|
| 268 | + | fn canonical_json_sorts_keys_at_every_level() { |
|
| 269 | + | let a = canonical_json(&serde_json::json!({"b": 1, "a": {"d": 1, "c": 2}})); |
|
| 270 | + | assert_eq!(a, br#"{"a":{"c":2,"d":1},"b":1}"#); |
|
| 271 | + | } |
|
| 272 | + | ||
| 273 | + | #[test] |
|
| 274 | + | fn the_envelope_is_frozen() { |
|
| 275 | + | assert_eq!( |
|
| 276 | + | ENVELOPE_FIELDS.join(","), |
|
| 277 | + | "seq,id,at_ms,hlc,prev,hash,auth,kind,actor,session,project,subject,payload,trace", |
|
| 278 | + | "the envelope is frozen at 0.1: add fields at new keys, never change these (spec §9.1)" |
|
| 279 | + | ); |
|
| 280 | + | assert_eq!(ENVELOPE_VERSION, 1); |
|
| 281 | + | } |
|
| 282 | + | } |
added crates/spectra-core/src/hlc.rs +73 −0
| 1 | + | //! The hybrid logical clock on every event (spec §9.1), with Oxygen's drift |
|
| 2 | + | //! bound (oxygen spec §6.4): observing a remote time never moves this clock |
|
| 3 | + | //! more than five minutes ahead of the local wall clock. |
|
| 4 | + | ||
| 5 | + | use std::sync::Mutex; |
|
| 6 | + | ||
| 7 | + | /// How far ahead of the local clock an observed time may move ours. |
|
| 8 | + | pub const MAX_DRIFT_MS: u64 = 5 * 60 * 1000; |
|
| 9 | + | ||
| 10 | + | /// A hybrid logical clock reading: wall milliseconds and a counter. |
|
| 11 | + | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] |
|
| 12 | + | pub struct Hlc { |
|
| 13 | + | pub wall_ms: u64, |
|
| 14 | + | pub logical: u32, |
|
| 15 | + | } |
|
| 16 | + | ||
| 17 | + | /// The clock: monotonic across calls, whatever the wall clock does. |
|
| 18 | + | #[derive(Debug, Default)] |
|
| 19 | + | pub struct Clock { |
|
| 20 | + | last: Mutex<Hlc>, |
|
| 21 | + | } |
|
| 22 | + | ||
| 23 | + | impl Clock { |
|
| 24 | + | /// A clock that continues from `last` (the ledger's newest reading). |
|
| 25 | + | pub fn starting_at(last: Hlc) -> Clock { |
|
| 26 | + | Clock { last: Mutex::new(last) } |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | /// The next reading for a local event, at wall time `now_ms`. |
|
| 30 | + | pub fn tick(&self, now_ms: u64) -> Hlc { |
|
| 31 | + | let mut last = self.last.lock().unwrap(); |
|
| 32 | + | *last = if now_ms > last.wall_ms { |
|
| 33 | + | Hlc { wall_ms: now_ms, logical: 0 } |
|
| 34 | + | } else { |
|
| 35 | + | Hlc { wall_ms: last.wall_ms, logical: last.logical.saturating_add(1) } |
|
| 36 | + | }; |
|
| 37 | + | *last |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | /// Take a remote reading into account, as on receiving another |
|
| 41 | + | /// Spectra's event; the remote reading is kept as it is, only this |
|
| 42 | + | /// clock's own advance is bounded. |
|
| 43 | + | pub fn observe(&self, remote: Hlc, now_ms: u64) { |
|
| 44 | + | let mut last = self.last.lock().unwrap(); |
|
| 45 | + | let bound = now_ms.saturating_add(MAX_DRIFT_MS); |
|
| 46 | + | let remote = if remote.wall_ms > bound { Hlc { wall_ms: bound, logical: 0 } } else { remote }; |
|
| 47 | + | if remote > *last { |
|
| 48 | + | *last = remote; |
|
| 49 | + | } |
|
| 50 | + | } |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | #[cfg(test)] |
|
| 54 | + | mod tests { |
|
| 55 | + | use super::*; |
|
| 56 | + | ||
| 57 | + | #[test] |
|
| 58 | + | fn ticks_are_monotonic_when_the_wall_clock_stalls_or_steps_back() { |
|
| 59 | + | let c = Clock::default(); |
|
| 60 | + | let a = c.tick(1_000); |
|
| 61 | + | let b = c.tick(1_000); |
|
| 62 | + | let d = c.tick(500); |
|
| 63 | + | assert!(a < b && b < d); |
|
| 64 | + | assert_eq!(c.tick(2_000), Hlc { wall_ms: 2_000, logical: 0 }); |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | #[test] |
|
| 68 | + | fn a_far_future_remote_time_moves_the_clock_by_the_drift_bound_at_most() { |
|
| 69 | + | let c = Clock::default(); |
|
| 70 | + | c.observe(Hlc { wall_ms: 10_000_000_000, logical: 7 }, 1_000); |
|
| 71 | + | assert_eq!(c.tick(1_000).wall_ms, 1_000 + MAX_DRIFT_MS); |
|
| 72 | + | } |
|
| 73 | + | } |
added crates/spectra-core/src/ids.rs +40 −0
| 1 | + | //! Identifiers (spec §9.4): ULIDs, 26 characters of Crockford base32, time |
|
| 2 | + | //! ordered and globally unique, so an id made on one machine merges into a |
|
| 3 | + | //! hub without renaming. |
|
| 4 | + | ||
| 5 | + | use rand::RngCore; |
|
| 6 | + | ||
| 7 | + | const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; |
|
| 8 | + | ||
| 9 | + | /// A new ULID for the current time. |
|
| 10 | + | pub fn ulid() -> String { |
|
| 11 | + | let mut r = [0u8; 16]; |
|
| 12 | + | rand::rngs::OsRng.fill_bytes(&mut r); |
|
| 13 | + | ulid_from(now_ms(), u128::from_be_bytes(r)) |
|
| 14 | + | } |
|
| 15 | + | ||
| 16 | + | /// A ULID from a millisecond time and 80 bits taken from `random`. |
|
| 17 | + | pub fn ulid_from(ms: u64, random: u128) -> String { |
|
| 18 | + | let v: u128 = ((ms as u128 & ((1 << 48) - 1)) << 80) | (random & ((1u128 << 80) - 1)); |
|
| 19 | + | (0..26).map(|i| CROCKFORD[((v >> (125 - 5 * i)) & 31) as usize] as char).collect() |
|
| 20 | + | } |
|
| 21 | + | ||
| 22 | + | /// Milliseconds since the Unix epoch. |
|
| 23 | + | pub fn now_ms() -> u64 { |
|
| 24 | + | std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0) |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | #[cfg(test)] |
|
| 28 | + | mod tests { |
|
| 29 | + | use super::*; |
|
| 30 | + | ||
| 31 | + | #[test] |
|
| 32 | + | fn ulids_are_26_chars_and_sort_by_time() { |
|
| 33 | + | let a = ulid_from(1_000, 5); |
|
| 34 | + | let b = ulid_from(2_000, 0); |
|
| 35 | + | assert_eq!(a.len(), 26); |
|
| 36 | + | assert!(a < b); |
|
| 37 | + | assert_eq!(ulid_from(0, 0), "00000000000000000000000000"); |
|
| 38 | + | assert_ne!(ulid(), ulid()); |
|
| 39 | + | } |
|
| 40 | + | } |
added crates/spectra-core/src/keys.rs +158 −0
| 1 | + | //! Keys and identities on disk, laid out exactly as `oxy` lays them out |
|
| 2 | + | //! (spec §8.1, §21.3): an `OXY_HOME` holds `keys/<KEYID>.secret` (private), |
|
| 3 | + | //! `keys/<KEYID>.identity` and `config.toml` naming the key as `author.key`. |
|
| 4 | + | //! Spectra writes them with `oxy-core`'s own functions, so `oxy` reads what |
|
| 5 | + | //! Spectra made and the other way round. |
|
| 6 | + | //! |
|
| 7 | + | //! The instance key (spec §9.5) is kept in the data directory instead, at |
|
| 8 | + | //! `keys/instance.secret` and `keys/instance.identity`. |
|
| 9 | + | ||
| 10 | + | use anyhow::{bail, Context, Result}; |
|
| 11 | + | use oxy_core::durable::{write_atomic, write_atomic_private}; |
|
| 12 | + | use oxy_core::hash::KeyId; |
|
| 13 | + | use oxy_core::identity::{Identity, SecretKey, Signature}; |
|
| 14 | + | use std::fs; |
|
| 15 | + | use std::path::{Path, PathBuf}; |
|
| 16 | + | ||
| 17 | + | /// A key pair with its identity: the instance, an agent, or the owner. |
|
| 18 | + | #[derive(Clone, Debug)] |
|
| 19 | + | pub struct KeyPair { |
|
| 20 | + | pub key: KeyId, |
|
| 21 | + | pub identity: Identity, |
|
| 22 | + | secret: SecretKey, |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | impl KeyPair { |
|
| 26 | + | /// A fresh key pair named `name`, made now. |
|
| 27 | + | pub fn generate(name: &str, email: &str) -> KeyPair { |
|
| 28 | + | let secret = SecretKey::generate(); |
|
| 29 | + | let identity = secret.identity(name, email, crate::ids::now_ms()); |
|
| 30 | + | KeyPair { key: identity.hash(), identity, secret } |
|
| 31 | + | } |
|
| 32 | + | ||
| 33 | + | /// Sign `message` (a Spectra domain-prefixed byte string, at least 16 |
|
| 34 | + | /// bytes) through Oxygen's challenge signing: the target is |
|
| 35 | + | /// `BLAKE3("oxy1 challenge " || message)`, so a Spectra signature can |
|
| 36 | + | /// never be taken for a change, an attestation or a policy. |
|
| 37 | + | pub fn sign(&self, message: &[u8]) -> Signature { |
|
| 38 | + | self.secret.sign_challenge(&self.key, message).expect("Spectra messages are longer than 16 bytes") |
|
| 39 | + | } |
|
| 40 | + | ||
| 41 | + | /// Make a key pair in an `OXY_HOME` laid out as `oxy identity create` |
|
| 42 | + | /// lays one out, and name it the home's author. |
|
| 43 | + | pub fn create_home(home: &Path, name: &str, email: &str) -> Result<KeyPair> { |
|
| 44 | + | let pair = KeyPair::generate(name, email); |
|
| 45 | + | let keys = home.join("keys"); |
|
| 46 | + | fs::create_dir_all(&keys).with_context(|| format!("creating {}", keys.display()))?; |
|
| 47 | + | #[cfg(unix)] |
|
| 48 | + | { |
|
| 49 | + | use std::os::unix::fs::PermissionsExt; |
|
| 50 | + | fs::set_permissions(home, fs::Permissions::from_mode(0o700))?; |
|
| 51 | + | } |
|
| 52 | + | write_atomic_private(&keys.join(format!("{}.secret", pair.key.to_hex())), &pair.secret.encode(&pair.key))?; |
|
| 53 | + | write_atomic(&keys.join(format!("{}.identity", pair.key.to_hex())), &pair.identity.encode())?; |
|
| 54 | + | let config = home.join("config.toml"); |
|
| 55 | + | oxy_core::repo::Config::set_user(&config, "author.name", name)?; |
|
| 56 | + | oxy_core::repo::Config::set_user(&config, "author.email", email)?; |
|
| 57 | + | oxy_core::repo::Config::set_user(&config, "author.key", &pair.key.to_hex())?; |
|
| 58 | + | Ok(pair) |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | /// The key pair an `OXY_HOME` signs with: `author.key` from its |
|
| 62 | + | /// `config.toml`, else its only secret key. |
|
| 63 | + | pub fn load_home(home: &Path) -> Result<KeyPair> { |
|
| 64 | + | let keys = home.join("keys"); |
|
| 65 | + | let key = match author_key(home)? { |
|
| 66 | + | Some(k) => k, |
|
| 67 | + | None => { |
|
| 68 | + | let secrets: Vec<PathBuf> = fs::read_dir(&keys) |
|
| 69 | + | .with_context(|| format!("reading {}", keys.display()))? |
|
| 70 | + | .filter_map(|e| e.ok().map(|e| e.path())) |
|
| 71 | + | .filter(|p| p.extension().is_some_and(|x| x == "secret")) |
|
| 72 | + | .collect(); |
|
| 73 | + | match secrets.as_slice() { |
|
| 74 | + | [one] => KeyId::from_hex(one.file_stem().and_then(|s| s.to_str()).unwrap_or("")) |
|
| 75 | + | .context("a secret key file is not named by its key")?, |
|
| 76 | + | [] => bail!("{} holds no secret key: an agent home is made by `spectra agent add`", home.display()), |
|
| 77 | + | _ => bail!("{} holds several keys and its config.toml names none (author.key)", home.display()), |
|
| 78 | + | } |
|
| 79 | + | } |
|
| 80 | + | }; |
|
| 81 | + | KeyPair::load(&keys.join(format!("{}.secret", key.to_hex())), &keys.join(format!("{}.identity", key.to_hex()))) |
|
| 82 | + | } |
|
| 83 | + | ||
| 84 | + | /// Read a secret and its identity, checking that they belong together. |
|
| 85 | + | pub fn load(secret: &Path, identity: &Path) -> Result<KeyPair> { |
|
| 86 | + | let (key, secret_key) = SecretKey::decode(&fs::read(secret).with_context(|| format!("reading {}", secret.display()))?) |
|
| 87 | + | .map_err(|e| anyhow::anyhow!("{}: {e}", secret.display()))?; |
|
| 88 | + | let identity = Identity::decode(&fs::read(identity).with_context(|| format!("reading {}", identity.display()))?) |
|
| 89 | + | .map_err(|e| anyhow::anyhow!("{}: {e}", identity.display()))?; |
|
| 90 | + | if identity.hash() != key { |
|
| 91 | + | bail!("{} is not the identity of {}", identity.name, secret.display()); |
|
| 92 | + | } |
|
| 93 | + | if secret_key.identity(&identity.name, &identity.email, identity.created).ed25519_pk != identity.ed25519_pk { |
|
| 94 | + | bail!("the secret key in {} does not match its identity", secret.display()); |
|
| 95 | + | } |
|
| 96 | + | Ok(KeyPair { key, identity, secret: secret_key }) |
|
| 97 | + | } |
|
| 98 | + | ||
| 99 | + | /// The data directory's instance key, made on first use (spec §9.5). |
|
| 100 | + | pub fn instance(data: &Path, name: &str) -> Result<KeyPair> { |
|
| 101 | + | let keys = data.join("keys"); |
|
| 102 | + | let (secret, identity) = (keys.join("instance.secret"), keys.join("instance.identity")); |
|
| 103 | + | if secret.exists() { |
|
| 104 | + | return KeyPair::load(&secret, &identity); |
|
| 105 | + | } |
|
| 106 | + | fs::create_dir_all(&keys)?; |
|
| 107 | + | let pair = KeyPair::generate(name, ""); |
|
| 108 | + | write_atomic_private(&secret, &pair.secret.encode(&pair.key))?; |
|
| 109 | + | write_atomic(&identity, &pair.identity.encode())?; |
|
| 110 | + | Ok(pair) |
|
| 111 | + | } |
|
| 112 | + | } |
|
| 113 | + | ||
| 114 | + | /// The identity an `OXY_HOME` names as its author (`oxy identity export` |
|
| 115 | + | /// without the CLI): for reading the owner's identity at `spectra init`. |
|
| 116 | + | pub fn home_identity(home: &Path) -> Result<Identity> { |
|
| 117 | + | let key = author_key(home)?.with_context(|| format!("{} names no identity (author.key in config.toml)", home.display()))?; |
|
| 118 | + | let path = home.join("keys").join(format!("{}.identity", key.to_hex())); |
|
| 119 | + | Identity::decode(&fs::read(&path).with_context(|| format!("reading {}", path.display()))?) |
|
| 120 | + | .map_err(|e| anyhow::anyhow!("{}: {e}", path.display())) |
|
| 121 | + | } |
|
| 122 | + | ||
| 123 | + | fn author_key(home: &Path) -> Result<Option<KeyId>> { |
|
| 124 | + | let config = home.join("config.toml"); |
|
| 125 | + | let Ok(text) = fs::read_to_string(&config) else { return Ok(None) }; |
|
| 126 | + | let table: toml::Table = text.parse().with_context(|| format!("reading {}", config.display()))?; |
|
| 127 | + | let found = table.get("author").and_then(|a| a.get("key")).or_else(|| table.get("author.key")).and_then(|v| v.as_str()); |
|
| 128 | + | Ok(match found { |
|
| 129 | + | Some(hex) => Some(KeyId::from_hex(hex).with_context(|| format!("{}: author.key is not a key", config.display()))?), |
|
| 130 | + | None => None, |
|
| 131 | + | }) |
|
| 132 | + | } |
|
| 133 | + | ||
| 134 | + | #[cfg(test)] |
|
| 135 | + | mod tests { |
|
| 136 | + | use super::*; |
|
| 137 | + | ||
| 138 | + | #[test] |
|
| 139 | + | fn a_home_made_here_reads_back_and_signs_verifiably() { |
|
| 140 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 141 | + | let made = KeyPair::create_home(dir.path(), "claude-oxygen", "").unwrap(); |
|
| 142 | + | let read = KeyPair::load_home(dir.path()).unwrap(); |
|
| 143 | + | assert_eq!(made.key, read.key); |
|
| 144 | + | assert_eq!(home_identity(dir.path()).unwrap().hash(), made.key); |
|
| 145 | + | let msg = b"spectra1 test message of some length"; |
|
| 146 | + | let sig = read.sign(msg); |
|
| 147 | + | oxy_core::identity::verify_challenge(&made.identity, &sig, msg).unwrap(); |
|
| 148 | + | assert!(oxy_core::identity::verify_challenge(&made.identity, &sig, b"spectra1 another message entirely").is_err()); |
|
| 149 | + | } |
|
| 150 | + | ||
| 151 | + | #[test] |
|
| 152 | + | fn the_instance_key_is_made_once() { |
|
| 153 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 154 | + | let a = KeyPair::instance(dir.path(), "laptop").unwrap(); |
|
| 155 | + | let b = KeyPair::instance(dir.path(), "laptop").unwrap(); |
|
| 156 | + | assert_eq!(a.key, b.key); |
|
| 157 | + | } |
|
| 158 | + | } |
added crates/spectra-core/src/ledger.rs +506 −0
| 1 | + | //! The ledger (spec §9, §9.5): the append-only, hash-chained event log in |
|
| 2 | + | //! SQLite, its checkpoints signed by the instance key, its verification, |
|
| 3 | + | //! and the bus that tells everything else what was appended. |
|
| 4 | + | //! |
|
| 5 | + | //! One writer at a time (a lock around the chain head and SQLite's own), |
|
| 6 | + | //! so `seq` is gap-free and every `prev` is the hash of the event before. |
|
| 7 | + | //! Readers never wait for it (WAL). |
|
| 8 | + | ||
| 9 | + | use crate::event::{canonical_json, Auth, Event, Method, NewEvent}; |
|
| 10 | + | use crate::hlc::{Clock, Hlc}; |
|
| 11 | + | use crate::keys::KeyPair; |
|
| 12 | + | use crate::{ids, projections}; |
|
| 13 | + | use anyhow::{bail, Context, Result}; |
|
| 14 | + | use oxy_core::hash::KeyId; |
|
| 15 | + | use oxy_core::identity::{Identity, Signature}; |
|
| 16 | + | use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteRow, SqliteSynchronous}; |
|
| 17 | + | use sqlx::{Row, SqlitePool}; |
|
| 18 | + | use std::path::Path; |
|
| 19 | + | use std::sync::Arc; |
|
| 20 | + | use tokio::sync::{broadcast, Mutex}; |
|
| 21 | + | ||
| 22 | + | /// Events between checkpoints (spec §9.5). |
|
| 23 | + | pub const CHECKPOINT_EVERY: u64 = 256; |
|
| 24 | + | /// The longest a written event waits for a checkpoint (spec §9.5). |
|
| 25 | + | pub const CHECKPOINT_INTERVAL_MS: u64 = 10 * 60 * 1000; |
|
| 26 | + | ||
| 27 | + | const CHECKPOINT_DOMAIN: &[u8] = b"spectra1 checkpoint "; |
|
| 28 | + | ||
| 29 | + | /// The ledger: the database, the chain head, the clock and the bus. |
|
| 30 | + | pub struct Ledger { |
|
| 31 | + | pool: SqlitePool, |
|
| 32 | + | head: Mutex<Head>, |
|
| 33 | + | clock: Clock, |
|
| 34 | + | bus: broadcast::Sender<Arc<Event>>, |
|
| 35 | + | instance: Option<KeyPair>, |
|
| 36 | + | } |
|
| 37 | + | ||
| 38 | + | #[derive(Clone, Copy, Debug)] |
|
| 39 | + | struct Head { |
|
| 40 | + | seq: u64, |
|
| 41 | + | hash: [u8; 32], |
|
| 42 | + | since_checkpoint: u64, |
|
| 43 | + | oldest_unchecked_ms: Option<u64>, |
|
| 44 | + | } |
|
| 45 | + | ||
| 46 | + | impl Ledger { |
|
| 47 | + | /// Open (or create) `path`, apply migrations, and refuse a ledger |
|
| 48 | + | /// written by a newer format. Checkpoints are signed with `instance`; |
|
| 49 | + | /// without one (tests, read-only tools) none are made. |
|
| 50 | + | pub async fn open(path: &Path, instance: Option<KeyPair>) -> Result<Ledger> { |
|
| 51 | + | let opts = SqliteConnectOptions::new() |
|
| 52 | + | .filename(path) |
|
| 53 | + | .create_if_missing(true) |
|
| 54 | + | .journal_mode(SqliteJournalMode::Wal) |
|
| 55 | + | .synchronous(SqliteSynchronous::Normal) |
|
| 56 | + | .foreign_keys(true) |
|
| 57 | + | .busy_timeout(std::time::Duration::from_secs(10)); |
|
| 58 | + | let pool = |
|
| 59 | + | SqlitePoolOptions::new().max_connections(8).connect_with(opts).await.with_context(|| format!("opening {}", path.display()))?; |
|
| 60 | + | sqlx::migrate!("./migrations").run(&pool).await.context("applying migrations")?; |
|
| 61 | + | let format: Option<String> = sqlx::query_scalar("SELECT value FROM meta WHERE key = 'ledger_format'").fetch_optional(&pool).await?; |
|
| 62 | + | match format.as_deref().map(str::parse::<u32>) { |
|
| 63 | + | None => { |
|
| 64 | + | sqlx::query("INSERT INTO meta (key, value) VALUES ('ledger_format', ?)") |
|
| 65 | + | .bind(crate::LEDGER_FORMAT.to_string()) |
|
| 66 | + | .execute(&pool) |
|
| 67 | + | .await?; |
|
| 68 | + | } |
|
| 69 | + | Some(Ok(f)) if f <= crate::LEDGER_FORMAT => {} |
|
| 70 | + | Some(_) => { |
|
| 71 | + | bail!("{} was written by a newer Spectra (ledger format {}): upgrade spectra", path.display(), format.unwrap_or_default()) |
|
| 72 | + | } |
|
| 73 | + | } |
|
| 74 | + | let last = |
|
| 75 | + | sqlx::query("SELECT seq, hash, hlc_wall, hlc_logical FROM events ORDER BY seq DESC LIMIT 1").fetch_optional(&pool).await?; |
|
| 76 | + | let (seq, hash, hlc) = match last { |
|
| 77 | + | Some(r) => ( |
|
| 78 | + | r.get::<i64, _>(0) as u64, |
|
| 79 | + | to32(r.get::<Vec<u8>, _>(1))?, |
|
| 80 | + | Hlc { wall_ms: r.get::<i64, _>(2) as u64, logical: r.get::<i64, _>(3) as u32 }, |
|
| 81 | + | ), |
|
| 82 | + | None => (0, [0; 32], Hlc::default()), |
|
| 83 | + | }; |
|
| 84 | + | let checkpoint_seq: i64 = |
|
| 85 | + | sqlx::query_scalar("SELECT COALESCE(MAX(seq), 0) FROM events WHERE kind = 'ledger.checkpoint'").fetch_one(&pool).await?; |
|
| 86 | + | let since = seq.saturating_sub(checkpoint_seq as u64); |
|
| 87 | + | let (bus, _) = broadcast::channel(4096); |
|
| 88 | + | Ok(Ledger { |
|
| 89 | + | pool, |
|
| 90 | + | head: Mutex::new(Head { seq, hash, since_checkpoint: since, oldest_unchecked_ms: (since > 0).then(ids::now_ms) }), |
|
| 91 | + | clock: Clock::starting_at(hlc), |
|
| 92 | + | bus, |
|
| 93 | + | instance, |
|
| 94 | + | }) |
|
| 95 | + | } |
|
| 96 | + | ||
| 97 | + | /// The database, for projections' readers. |
|
| 98 | + | pub fn pool(&self) -> &SqlitePool { |
|
| 99 | + | &self.pool |
|
| 100 | + | } |
|
| 101 | + | ||
| 102 | + | /// Every appended event, as it commits. |
|
| 103 | + | pub fn subscribe(&self) -> broadcast::Receiver<Arc<Event>> { |
|
| 104 | + | self.bus.subscribe() |
|
| 105 | + | } |
|
| 106 | + | ||
| 107 | + | /// The instance key, when this ledger signs checkpoints. |
|
| 108 | + | pub fn instance(&self) -> Option<&KeyPair> { |
|
| 109 | + | self.instance.as_ref() |
|
| 110 | + | } |
|
| 111 | + | ||
| 112 | + | /// The newest event's sequence number and hash. |
|
| 113 | + | pub async fn head(&self) -> (u64, [u8; 32]) { |
|
| 114 | + | let h = self.head.lock().await; |
|
| 115 | + | (h.seq, h.hash) |
|
| 116 | + | } |
|
| 117 | + | ||
| 118 | + | /// Append `events` in one transaction, chained and projected, then |
|
| 119 | + | /// publish them; a checkpoint follows when one is due. |
|
| 120 | + | pub async fn append(&self, events: Vec<NewEvent>) -> Result<Vec<Arc<Event>>> { |
|
| 121 | + | let mut head = self.head.lock().await; |
|
| 122 | + | let mut tx = self.pool.begin().await?; |
|
| 123 | + | let mut out = Vec::with_capacity(events.len() + 1); |
|
| 124 | + | let mut h = *head; |
|
| 125 | + | for n in events { |
|
| 126 | + | let e = self.chain(&mut h, n); |
|
| 127 | + | insert(&mut tx, &e).await?; |
|
| 128 | + | projections::apply(&mut tx, &e).await?; |
|
| 129 | + | h.since_checkpoint += 1; |
|
| 130 | + | h.oldest_unchecked_ms.get_or_insert(e.at_ms); |
|
| 131 | + | out.push(e); |
|
| 132 | + | // Every 256 events exactly, even inside a large batch. |
|
| 133 | + | if h.since_checkpoint >= CHECKPOINT_EVERY && self.instance.is_some() { |
|
| 134 | + | let cp = self.checkpoint_event(&mut h); |
|
| 135 | + | insert(&mut tx, &cp).await?; |
|
| 136 | + | projections::apply(&mut tx, &cp).await?; |
|
| 137 | + | out.push(cp); |
|
| 138 | + | } |
|
| 139 | + | } |
|
| 140 | + | let due = h.oldest_unchecked_ms.is_some_and(|t| ids::now_ms().saturating_sub(t) >= CHECKPOINT_INTERVAL_MS); |
|
| 141 | + | if due && self.instance.is_some() { |
|
| 142 | + | let cp = self.checkpoint_event(&mut h); |
|
| 143 | + | insert(&mut tx, &cp).await?; |
|
| 144 | + | projections::apply(&mut tx, &cp).await?; |
|
| 145 | + | out.push(cp); |
|
| 146 | + | } |
|
| 147 | + | tx.commit().await?; |
|
| 148 | + | *head = h; |
|
| 149 | + | drop(head); |
|
| 150 | + | let out: Vec<Arc<Event>> = out.into_iter().map(Arc::new).collect(); |
|
| 151 | + | for e in &out { |
|
| 152 | + | let _ = self.bus.send(e.clone()); |
|
| 153 | + | } |
|
| 154 | + | Ok(out) |
|
| 155 | + | } |
|
| 156 | + | ||
| 157 | + | /// Append one event. |
|
| 158 | + | pub async fn append_one(&self, e: NewEvent) -> Result<Arc<Event>> { |
|
| 159 | + | Ok(self.append(vec![e]).await?.remove(0)) |
|
| 160 | + | } |
|
| 161 | + | ||
| 162 | + | /// Write a checkpoint now if any event is not covered by one (at |
|
| 163 | + | /// shutdown, and from the ten-minute timer). |
|
| 164 | + | pub async fn checkpoint(&self) -> Result<Option<Arc<Event>>> { |
|
| 165 | + | let Some(_) = &self.instance else { return Ok(None) }; |
|
| 166 | + | let mut head = self.head.lock().await; |
|
| 167 | + | if head.since_checkpoint == 0 { |
|
| 168 | + | return Ok(None); |
|
| 169 | + | } |
|
| 170 | + | let mut h = *head; |
|
| 171 | + | let cp = self.checkpoint_event(&mut h); |
|
| 172 | + | let mut tx = self.pool.begin().await?; |
|
| 173 | + | insert(&mut tx, &cp).await?; |
|
| 174 | + | projections::apply(&mut tx, &cp).await?; |
|
| 175 | + | tx.commit().await?; |
|
| 176 | + | *head = h; |
|
| 177 | + | drop(head); |
|
| 178 | + | let cp = Arc::new(cp); |
|
| 179 | + | let _ = self.bus.send(cp.clone()); |
|
| 180 | + | Ok(Some(cp)) |
|
| 181 | + | } |
|
| 182 | + | ||
| 183 | + | fn chain(&self, h: &mut Head, n: NewEvent) -> Event { |
|
| 184 | + | let at_ms = ids::now_ms(); |
|
| 185 | + | let mut e = Event { |
|
| 186 | + | seq: h.seq + 1, |
|
| 187 | + | id: ids::ulid(), |
|
| 188 | + | at_ms, |
|
| 189 | + | hlc: self.clock.tick(at_ms), |
|
| 190 | + | prev: h.hash, |
|
| 191 | + | hash: [0; 32], |
|
| 192 | + | auth: n.auth, |
|
| 193 | + | kind: n.kind, |
|
| 194 | + | actor: n.actor, |
|
| 195 | + | session: n.session, |
|
| 196 | + | project: n.project, |
|
| 197 | + | subject: n.subject, |
|
| 198 | + | payload: Some(canonical_json(&n.payload)), |
|
| 199 | + | trace: n.trace, |
|
| 200 | + | }; |
|
| 201 | + | e.hash = e.computed_hash(); |
|
| 202 | + | h.seq = e.seq; |
|
| 203 | + | h.hash = e.hash; |
|
| 204 | + | e |
|
| 205 | + | } |
|
| 206 | + | ||
| 207 | + | /// A checkpoint naming the head `h` holds, signed by the instance key |
|
| 208 | + | /// over `"spectra1 checkpoint " || hash || seq` (spec §9.5). |
|
| 209 | + | fn checkpoint_event(&self, h: &mut Head) -> Event { |
|
| 210 | + | let instance = self.instance.as_ref().expect("checked by callers"); |
|
| 211 | + | let (seq, hash, count) = (h.seq, h.hash, h.since_checkpoint); |
|
| 212 | + | let sig = instance.sign(&checkpoint_message(seq, &hash)); |
|
| 213 | + | let n = NewEvent::system("ledger.checkpoint", serde_json::json!({ "seq": seq, "hash": hex::encode(hash), "count": count })).by( |
|
| 214 | + | "system", |
|
| 215 | + | Auth { |
|
| 216 | + | key: Some(instance.key.0), |
|
| 217 | + | method: Method::Signature, |
|
| 218 | + | assurance: 2, |
|
| 219 | + | proof: Some(oxy_core::cbor::encode(&sig.to_value())), |
|
| 220 | + | }, |
|
| 221 | + | ); |
|
| 222 | + | let e = self.chain(h, n); |
|
| 223 | + | h.since_checkpoint = 0; |
|
| 224 | + | h.oldest_unchecked_ms = None; |
|
| 225 | + | e |
|
| 226 | + | } |
|
| 227 | + | ||
| 228 | + | /// Events from `from` (inclusive), oldest first, at most `limit`. |
|
| 229 | + | pub async fn events(&self, from: u64, limit: u32) -> Result<Vec<Event>> { |
|
| 230 | + | let rows = sqlx::query(&format!("{SELECT_EVENT} WHERE seq >= ? ORDER BY seq LIMIT ?")) |
|
| 231 | + | .bind(from as i64) |
|
| 232 | + | .bind(limit as i64) |
|
| 233 | + | .fetch_all(&self.pool) |
|
| 234 | + | .await?; |
|
| 235 | + | rows.iter().map(row_to_event).collect() |
|
| 236 | + | } |
|
| 237 | + | ||
| 238 | + | /// Recompute the chain and check every checkpoint and every proof it |
|
| 239 | + | /// can, from the ledger alone (spec §9.5, `spectra admin verify`). |
|
| 240 | + | pub async fn verify(&self, keys: &dyn KeyResolver) -> Result<Verification> { |
|
| 241 | + | let mut v = Verification::default(); |
|
| 242 | + | let mut prev = [0u8; 32]; |
|
| 243 | + | let mut expect = 1u64; |
|
| 244 | + | let mut instance: Option<Identity> = None; |
|
| 245 | + | let mut from = 1; |
|
| 246 | + | loop { |
|
| 247 | + | let batch = self.events(from, 2_000).await?; |
|
| 248 | + | if batch.is_empty() { |
|
| 249 | + | break; |
|
| 250 | + | } |
|
| 251 | + | for e in &batch { |
|
| 252 | + | v.events += 1; |
|
| 253 | + | if e.seq != expect { |
|
| 254 | + | v.finding(e.seq, Problem::Gap, format!("expected event {expect}")); |
|
| 255 | + | expect = e.seq; |
|
| 256 | + | } |
|
| 257 | + | if e.prev != prev { |
|
| 258 | + | v.finding(e.seq, Problem::ChainBreak, "prev is not the hash of the event before".into()); |
|
| 259 | + | } |
|
| 260 | + | if e.payload.is_some() && e.computed_hash() != e.hash { |
|
| 261 | + | v.finding(e.seq, Problem::HashMismatch, "the event's fields do not give its hash: it was edited".into()); |
|
| 262 | + | } |
|
| 263 | + | if e.kind == "identity.instance" { |
|
| 264 | + | instance = e |
|
| 265 | + | .json() |
|
| 266 | + | .get("identity") |
|
| 267 | + | .and_then(|h| h.as_str()) |
|
| 268 | + | .and_then(|h| hex::decode(h).ok()) |
|
| 269 | + | .and_then(|b| Identity::decode(&b).ok()); |
|
| 270 | + | } |
|
| 271 | + | if e.kind == "ledger.checkpoint" { |
|
| 272 | + | v.checkpoints += 1; |
|
| 273 | + | match check_checkpoint(e, prev, instance.as_ref()) { |
|
| 274 | + | Ok(()) => v.last_checkpoint = Some(e.seq), |
|
| 275 | + | Err(why) => v.finding(e.seq, Problem::BadCheckpoint, why), |
|
| 276 | + | } |
|
| 277 | + | } else if e.auth.method == Method::Signature || e.auth.method == Method::Passkey { |
|
| 278 | + | if let Err(why) = keys.check(e) { |
|
| 279 | + | v.finding(e.seq, Problem::BadProof, why); |
|
| 280 | + | } |
|
| 281 | + | } |
|
| 282 | + | prev = e.hash; |
|
| 283 | + | expect += 1; |
|
| 284 | + | } |
|
| 285 | + | from = batch.last().map(|e| e.seq + 1).unwrap_or(from); |
|
| 286 | + | } |
|
| 287 | + | v.head = prev; |
|
| 288 | + | v.uncovered = v.events.saturating_sub(v.last_checkpoint.unwrap_or(0)); |
|
| 289 | + | Ok(v) |
|
| 290 | + | } |
|
| 291 | + | ||
| 292 | + | /// Clear every projection and replay the ledger into them. |
|
| 293 | + | pub async fn rebuild(&self) -> Result<()> { |
|
| 294 | + | let _head = self.head.lock().await; |
|
| 295 | + | let mut tx = self.pool.begin().await?; |
|
| 296 | + | replay(&mut tx).await?; |
|
| 297 | + | tx.commit().await?; |
|
| 298 | + | Ok(()) |
|
| 299 | + | } |
|
| 300 | + | ||
| 301 | + | /// Whether the projections equal a rebuild from the ledger: rebuild in |
|
| 302 | + | /// a transaction, compare, and roll back. Returns the tables that differ. |
|
| 303 | + | pub async fn check(&self) -> Result<Vec<String>> { |
|
| 304 | + | let _head = self.head.lock().await; |
|
| 305 | + | let mut tx = self.pool.begin().await?; |
|
| 306 | + | let mut before = Vec::new(); |
|
| 307 | + | for t in projections::TABLES { |
|
| 308 | + | before.push(dump(&mut tx, t).await?); |
|
| 309 | + | } |
|
| 310 | + | replay(&mut tx).await?; |
|
| 311 | + | let mut differ = Vec::new(); |
|
| 312 | + | for (t, b) in projections::TABLES.iter().zip(before) { |
|
| 313 | + | if dump(&mut tx, t).await? != b { |
|
| 314 | + | differ.push(t.to_string()); |
|
| 315 | + | } |
|
| 316 | + | } |
|
| 317 | + | tx.rollback().await?; |
|
| 318 | + | Ok(differ) |
|
| 319 | + | } |
|
| 320 | + | } |
|
| 321 | + | ||
| 322 | + | /// `"spectra1 checkpoint " || hash || seq (8 bytes, big-endian)`. |
|
| 323 | + | pub fn checkpoint_message(seq: u64, hash: &[u8; 32]) -> Vec<u8> { |
|
| 324 | + | let mut m = CHECKPOINT_DOMAIN.to_vec(); |
|
| 325 | + | m.extend_from_slice(hash); |
|
| 326 | + | m.extend_from_slice(&seq.to_be_bytes()); |
|
| 327 | + | m |
|
| 328 | + | } |
|
| 329 | + | ||
| 330 | + | fn check_checkpoint(e: &Event, prev: [u8; 32], instance: Option<&Identity>) -> std::result::Result<(), String> { |
|
| 331 | + | let p = e.json(); |
|
| 332 | + | let seq = p.get("seq").and_then(|s| s.as_u64()).ok_or("no seq")?; |
|
| 333 | + | let hash: [u8; 32] = |
|
| 334 | + | p.get("hash").and_then(|h| h.as_str()).and_then(|h| hex::decode(h).ok()).and_then(|b| b.try_into().ok()).ok_or("no hash")?; |
|
| 335 | + | if seq + 1 != e.seq || hash != prev { |
|
| 336 | + | return Err("names a head other than the event before it".into()); |
|
| 337 | + | } |
|
| 338 | + | let instance = instance.ok_or("no instance identity precedes it")?; |
|
| 339 | + | if e.auth.key != Some(instance.hash().0) { |
|
| 340 | + | return Err("signed by a key other than the instance's".into()); |
|
| 341 | + | } |
|
| 342 | + | let proof = e.auth.proof.as_deref().ok_or("unsigned")?; |
|
| 343 | + | let sig = oxy_core::cbor::decode(proof).ok().and_then(|v| Signature::from_value(&v).ok()).ok_or("malformed signature")?; |
|
| 344 | + | oxy_core::identity::verify_challenge(instance, &sig, &checkpoint_message(seq, &hash)) |
|
| 345 | + | .map_err(|e| format!("signature does not verify: {e}")) |
|
| 346 | + | } |
|
| 347 | + | ||
| 348 | + | /// Checks the proof of a signed event: the identities and delegations that |
|
| 349 | + | /// say whose key it was (spec §8.5). Principals supply it (`principals`). |
|
| 350 | + | pub trait KeyResolver: Send + Sync { |
|
| 351 | + | fn check(&self, e: &Event) -> std::result::Result<(), String>; |
|
| 352 | + | } |
|
| 353 | + | ||
| 354 | + | /// A resolver that knows no keys: every signed event is reported. |
|
| 355 | + | pub struct NoKeys; |
|
| 356 | + | impl KeyResolver for NoKeys { |
|
| 357 | + | fn check(&self, _: &Event) -> std::result::Result<(), String> { |
|
| 358 | + | Err("no identity is known for its key".into()) |
|
| 359 | + | } |
|
| 360 | + | } |
|
| 361 | + | ||
| 362 | + | /// What `verify` found. |
|
| 363 | + | #[derive(Clone, Debug, Default, serde::Serialize)] |
|
| 364 | + | pub struct Verification { |
|
| 365 | + | pub events: u64, |
|
| 366 | + | pub checkpoints: u64, |
|
| 367 | + | pub last_checkpoint: Option<u64>, |
|
| 368 | + | /// Events after the last good checkpoint: chained, not yet signed. |
|
| 369 | + | pub uncovered: u64, |
|
| 370 | + | #[serde(serialize_with = "ser_hex")] |
|
| 371 | + | pub head: [u8; 32], |
|
| 372 | + | pub findings: Vec<Finding>, |
|
| 373 | + | } |
|
| 374 | + | ||
| 375 | + | fn ser_hex<S: serde::Serializer>(h: &[u8; 32], s: S) -> std::result::Result<S::Ok, S::Error> { |
|
| 376 | + | s.serialize_str(&hex::encode(h)) |
|
| 377 | + | } |
|
| 378 | + | ||
| 379 | + | impl Verification { |
|
| 380 | + | pub fn clean(&self) -> bool { |
|
| 381 | + | self.findings.is_empty() |
|
| 382 | + | } |
|
| 383 | + | fn finding(&mut self, seq: u64, problem: Problem, detail: String) { |
|
| 384 | + | self.findings.push(Finding { seq, problem, detail }); |
|
| 385 | + | } |
|
| 386 | + | } |
|
| 387 | + | ||
| 388 | + | /// One thing wrong with the ledger. |
|
| 389 | + | #[derive(Clone, Debug, serde::Serialize)] |
|
| 390 | + | pub struct Finding { |
|
| 391 | + | pub seq: u64, |
|
| 392 | + | pub problem: Problem, |
|
| 393 | + | pub detail: String, |
|
| 394 | + | } |
|
| 395 | + | ||
| 396 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] |
|
| 397 | + | #[serde(rename_all = "snake_case")] |
|
| 398 | + | pub enum Problem { |
|
| 399 | + | Gap, |
|
| 400 | + | ChainBreak, |
|
| 401 | + | HashMismatch, |
|
| 402 | + | BadCheckpoint, |
|
| 403 | + | BadProof, |
|
| 404 | + | } |
|
| 405 | + | ||
| 406 | + | const SELECT_EVENT: &str = "SELECT seq, id, at_ms, hlc_wall, hlc_logical, prev, hash, auth_key, auth_method, auth_assurance, auth_proof, kind, actor, session, project, subject, payload, trace FROM events"; |
|
| 407 | + | ||
| 408 | + | async fn insert(conn: &mut sqlx::SqliteConnection, e: &Event) -> sqlx::Result<()> { |
|
| 409 | + | sqlx::query( |
|
| 410 | + | "INSERT INTO events (seq, id, at_ms, hlc_wall, hlc_logical, prev, hash, auth_key, auth_method, auth_assurance, auth_proof, kind, actor, session, project, subject, payload, trace) \ |
|
| 411 | + | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |
|
| 412 | + | ) |
|
| 413 | + | .bind(e.seq as i64) |
|
| 414 | + | .bind(&e.id) |
|
| 415 | + | .bind(e.at_ms as i64) |
|
| 416 | + | .bind(e.hlc.wall_ms as i64) |
|
| 417 | + | .bind(e.hlc.logical as i64) |
|
| 418 | + | .bind(e.prev.to_vec()) |
|
| 419 | + | .bind(e.hash.to_vec()) |
|
| 420 | + | .bind(e.auth.key.map(|k| k.to_vec())) |
|
| 421 | + | .bind(e.auth.method as i64) |
|
| 422 | + | .bind(e.auth.assurance as i64) |
|
| 423 | + | .bind(e.auth.proof.clone()) |
|
| 424 | + | .bind(&e.kind) |
|
| 425 | + | .bind(&e.actor) |
|
| 426 | + | .bind(&e.session) |
|
| 427 | + | .bind(&e.project) |
|
| 428 | + | .bind(&e.subject) |
|
| 429 | + | .bind(e.payload.clone()) |
|
| 430 | + | .bind(&e.trace) |
|
| 431 | + | .execute(conn) |
|
| 432 | + | .await?; |
|
| 433 | + | Ok(()) |
|
| 434 | + | } |
|
| 435 | + | ||
| 436 | + | fn row_to_event(r: &SqliteRow) -> Result<Event> { |
|
| 437 | + | Ok(Event { |
|
| 438 | + | seq: r.get::<i64, _>(0) as u64, |
|
| 439 | + | id: r.get(1), |
|
| 440 | + | at_ms: r.get::<i64, _>(2) as u64, |
|
| 441 | + | hlc: Hlc { wall_ms: r.get::<i64, _>(3) as u64, logical: r.get::<i64, _>(4) as u32 }, |
|
| 442 | + | prev: to32(r.get(5))?, |
|
| 443 | + | hash: to32(r.get(6))?, |
|
| 444 | + | auth: Auth { |
|
| 445 | + | key: r.get::<Option<Vec<u8>>, _>(7).map(to32).transpose()?, |
|
| 446 | + | method: Method::from_u8(r.get::<i64, _>(8) as u8).context("unknown auth method")?, |
|
| 447 | + | assurance: r.get::<i64, _>(9) as u8, |
|
| 448 | + | proof: r.get(10), |
|
| 449 | + | }, |
|
| 450 | + | kind: r.get(11), |
|
| 451 | + | actor: r.get(12), |
|
| 452 | + | session: r.get(13), |
|
| 453 | + | project: r.get(14), |
|
| 454 | + | subject: r.get(15), |
|
| 455 | + | payload: r.get(16), |
|
| 456 | + | trace: r.get(17), |
|
| 457 | + | }) |
|
| 458 | + | } |
|
| 459 | + | ||
| 460 | + | fn to32(v: Vec<u8>) -> Result<[u8; 32]> { |
|
| 461 | + | v.try_into().map_err(|_| anyhow::anyhow!("a stored hash is not 32 bytes")) |
|
| 462 | + | } |
|
| 463 | + | ||
| 464 | + | async fn replay(conn: &mut sqlx::SqliteConnection) -> Result<()> { |
|
| 465 | + | for t in projections::TABLES { |
|
| 466 | + | sqlx::query(&format!("DELETE FROM {t}")).execute(&mut *conn).await?; |
|
| 467 | + | } |
|
| 468 | + | let mut from = 1i64; |
|
| 469 | + | loop { |
|
| 470 | + | let rows = sqlx::query(&format!("{SELECT_EVENT} WHERE seq >= ? ORDER BY seq LIMIT 2000")).bind(from).fetch_all(&mut *conn).await?; |
|
| 471 | + | if rows.is_empty() { |
|
| 472 | + | break; |
|
| 473 | + | } |
|
| 474 | + | for r in &rows { |
|
| 475 | + | let e = row_to_event(r)?; |
|
| 476 | + | projections::apply(conn, &e).await?; |
|
| 477 | + | from = e.seq as i64 + 1; |
|
| 478 | + | } |
|
| 479 | + | } |
|
| 480 | + | Ok(()) |
|
| 481 | + | } |
|
| 482 | + | ||
| 483 | + | async fn dump(conn: &mut sqlx::SqliteConnection, table: &str) -> Result<Vec<String>> { |
|
| 484 | + | // Every column as text, rows in a fixed order: enough to compare. |
|
| 485 | + | let rows = sqlx::query(&format!("SELECT * FROM {table} ORDER BY 1, 2")).fetch_all(&mut *conn).await?; |
|
| 486 | + | Ok(rows |
|
| 487 | + | .iter() |
|
| 488 | + | .map(|r| { |
|
| 489 | + | (0..r.columns().len()) |
|
| 490 | + | .map(|i| match r.try_get::<Option<String>, _>(i) { |
|
| 491 | + | Ok(v) => format!("{v:?}"), |
|
| 492 | + | Err(_) => match r.try_get::<Option<i64>, _>(i) { |
|
| 493 | + | Ok(n) => format!("{n:?}"), |
|
| 494 | + | Err(_) => format!("{:?}", r.try_get::<Option<Vec<u8>>, _>(i).ok().flatten()), |
|
| 495 | + | }, |
|
| 496 | + | }) |
|
| 497 | + | .collect::<Vec<_>>() |
|
| 498 | + | .join("|") |
|
| 499 | + | }) |
|
| 500 | + | .collect()) |
|
| 501 | + | } |
|
| 502 | + | ||
| 503 | + | /// The identity of `key` among `identities`, by KeyId. |
|
| 504 | + | pub fn find_identity<'a>(identities: &'a [Identity], key: &KeyId) -> Option<&'a Identity> { |
|
| 505 | + | identities.iter().find(|i| &i.hash() == key) |
|
| 506 | + | } |
added crates/spectra-core/src/projections.rs +19 −0
| 1 | + | //! Projections (spec §9.3): tables kept from the ledger in the same |
|
| 2 | + | //! transaction as each append, and rebuilt from it by `spectra admin |
|
| 3 | + | //! rebuild`. Every table here must be a function of the events alone; the |
|
| 4 | + | //! test harness and `spectra admin check` hold them to it. |
|
| 5 | + | ||
| 6 | + | use crate::event::Event; |
|
| 7 | + | use sqlx::SqliteConnection; |
|
| 8 | + | ||
| 9 | + | /// The projection tables, in the order they are cleared and compared. |
|
| 10 | + | pub const TABLES: &[&str] = &["event_counts"]; |
|
| 11 | + | ||
| 12 | + | /// Apply one event to every projection. |
|
| 13 | + | pub async fn apply(conn: &mut SqliteConnection, e: &Event) -> sqlx::Result<()> { |
|
| 14 | + | sqlx::query("INSERT INTO event_counts (kind, n) VALUES (?, 1) ON CONFLICT(kind) DO UPDATE SET n = n + 1") |
|
| 15 | + | .bind(&e.kind) |
|
| 16 | + | .execute(&mut *conn) |
|
| 17 | + | .await?; |
|
| 18 | + | Ok(()) |
|
| 19 | + | } |
added crates/spectra-core/tests/ledger.rs +194 −0
| 1 | + | //! The ledger's promises (spec §9.5, goal G6): an untouched ledger verifies, |
|
| 2 | + | //! with checkpoints signed by the instance key; an edited, removed or |
|
| 3 | + | //! re-signed event is caught; projections always equal a rebuild. |
|
| 4 | + | ||
| 5 | + | use rand::{Rng, SeedableRng}; |
|
| 6 | + | use spectra_core::event::NewEvent; |
|
| 7 | + | use spectra_core::keys::KeyPair; |
|
| 8 | + | use spectra_core::ledger::{Ledger, NoKeys, Problem, CHECKPOINT_EVERY}; |
|
| 9 | + | use std::path::Path; |
|
| 10 | + | ||
| 11 | + | async fn open(dir: &Path) -> Ledger { |
|
| 12 | + | let instance = KeyPair::instance(dir, "test-instance").unwrap(); |
|
| 13 | + | let ledger = Ledger::open(&dir.join("spectra.db"), Some(instance.clone())).await.unwrap(); |
|
| 14 | + | if ledger.head().await.0 == 0 { |
|
| 15 | + | ledger |
|
| 16 | + | .append_one(NewEvent::system( |
|
| 17 | + | "identity.instance", |
|
| 18 | + | serde_json::json!({ "key": instance.key.to_hex(), "identity": hex::encode(instance.identity.encode()) }), |
|
| 19 | + | )) |
|
| 20 | + | .await |
|
| 21 | + | .unwrap(); |
|
| 22 | + | } |
|
| 23 | + | ledger |
|
| 24 | + | } |
|
| 25 | + | ||
| 26 | + | fn msg(i: usize) -> NewEvent { |
|
| 27 | + | NewEvent::system("test.message", serde_json::json!({ "n": i, "body": format!("message {i}") })).subject(format!("m{i}")) |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | async fn raw(dir: &Path, sql: &str) { |
|
| 31 | + | let pool = sqlx::SqlitePool::connect(&format!("sqlite://{}", dir.join("spectra.db").display())).await.unwrap(); |
|
| 32 | + | sqlx::query(sql).execute(&pool).await.unwrap(); |
|
| 33 | + | pool.close().await; |
|
| 34 | + | } |
|
| 35 | + | ||
| 36 | + | #[tokio::test] |
|
| 37 | + | async fn an_untouched_ledger_verifies_with_signed_checkpoints() { |
|
| 38 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 39 | + | let ledger = open(dir.path()).await; |
|
| 40 | + | for i in 0..(CHECKPOINT_EVERY as usize + 10) { |
|
| 41 | + | ledger.append_one(msg(i)).await.unwrap(); |
|
| 42 | + | } |
|
| 43 | + | let cp = ledger.checkpoint().await.unwrap().expect("events since the last checkpoint"); |
|
| 44 | + | assert_eq!(cp.kind, "ledger.checkpoint"); |
|
| 45 | + | let v = ledger.verify(&NoKeys).await.unwrap(); |
|
| 46 | + | assert!(v.clean(), "{:?}", v.findings); |
|
| 47 | + | assert_eq!(v.checkpoints, 2, "one at 256 events, one on demand"); |
|
| 48 | + | assert_eq!(v.uncovered, 0); |
|
| 49 | + | assert_eq!(v.head, ledger.head().await.1); |
|
| 50 | + | assert!(ledger.checkpoint().await.unwrap().is_none(), "nothing new to cover"); |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | #[tokio::test] |
|
| 54 | + | async fn an_edited_event_is_caught() { |
|
| 55 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 56 | + | let ledger = open(dir.path()).await; |
|
| 57 | + | for i in 0..20 { |
|
| 58 | + | ledger.append_one(msg(i)).await.unwrap(); |
|
| 59 | + | } |
|
| 60 | + | ledger.checkpoint().await.unwrap(); |
|
| 61 | + | drop(ledger); |
|
| 62 | + | raw(dir.path(), r#"UPDATE events SET payload = CAST('{"body":"message 5, edited","n":5}' AS BLOB) WHERE subject = 'm5'"#).await; |
|
| 63 | + | let v = open(dir.path()).await.verify(&NoKeys).await.unwrap(); |
|
| 64 | + | assert_eq!(v.findings.len(), 1, "{:?}", v.findings); |
|
| 65 | + | assert_eq!(v.findings[0].problem, Problem::HashMismatch); |
|
| 66 | + | } |
|
| 67 | + | ||
| 68 | + | #[tokio::test] |
|
| 69 | + | async fn an_edit_with_its_hash_recomputed_breaks_the_chain_or_the_checkpoint() { |
|
| 70 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 71 | + | let ledger = open(dir.path()).await; |
|
| 72 | + | for i in 0..20 { |
|
| 73 | + | ledger.append_one(msg(i)).await.unwrap(); |
|
| 74 | + | } |
|
| 75 | + | ledger.checkpoint().await.unwrap(); |
|
| 76 | + | // A careful forger recomputes the edited event's hash: the next event's |
|
| 77 | + | // prev no longer matches, and the checkpoint names the old head. |
|
| 78 | + | let mut e = ledger.events(1, 100).await.unwrap().into_iter().find(|e| e.subject.as_deref() == Some("m19")).unwrap(); |
|
| 79 | + | e.payload = Some(br#"{"body":"forged","n":19}"#.to_vec()); |
|
| 80 | + | let forged = hex::encode(e.computed_hash()); |
|
| 81 | + | drop(ledger); |
|
| 82 | + | raw( |
|
| 83 | + | dir.path(), |
|
| 84 | + | &format!(r#"UPDATE events SET payload = CAST('{{"body":"forged","n":19}}' AS BLOB), hash = X'{forged}' WHERE seq = {}"#, e.seq), |
|
| 85 | + | ) |
|
| 86 | + | .await; |
|
| 87 | + | let v = open(dir.path()).await.verify(&NoKeys).await.unwrap(); |
|
| 88 | + | assert!(v.findings.iter().any(|f| f.problem == Problem::ChainBreak || f.problem == Problem::BadCheckpoint), "{:?}", v.findings); |
|
| 89 | + | } |
|
| 90 | + | ||
| 91 | + | #[tokio::test] |
|
| 92 | + | async fn a_removed_event_is_caught() { |
|
| 93 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 94 | + | let ledger = open(dir.path()).await; |
|
| 95 | + | for i in 0..10 { |
|
| 96 | + | ledger.append_one(msg(i)).await.unwrap(); |
|
| 97 | + | } |
|
| 98 | + | drop(ledger); |
|
| 99 | + | raw(dir.path(), "DELETE FROM events WHERE subject = 'm4'").await; |
|
| 100 | + | let v = open(dir.path()).await.verify(&NoKeys).await.unwrap(); |
|
| 101 | + | assert!(v.findings.iter().any(|f| f.problem == Problem::Gap), "{:?}", v.findings); |
|
| 102 | + | assert!(v.findings.iter().any(|f| f.problem == Problem::ChainBreak), "{:?}", v.findings); |
|
| 103 | + | } |
|
| 104 | + | ||
| 105 | + | #[tokio::test] |
|
| 106 | + | async fn a_checkpoint_signed_by_another_key_is_caught() { |
|
| 107 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 108 | + | let ledger = open(dir.path()).await; |
|
| 109 | + | ledger.append_one(msg(1)).await.unwrap(); |
|
| 110 | + | let cp = ledger.checkpoint().await.unwrap().unwrap(); |
|
| 111 | + | drop(ledger); |
|
| 112 | + | let mallory = KeyPair::generate("mallory", ""); |
|
| 113 | + | let sig = mallory.sign(&spectra_core::ledger::checkpoint_message(cp.seq - 1, &cp.prev)); |
|
| 114 | + | let proof = hex::encode(oxy_core::cbor::encode(&sig.to_value())); |
|
| 115 | + | raw(dir.path(), &format!("UPDATE events SET auth_proof = X'{proof}' WHERE seq = {}", cp.seq)).await; |
|
| 116 | + | let v = open(dir.path()).await.verify(&NoKeys).await.unwrap(); |
|
| 117 | + | assert_eq!(v.findings.len(), 1, "{:?}", v.findings); |
|
| 118 | + | assert_eq!(v.findings[0].problem, Problem::BadCheckpoint); |
|
| 119 | + | } |
|
| 120 | + | ||
| 121 | + | #[tokio::test] |
|
| 122 | + | async fn projections_equal_a_rebuild_over_ten_thousand_random_events() { |
|
| 123 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 124 | + | let ledger = open(dir.path()).await; |
|
| 125 | + | let mut rng = rand::rngs::StdRng::seed_from_u64(7); |
|
| 126 | + | let kinds = ["message.posted", "agent.presence", "task.created", "claim.taken", "tool.called"]; |
|
| 127 | + | let started = std::time::Instant::now(); |
|
| 128 | + | let mut appended = 0; |
|
| 129 | + | while appended < 10_000 { |
|
| 130 | + | let n = rng.gen_range(1..50); |
|
| 131 | + | let batch = (0..n) |
|
| 132 | + | .map(|i| { |
|
| 133 | + | NewEvent::system(kinds[rng.gen_range(0..kinds.len())], serde_json::json!({ "i": appended + i, "r": rng.gen::<u32>() })) |
|
| 134 | + | }) |
|
| 135 | + | .collect(); |
|
| 136 | + | ledger.append(batch).await.unwrap(); |
|
| 137 | + | appended += n; |
|
| 138 | + | } |
|
| 139 | + | let per_batch_ms = started.elapsed().as_millis() as f64 / (10_000.0 / 25.0); |
|
| 140 | + | assert!(ledger.check().await.unwrap().is_empty(), "projections drifted from the ledger"); |
|
| 141 | + | ledger.rebuild().await.unwrap(); |
|
| 142 | + | assert!(ledger.check().await.unwrap().is_empty()); |
|
| 143 | + | let v = ledger.verify(&NoKeys).await.unwrap(); |
|
| 144 | + | assert!(v.clean(), "{:?}", &v.findings[..v.findings.len().min(3)]); |
|
| 145 | + | assert!(v.checkpoints >= 10_000 / CHECKPOINT_EVERY); |
|
| 146 | + | // Generous in a debug build; the release number is in docs/plan.md. |
|
| 147 | + | assert!(per_batch_ms < 200.0, "{per_batch_ms} ms a batch"); |
|
| 148 | + | } |
|
| 149 | + | ||
| 150 | + | #[tokio::test] |
|
| 151 | + | async fn a_ledger_from_a_newer_spectra_is_refused() { |
|
| 152 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 153 | + | drop(open(dir.path()).await); |
|
| 154 | + | raw(dir.path(), "UPDATE meta SET value = '99' WHERE key = 'ledger_format'").await; |
|
| 155 | + | let err = Ledger::open(&dir.path().join("spectra.db"), None).await.err().expect("refused"); |
|
| 156 | + | assert!(err.to_string().contains("newer Spectra"), "{err}"); |
|
| 157 | + | } |
|
| 158 | + | ||
| 159 | + | #[tokio::test] |
|
| 160 | + | async fn the_bus_delivers_what_was_appended_in_order() { |
|
| 161 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 162 | + | let ledger = open(dir.path()).await; |
|
| 163 | + | let mut rx = ledger.subscribe(); |
|
| 164 | + | ledger.append(vec![msg(1), msg(2)]).await.unwrap(); |
|
| 165 | + | assert_eq!(rx.recv().await.unwrap().subject.as_deref(), Some("m1")); |
|
| 166 | + | assert_eq!(rx.recv().await.unwrap().subject.as_deref(), Some("m2")); |
|
| 167 | + | } |
|
| 168 | + | ||
| 169 | + | #[tokio::test] |
|
| 170 | + | async fn reopening_continues_the_chain() { |
|
| 171 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 172 | + | let ledger = open(dir.path()).await; |
|
| 173 | + | ledger.append_one(msg(1)).await.unwrap(); |
|
| 174 | + | let (seq, hash) = ledger.head().await; |
|
| 175 | + | drop(ledger); |
|
| 176 | + | let ledger = open(dir.path()).await; |
|
| 177 | + | let e = ledger.append_one(msg(2)).await.unwrap(); |
|
| 178 | + | assert_eq!((e.seq, e.prev), (seq + 1, hash)); |
|
| 179 | + | assert!(ledger.verify(&NoKeys).await.unwrap().clean()); |
|
| 180 | + | } |
|
| 181 | + | ||
| 182 | + | #[tokio::test] |
|
| 183 | + | async fn a_single_append_is_fast() { |
|
| 184 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 185 | + | let ledger = open(dir.path()).await; |
|
| 186 | + | let started = std::time::Instant::now(); |
|
| 187 | + | for i in 0..1_000 { |
|
| 188 | + | ledger.append_one(msg(i)).await.unwrap(); |
|
| 189 | + | } |
|
| 190 | + | let each_us = started.elapsed().as_micros() / 1_000; |
|
| 191 | + | eprintln!("append: {each_us} µs an event over 1,000 single appends"); |
|
| 192 | + | // The target is 10 ms with fsync (spec §24); debug builds get headroom. |
|
| 193 | + | assert!(each_us < 50_000, "{each_us} µs"); |
|
| 194 | + | } |
added src/cli/admin.rs +55 −0
| 1 | + | //! `spectra admin`: the ledger's own checks (spec §9.5, §19). |
|
| 2 | + | ||
| 3 | + | use super::{ui, Ctx}; |
|
| 4 | + | use anyhow::Result; |
|
| 5 | + | use spectra_core::ledger::{Ledger, NoKeys}; |
|
| 6 | + | ||
| 7 | + | /// `spectra admin verify`: recompute the chain, check every checkpoint and |
|
| 8 | + | /// proof; exit 1 when anything is wrong. |
|
| 9 | + | pub async fn verify(ctx: &Ctx, json: bool) -> Result<i32> { |
|
| 10 | + | let ledger = Ledger::open(&ctx.db()?, None).await?; |
|
| 11 | + | let v = ledger.verify(&NoKeys).await?; |
|
| 12 | + | if json { |
|
| 13 | + | println!("{}", serde_json::to_string(&v)?); |
|
| 14 | + | } else if v.clean() { |
|
| 15 | + | println!("{} ledger verified", ui::done(ui::glyph("✔", "ok"))); |
|
| 16 | + | println!("{}", ui::kv("events", v.events)); |
|
| 17 | + | println!( |
|
| 18 | + | "{}", |
|
| 19 | + | ui::kv( |
|
| 20 | + | "checkpoints", |
|
| 21 | + | format!("{} (last at {})", v.checkpoints, v.last_checkpoint.map(|s| s.to_string()).unwrap_or("none".into())) |
|
| 22 | + | ) |
|
| 23 | + | ); |
|
| 24 | + | println!("{}", ui::kv("uncovered", format!("{} event(s) after the last checkpoint", v.uncovered))); |
|
| 25 | + | println!("{}", ui::kv("head", ui::hash(hex::encode(v.head)))); |
|
| 26 | + | } else { |
|
| 27 | + | println!("{} ledger does not verify: {} finding(s)", ui::err(ui::glyph("✘", "x")), v.findings.len()); |
|
| 28 | + | for f in &v.findings { |
|
| 29 | + | println!(" {} {:?}: {}", ui::id(format!("#{}", f.seq)), f.problem, f.detail); |
|
| 30 | + | } |
|
| 31 | + | } |
|
| 32 | + | Ok(if v.clean() { 0 } else { 1 }) |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | /// `spectra admin check`: do the projections equal a rebuild? |
|
| 36 | + | pub async fn check(ctx: &Ctx) -> Result<i32> { |
|
| 37 | + | let ledger = Ledger::open(&ctx.db()?, None).await?; |
|
| 38 | + | let differ = ledger.check().await?; |
|
| 39 | + | if differ.is_empty() { |
|
| 40 | + | println!("{} projections match the ledger", ui::done(ui::glyph("✔", "ok"))); |
|
| 41 | + | Ok(0) |
|
| 42 | + | } else { |
|
| 43 | + | println!("{} projections differ from the ledger: {}", ui::err(ui::glyph("✘", "x")), differ.join(", ")); |
|
| 44 | + | println!(" run {} to rebuild them", ui::id("spectra admin rebuild")); |
|
| 45 | + | Ok(1) |
|
| 46 | + | } |
|
| 47 | + | } |
|
| 48 | + | ||
| 49 | + | /// `spectra admin rebuild`: replay the ledger into every projection. |
|
| 50 | + | pub async fn rebuild(ctx: &Ctx) -> Result<i32> { |
|
| 51 | + | let ledger = Ledger::open(&ctx.db()?, None).await?; |
|
| 52 | + | ledger.rebuild().await?; |
|
| 53 | + | println!("{} projections rebuilt from {} event(s)", ui::done(ui::glyph("✔", "ok")), ledger.head().await.0); |
|
| 54 | + | Ok(0) |
|
| 55 | + | } |
added tests/cli_admin.rs +67 −0
| 1 | + | //! `spectra admin verify|check|rebuild` against a real ledger, run as the |
|
| 2 | + | //! built binary in a hermetic environment: a temporary data directory and |
|
| 3 | + | //! `OXY_HOME`, never the user's own. |
|
| 4 | + | ||
| 5 | + | use spectra_core::event::NewEvent; |
|
| 6 | + | use spectra_core::keys::KeyPair; |
|
| 7 | + | use spectra_core::ledger::Ledger; |
|
| 8 | + | use std::path::Path; |
|
| 9 | + | use std::process::Command; |
|
| 10 | + | ||
| 11 | + | /// The binary with every directory it might read pointed at `dir`. |
|
| 12 | + | fn hermetic(dir: &Path) -> Command { |
|
| 13 | + | let mut c = Command::new(env!("CARGO_BIN_EXE_spectra")); |
|
| 14 | + | c.env("SPECTRA_DATA", dir.join("data")) |
|
| 15 | + | .env("OXY_HOME", dir.join("oxy")) |
|
| 16 | + | .env("HOME", dir) |
|
| 17 | + | .env("XDG_DATA_HOME", dir.join("xdg")) |
|
| 18 | + | .env("NO_COLOR", "1"); |
|
| 19 | + | c |
|
| 20 | + | } |
|
| 21 | + | ||
| 22 | + | async fn ledger_with_events(data: &Path, n: usize) { |
|
| 23 | + | std::fs::create_dir_all(data).unwrap(); |
|
| 24 | + | let instance = KeyPair::instance(data, "test").unwrap(); |
|
| 25 | + | let ledger = Ledger::open(&data.join("spectra.db"), Some(instance.clone())).await.unwrap(); |
|
| 26 | + | ledger |
|
| 27 | + | .append_one(NewEvent::system("identity.instance", serde_json::json!({ "identity": hex::encode(instance.identity.encode()) }))) |
|
| 28 | + | .await |
|
| 29 | + | .unwrap(); |
|
| 30 | + | for i in 0..n { |
|
| 31 | + | ledger.append_one(NewEvent::system("test.message", serde_json::json!({ "n": i }))).await.unwrap(); |
|
| 32 | + | } |
|
| 33 | + | ledger.checkpoint().await.unwrap(); |
|
| 34 | + | } |
|
| 35 | + | ||
| 36 | + | #[tokio::test] |
|
| 37 | + | async fn verify_check_and_rebuild_on_a_clean_and_a_tampered_ledger() { |
|
| 38 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 39 | + | let data = dir.path().join("data"); |
|
| 40 | + | ledger_with_events(&data, 40).await; |
|
| 41 | + | ||
| 42 | + | let out = hermetic(dir.path()).args(["admin", "verify", "--json"]).output().unwrap(); |
|
| 43 | + | assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); |
|
| 44 | + | let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("one JSON document on stdout"); |
|
| 45 | + | assert_eq!(v["events"], 42); |
|
| 46 | + | assert_eq!(v["findings"].as_array().unwrap().len(), 0); |
|
| 47 | + | assert_eq!(v["uncovered"], 0); |
|
| 48 | + | ||
| 49 | + | assert!(hermetic(dir.path()).args(["admin", "check"]).status().unwrap().success()); |
|
| 50 | + | assert!(hermetic(dir.path()).args(["admin", "rebuild"]).status().unwrap().success()); |
|
| 51 | + | ||
| 52 | + | let pool = sqlx::SqlitePool::connect(&format!("sqlite://{}", data.join("spectra.db").display())).await.unwrap(); |
|
| 53 | + | sqlx::query("UPDATE events SET actor = 'agent:mallory' WHERE seq = 10").execute(&pool).await.unwrap(); |
|
| 54 | + | pool.close().await; |
|
| 55 | + | let out = hermetic(dir.path()).args(["admin", "verify"]).output().unwrap(); |
|
| 56 | + | assert_eq!(out.status.code(), Some(1)); |
|
| 57 | + | let text = String::from_utf8_lossy(&out.stdout); |
|
| 58 | + | assert!(text.contains("does not verify") && text.contains("#10") && text.contains("HashMismatch"), "{text}"); |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | #[test] |
|
| 62 | + | fn a_missing_data_directory_says_what_to_do() { |
|
| 63 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 64 | + | let out = hermetic(dir.path()).args(["admin", "verify"]).output().unwrap(); |
|
| 65 | + | assert_eq!(out.status.code(), Some(1)); |
|
| 66 | + | assert!(String::from_utf8_lossy(&out.stderr).contains("spectra init")); |
|
| 67 | + | } |
modified .flow/pipeline.kdl +5 −4
| … | 32 unchanged lines |
||
| 33 | 33 | network "isolated" |
|
| 34 | 34 | } |
|
| 35 | 35 | ||
| 36 | - | // Spectra's own packages only: `--all` would reach the pinned Oxygen in |
|
| 37 | - | // deps/, which has its own formatting (as in oxygenhub). |
|
| 36 | + | // Spectra's own packages only, here and in clippy and test: `--all` and |
|
| 37 | + | // `--workspace` would reach the pinned Oxygen in deps/, which has its own |
|
| 38 | + | // formatting, lints and tests (as in oxygenhub). |
|
| 38 | 39 | job "fmt" { |
|
| 39 | 40 | sandbox "rust-small" |
|
| 40 | 41 | exec "cargo" "fmt" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "-p" "radar" "--check" |
|
| … | 16 unchanged lines |
||
| 57 | 58 | env { |
|
| 58 | 59 | SPECTRA_BUILD "ci" |
|
| 59 | 60 | } |
|
| 60 | - | exec "cargo" "clippy" "--workspace" "--all-targets" "--locked" "--" "-D" "warnings" |
|
| 61 | + | exec "cargo" "clippy" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "-p" "radar" "--all-targets" "--locked" "--" "-D" "warnings" |
|
| 61 | 62 | } |
|
| 62 | 63 | ||
| 63 | 64 | // The tests, run under cargo-nextest and cargo-llvm-cov: a test report |
|
| … | 12 unchanged lines |
||
| 76 | 77 | env { |
|
| 77 | 78 | SPECTRA_BUILD "ci" |
|
| 78 | 79 | } |
|
| 79 | - | exec "cargo" "llvm-cov" "nextest" "--workspace" "--locked" "--profile" "ci" "--lcov" "--output-path" "/src/target/lcov.info" |
|
| 80 | + | exec "cargo" "llvm-cov" "nextest" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "-p" "radar" "--locked" "--profile" "ci" "--lcov" "--output-path" "/src/target/lcov.info" |
|
| 80 | 81 | output { |
|
| 81 | 82 | artifact "junit" { |
|
| 82 | 83 | path "/src/target/nextest/ci/junit.xml" |
|
| … | 27 unchanged lines |
modified CHANGELOG.md +1 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **Added: the ledger (#3).** Every event is deterministic CBOR (through `oxy-core`) hashed with a domain-separated BLAKE3 and chained to the one before; the envelope, with its hybrid logical clock, is frozen. The instance key signs a checkpoint every 256 events, every ten minutes while writing, and on demand. `spectra admin verify [--json]` recomputes the chain and checks every checkpoint, catching an edited, removed or re-signed event; `spectra admin check` and `rebuild` keep projections equal to the ledger. A single append takes 0.15 ms. |
|
| 5 | 6 | - **Added: CI on OxygenFlow (#2).** `.flow/pipeline.kdl` in the shape of Oxygen's: fmt, clippy, test (nextest under llvm-cov, with JUnit and coverage for the hub) and build, in the vendored image with no network. |
|
| 6 | 7 | - **Added: the project skeleton (#1).** A Cargo workspace in Oxygen's shape (`spectra-core`, `spectra-mcp`, `spectra-api`, `radar` and the `spectra` binary), Oxygen pinned under `deps/oxygen` for identities and signatures, `make check`, the changelog check, `spectra version [--json]` with the build identity, and `scripts/deps-check.sh` to keep Spectra buildable by the vendored OxygenFlow image. |
modified Cargo.lock +1016 −35
| … | 17 unchanged lines |
||
| 18 | 18 | ] |
|
| 19 | 19 | ||
| 20 | 20 | [[package]] |
|
| 21 | + | name = "allocator-api2" |
|
| 22 | + | version = "0.2.21" |
|
| 23 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 24 | + | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" |
|
| 25 | + | ||
| 26 | + | [[package]] |
|
| 21 | 27 | name = "anstream" |
|
| 22 | 28 | version = "1.0.0" |
|
| 23 | 29 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 54 unchanged lines |
||
| 78 | 84 | version = "0.7.8" |
|
| 79 | 85 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 80 | 86 | checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" |
|
| 87 | + | ||
| 88 | + | [[package]] |
|
| 89 | + | name = "atoi" |
|
| 90 | + | version = "2.0.0" |
|
| 91 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 92 | + | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" |
|
| 93 | + | dependencies = [ |
|
| 94 | + | "num-traits", |
|
| 95 | + | ] |
|
| 81 | 96 | ||
| 82 | 97 | [[package]] |
|
| 83 | 98 | name = "autocfg" |
|
| … | 33 unchanged lines |
||
| 117 | 132 | version = "2.13.2" |
|
| 118 | 133 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 119 | 134 | checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" |
|
| 135 | + | dependencies = [ |
|
| 136 | + | "serde_core", |
|
| 137 | + | ] |
|
| 120 | 138 | ||
| 121 | 139 | [[package]] |
|
| 122 | 140 | name = "blake3" |
|
| … | 5 unchanged lines |
||
| 128 | 146 | "cc", |
|
| 129 | 147 | "cfg-if", |
|
| 130 | 148 | "constant_time_eq", |
|
| 131 | - | "cpufeatures", |
|
| 149 | + | "cpufeatures 0.3.1", |
|
| 132 | 150 | "rayon-core", |
|
| 151 | + | ] |
|
| 152 | + | ||
| 153 | + | [[package]] |
|
| 154 | + | name = "block-buffer" |
|
| 155 | + | version = "0.10.4" |
|
| 156 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 157 | + | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" |
|
| 158 | + | dependencies = [ |
|
| 159 | + | "generic-array", |
|
| 133 | 160 | ] |
|
| 134 | 161 | ||
| 135 | 162 | [[package]] |
|
| … | 22 unchanged lines |
||
| 158 | 185 | checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" |
|
| 159 | 186 | ||
| 160 | 187 | [[package]] |
|
| 188 | + | name = "byteorder" |
|
| 189 | + | version = "1.5.0" |
|
| 190 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 191 | + | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" |
|
| 192 | + | ||
| 193 | + | [[package]] |
|
| 161 | 194 | name = "bytes" |
|
| 162 | 195 | version = "1.12.1" |
|
| 163 | 196 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 81 unchanged lines |
||
| 245 | 278 | "unicode-width", |
|
| 246 | 279 | "windows-sys 0.59.0", |
|
| 247 | 280 | ] |
|
| 281 | + | ||
| 282 | + | [[package]] |
|
| 283 | + | name = "const-oid" |
|
| 284 | + | version = "0.9.6" |
|
| 285 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 286 | + | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" |
|
| 248 | 287 | ||
| 249 | 288 | [[package]] |
|
| 250 | 289 | name = "const-oid" |
|
| … | 9 unchanged lines |
||
| 260 | 299 | ||
| 261 | 300 | [[package]] |
|
| 262 | 301 | name = "cpufeatures" |
|
| 302 | + | version = "0.2.17" |
|
| 303 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 304 | + | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" |
|
| 305 | + | dependencies = [ |
|
| 306 | + | "libc", |
|
| 307 | + | ] |
|
| 308 | + | ||
| 309 | + | [[package]] |
|
| 310 | + | name = "cpufeatures" |
|
| 263 | 311 | version = "0.3.1" |
|
| 264 | 312 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 265 | 313 | checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" |
|
| … | 2 unchanged lines |
||
| 268 | 316 | ] |
|
| 269 | 317 | ||
| 270 | 318 | [[package]] |
|
| 319 | + | name = "crc" |
|
| 320 | + | version = "3.4.0" |
|
| 321 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 322 | + | checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" |
|
| 323 | + | dependencies = [ |
|
| 324 | + | "crc-catalog", |
|
| 325 | + | ] |
|
| 326 | + | ||
| 327 | + | [[package]] |
|
| 328 | + | name = "crc-catalog" |
|
| 329 | + | version = "2.5.0" |
|
| 330 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 331 | + | checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" |
|
| 332 | + | ||
| 333 | + | [[package]] |
|
| 271 | 334 | name = "crc32fast" |
|
| 272 | 335 | version = "1.5.2" |
|
| 273 | 336 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 22 unchanged lines |
||
| 296 | 359 | ] |
|
| 297 | 360 | ||
| 298 | 361 | [[package]] |
|
| 362 | + | name = "crossbeam-queue" |
|
| 363 | + | version = "0.3.14" |
|
| 364 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 365 | + | checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" |
|
| 366 | + | dependencies = [ |
|
| 367 | + | "crossbeam-utils", |
|
| 368 | + | ] |
|
| 369 | + | ||
| 370 | + | [[package]] |
|
| 299 | 371 | name = "crossbeam-utils" |
|
| 300 | 372 | version = "0.8.23" |
|
| 301 | 373 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 302 | 374 | checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" |
|
| 375 | + | ||
| 376 | + | [[package]] |
|
| 377 | + | name = "crypto-common" |
|
| 378 | + | version = "0.1.7" |
|
| 379 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 380 | + | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" |
|
| 381 | + | dependencies = [ |
|
| 382 | + | "generic-array", |
|
| 383 | + | "typenum", |
|
| 384 | + | ] |
|
| 303 | 385 | ||
| 304 | 386 | [[package]] |
|
| 305 | 387 | name = "crypto-common" |
|
| … | 22 unchanged lines |
||
| 328 | 410 | checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" |
|
| 329 | 411 | dependencies = [ |
|
| 330 | 412 | "cfg-if", |
|
| 331 | - | "cpufeatures", |
|
| 413 | + | "cpufeatures 0.3.1", |
|
| 332 | 414 | "curve25519-dalek-derive", |
|
| 333 | - | "digest", |
|
| 415 | + | "digest 0.11.3", |
|
| 334 | 416 | "fiat-crypto", |
|
| 335 | 417 | "rustc_version", |
|
| 336 | 418 | "subtle", |
|
| … | 13 unchanged lines |
||
| 350 | 432 | ||
| 351 | 433 | [[package]] |
|
| 352 | 434 | name = "der" |
|
| 435 | + | version = "0.7.10" |
|
| 436 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 437 | + | checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" |
|
| 438 | + | dependencies = [ |
|
| 439 | + | "const-oid 0.9.6", |
|
| 440 | + | "pem-rfc7468", |
|
| 441 | + | "zeroize", |
|
| 442 | + | ] |
|
| 443 | + | ||
| 444 | + | [[package]] |
|
| 445 | + | name = "der" |
|
| 353 | 446 | version = "0.8.2" |
|
| 354 | 447 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 355 | 448 | checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" |
|
| 356 | 449 | dependencies = [ |
|
| 357 | - | "const-oid", |
|
| 450 | + | "const-oid 0.10.2", |
|
| 358 | 451 | "zeroize", |
|
| 452 | + | ] |
|
| 453 | + | ||
| 454 | + | [[package]] |
|
| 455 | + | name = "digest" |
|
| 456 | + | version = "0.10.7" |
|
| 457 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 458 | + | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" |
|
| 459 | + | dependencies = [ |
|
| 460 | + | "block-buffer 0.10.4", |
|
| 461 | + | "const-oid 0.9.6", |
|
| 462 | + | "crypto-common 0.1.7", |
|
| 463 | + | "subtle", |
|
| 359 | 464 | ] |
|
| 360 | 465 | ||
| 361 | 466 | [[package]] |
|
| … | 2 unchanged lines |
||
| 364 | 469 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 365 | 470 | checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" |
|
| 366 | 471 | dependencies = [ |
|
| 367 | - | "block-buffer", |
|
| 368 | - | "const-oid", |
|
| 369 | - | "crypto-common", |
|
| 472 | + | "block-buffer 0.12.1", |
|
| 473 | + | "const-oid 0.10.2", |
|
| 474 | + | "crypto-common 0.2.2", |
|
| 370 | 475 | ] |
|
| 371 | 476 | ||
| 372 | 477 | [[package]] |
|
| … | 8 unchanged lines |
||
| 381 | 486 | ] |
|
| 382 | 487 | ||
| 383 | 488 | [[package]] |
|
| 489 | + | name = "dotenvy" |
|
| 490 | + | version = "0.15.7" |
|
| 491 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 492 | + | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" |
|
| 493 | + | ||
| 494 | + | [[package]] |
|
| 384 | 495 | name = "ed25519" |
|
| 385 | 496 | version = "3.0.0" |
|
| 386 | 497 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 387 | 498 | checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" |
|
| 388 | 499 | dependencies = [ |
|
| 389 | - | "signature", |
|
| 500 | + | "signature 3.0.0", |
|
| 390 | 501 | ] |
|
| 391 | 502 | ||
| 392 | 503 | [[package]] |
|
| … | 4 unchanged lines |
||
| 397 | 508 | dependencies = [ |
|
| 398 | 509 | "curve25519-dalek", |
|
| 399 | 510 | "ed25519", |
|
| 400 | - | "sha2", |
|
| 511 | + | "sha2 0.11.0", |
|
| 401 | 512 | "subtle", |
|
| 402 | 513 | "zeroize", |
|
| 403 | 514 | ] |
|
| … | 3 unchanged lines |
||
| 407 | 518 | version = "1.18.0" |
|
| 408 | 519 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 409 | 520 | checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" |
|
| 521 | + | dependencies = [ |
|
| 522 | + | "serde", |
|
| 523 | + | ] |
|
| 410 | 524 | ||
| 411 | 525 | [[package]] |
|
| 412 | 526 | name = "encode_unicode" |
|
| 413 | 527 | version = "1.0.0" |
|
| 414 | 528 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 415 | 529 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" |
|
| 530 | + | ||
| 531 | + | [[package]] |
|
| 532 | + | name = "equivalent" |
|
| 533 | + | version = "1.0.2" |
|
| 534 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 535 | + | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" |
|
| 416 | 536 | ||
| 417 | 537 | [[package]] |
|
| 418 | 538 | name = "errno" |
|
| … | 2 unchanged lines |
||
| 421 | 541 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" |
|
| 422 | 542 | dependencies = [ |
|
| 423 | 543 | "libc", |
|
| 424 | - | "windows-sys 0.52.0", |
|
| 544 | + | "windows-sys 0.61.2", |
|
| 425 | 545 | ] |
|
| 426 | 546 | ||
| 427 | 547 | [[package]] |
|
| 548 | + | name = "etcetera" |
|
| 549 | + | version = "0.8.0" |
|
| 550 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 551 | + | checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" |
|
| 552 | + | dependencies = [ |
|
| 553 | + | "cfg-if", |
|
| 554 | + | "home", |
|
| 555 | + | "windows-sys 0.48.0", |
|
| 556 | + | ] |
|
| 557 | + | ||
| 558 | + | [[package]] |
|
| 559 | + | name = "event-listener" |
|
| 560 | + | version = "5.4.2" |
|
| 561 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 562 | + | checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" |
|
| 563 | + | dependencies = [ |
|
| 564 | + | "parking", |
|
| 565 | + | "pin-project-lite", |
|
| 566 | + | ] |
|
| 567 | + | ||
| 568 | + | [[package]] |
|
| 428 | 569 | name = "fastrand" |
|
| 429 | 570 | version = "2.5.0" |
|
| 430 | 571 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 23 unchanged lines |
||
| 454 | 595 | ] |
|
| 455 | 596 | ||
| 456 | 597 | [[package]] |
|
| 598 | + | name = "flume" |
|
| 599 | + | version = "0.11.1" |
|
| 600 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 601 | + | checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" |
|
| 602 | + | dependencies = [ |
|
| 603 | + | "futures-core", |
|
| 604 | + | "futures-sink", |
|
| 605 | + | "spin", |
|
| 606 | + | ] |
|
| 607 | + | ||
| 608 | + | [[package]] |
|
| 457 | 609 | name = "fnv" |
|
| 458 | 610 | version = "1.0.7" |
|
| 459 | 611 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 460 | 612 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" |
|
| 613 | + | ||
| 614 | + | [[package]] |
|
| 615 | + | name = "foldhash" |
|
| 616 | + | version = "0.1.5" |
|
| 617 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 618 | + | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" |
|
| 461 | 619 | ||
| 462 | 620 | [[package]] |
|
| 463 | 621 | name = "form_urlencoded" |
|
| … | 2 unchanged lines |
||
| 466 | 624 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" |
|
| 467 | 625 | dependencies = [ |
|
| 468 | 626 | "percent-encoding", |
|
| 627 | + | ] |
|
| 628 | + | ||
| 629 | + | [[package]] |
|
| 630 | + | name = "futures-channel" |
|
| 631 | + | version = "0.3.34" |
|
| 632 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 633 | + | checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" |
|
| 634 | + | dependencies = [ |
|
| 635 | + | "futures-core", |
|
| 636 | + | "futures-sink", |
|
| 469 | 637 | ] |
|
| 470 | 638 | ||
| 471 | 639 | [[package]] |
|
| … | 3 unchanged lines |
||
| 475 | 643 | checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" |
|
| 476 | 644 | ||
| 477 | 645 | [[package]] |
|
| 646 | + | name = "futures-executor" |
|
| 647 | + | version = "0.3.34" |
|
| 648 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 649 | + | checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" |
|
| 650 | + | dependencies = [ |
|
| 651 | + | "futures-core", |
|
| 652 | + | "futures-task", |
|
| 653 | + | "futures-util", |
|
| 654 | + | ] |
|
| 655 | + | ||
| 656 | + | [[package]] |
|
| 657 | + | name = "futures-intrusive" |
|
| 658 | + | version = "0.5.0" |
|
| 659 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 660 | + | checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" |
|
| 661 | + | dependencies = [ |
|
| 662 | + | "futures-core", |
|
| 663 | + | "lock_api", |
|
| 664 | + | "parking_lot", |
|
| 665 | + | ] |
|
| 666 | + | ||
| 667 | + | [[package]] |
|
| 668 | + | name = "futures-io" |
|
| 669 | + | version = "0.3.34" |
|
| 670 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 671 | + | checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" |
|
| 672 | + | ||
| 673 | + | [[package]] |
|
| 674 | + | name = "futures-sink" |
|
| 675 | + | version = "0.3.34" |
|
| 676 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 677 | + | checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" |
|
| 678 | + | ||
| 679 | + | [[package]] |
|
| 478 | 680 | name = "futures-task" |
|
| 479 | 681 | version = "0.3.34" |
|
| 480 | 682 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 6 unchanged lines |
||
| 487 | 689 | checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" |
|
| 488 | 690 | dependencies = [ |
|
| 489 | 691 | "futures-core", |
|
| 692 | + | "futures-io", |
|
| 693 | + | "futures-sink", |
|
| 490 | 694 | "futures-task", |
|
| 695 | + | "memchr", |
|
| 491 | 696 | "pin-project-lite", |
|
| 492 | 697 | "slab", |
|
| 698 | + | ] |
|
| 699 | + | ||
| 700 | + | [[package]] |
|
| 701 | + | name = "generic-array" |
|
| 702 | + | version = "0.14.7" |
|
| 703 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 704 | + | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" |
|
| 705 | + | dependencies = [ |
|
| 706 | + | "typenum", |
|
| 707 | + | "version_check", |
|
| 493 | 708 | ] |
|
| 494 | 709 | ||
| 495 | 710 | [[package]] |
|
| … | 42 unchanged lines |
||
| 538 | 753 | "log", |
|
| 539 | 754 | "regex-automata", |
|
| 540 | 755 | "regex-syntax", |
|
| 756 | + | ] |
|
| 757 | + | ||
| 758 | + | [[package]] |
|
| 759 | + | name = "hashbrown" |
|
| 760 | + | version = "0.15.5" |
|
| 761 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 762 | + | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" |
|
| 763 | + | dependencies = [ |
|
| 764 | + | "allocator-api2", |
|
| 765 | + | "equivalent", |
|
| 766 | + | "foldhash", |
|
| 767 | + | ] |
|
| 768 | + | ||
| 769 | + | [[package]] |
|
| 770 | + | name = "hashbrown" |
|
| 771 | + | version = "0.17.1" |
|
| 772 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 773 | + | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" |
|
| 774 | + | ||
| 775 | + | [[package]] |
|
| 776 | + | name = "hashlink" |
|
| 777 | + | version = "0.10.0" |
|
| 778 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 779 | + | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" |
|
| 780 | + | dependencies = [ |
|
| 781 | + | "hashbrown 0.15.5", |
|
| 541 | 782 | ] |
|
| 542 | 783 | ||
| 543 | 784 | [[package]] |
|
| … | 7 unchanged lines |
||
| 551 | 792 | version = "0.4.3" |
|
| 552 | 793 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 553 | 794 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" |
|
| 795 | + | ||
| 796 | + | [[package]] |
|
| 797 | + | name = "hkdf" |
|
| 798 | + | version = "0.12.4" |
|
| 799 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 800 | + | checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" |
|
| 801 | + | dependencies = [ |
|
| 802 | + | "hmac", |
|
| 803 | + | ] |
|
| 804 | + | ||
| 805 | + | [[package]] |
|
| 806 | + | name = "hmac" |
|
| 807 | + | version = "0.12.1" |
|
| 808 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 809 | + | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" |
|
| 810 | + | dependencies = [ |
|
| 811 | + | "digest 0.10.7", |
|
| 812 | + | ] |
|
| 813 | + | ||
| 814 | + | [[package]] |
|
| 815 | + | name = "home" |
|
| 816 | + | version = "0.5.12" |
|
| 817 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 818 | + | checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" |
|
| 819 | + | dependencies = [ |
|
| 820 | + | "windows-sys 0.61.2", |
|
| 821 | + | ] |
|
| 554 | 822 | ||
| 555 | 823 | [[package]] |
|
| 556 | 824 | name = "hybrid-array" |
|
| … | 123 unchanged lines |
||
| 680 | 948 | "same-file", |
|
| 681 | 949 | "walkdir", |
|
| 682 | 950 | "winapi-util", |
|
| 951 | + | ] |
|
| 952 | + | ||
| 953 | + | [[package]] |
|
| 954 | + | name = "indexmap" |
|
| 955 | + | version = "2.14.2" |
|
| 956 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 957 | + | checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" |
|
| 958 | + | dependencies = [ |
|
| 959 | + | "equivalent", |
|
| 960 | + | "hashbrown 0.17.1", |
|
| 683 | 961 | ] |
|
| 684 | 962 | ||
| 685 | 963 | [[package]] |
|
| … | 49 unchanged lines |
||
| 735 | 1013 | checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" |
|
| 736 | 1014 | dependencies = [ |
|
| 737 | 1015 | "cfg-if", |
|
| 738 | - | "cpufeatures", |
|
| 1016 | + | "cpufeatures 0.3.1", |
|
| 739 | 1017 | ] |
|
| 740 | 1018 | ||
| 741 | 1019 | [[package]] |
|
| 1020 | + | name = "lazy_static" |
|
| 1021 | + | version = "1.5.1" |
|
| 1022 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1023 | + | checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" |
|
| 1024 | + | dependencies = [ |
|
| 1025 | + | "spin", |
|
| 1026 | + | ] |
|
| 1027 | + | ||
| 1028 | + | [[package]] |
|
| 742 | 1029 | name = "libc" |
|
| 743 | 1030 | version = "0.2.189" |
|
| 744 | 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 745 | 1032 | checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" |
|
| 1033 | + | ||
| 1034 | + | [[package]] |
|
| 1035 | + | name = "libm" |
|
| 1036 | + | version = "0.2.16" |
|
| 1037 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1038 | + | checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" |
|
| 746 | 1039 | ||
| 747 | 1040 | [[package]] |
|
| 748 | 1041 | name = "libmimalloc-sys" |
|
| … | 5 unchanged lines |
||
| 754 | 1047 | ] |
|
| 755 | 1048 | ||
| 756 | 1049 | [[package]] |
|
| 1050 | + | name = "libredox" |
|
| 1051 | + | version = "0.1.25" |
|
| 1052 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1053 | + | checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" |
|
| 1054 | + | dependencies = [ |
|
| 1055 | + | "bitflags", |
|
| 1056 | + | "libc", |
|
| 1057 | + | "plain", |
|
| 1058 | + | "redox_syscall 0.9.4", |
|
| 1059 | + | ] |
|
| 1060 | + | ||
| 1061 | + | [[package]] |
|
| 1062 | + | name = "libsqlite3-sys" |
|
| 1063 | + | version = "0.30.1" |
|
| 1064 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1065 | + | checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" |
|
| 1066 | + | dependencies = [ |
|
| 1067 | + | "cc", |
|
| 1068 | + | "pkg-config", |
|
| 1069 | + | "vcpkg", |
|
| 1070 | + | ] |
|
| 1071 | + | ||
| 1072 | + | [[package]] |
|
| 757 | 1073 | name = "linux-raw-sys" |
|
| 758 | 1074 | version = "0.12.1" |
|
| 759 | 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 4 unchanged lines |
||
| 764 | 1080 | version = "0.8.3" |
|
| 765 | 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 766 | 1082 | checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" |
|
| 1083 | + | ||
| 1084 | + | [[package]] |
|
| 1085 | + | name = "lock_api" |
|
| 1086 | + | version = "0.4.14" |
|
| 1087 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1088 | + | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" |
|
| 1089 | + | dependencies = [ |
|
| 1090 | + | "scopeguard", |
|
| 1091 | + | ] |
|
| 767 | 1092 | ||
| 768 | 1093 | [[package]] |
|
| 769 | 1094 | name = "log" |
|
| … | 2 unchanged lines |
||
| 772 | 1097 | checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" |
|
| 773 | 1098 | ||
| 774 | 1099 | [[package]] |
|
| 1100 | + | name = "md-5" |
|
| 1101 | + | version = "0.10.6" |
|
| 1102 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1103 | + | checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" |
|
| 1104 | + | dependencies = [ |
|
| 1105 | + | "cfg-if", |
|
| 1106 | + | "digest 0.10.7", |
|
| 1107 | + | ] |
|
| 1108 | + | ||
| 1109 | + | [[package]] |
|
| 775 | 1110 | name = "memchr" |
|
| 776 | 1111 | version = "2.8.3" |
|
| 777 | 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 35 unchanged lines |
||
| 813 | 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 814 | 1149 | checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" |
|
| 815 | 1150 | dependencies = [ |
|
| 816 | - | "const-oid", |
|
| 817 | - | "crypto-common", |
|
| 1151 | + | "const-oid 0.10.2", |
|
| 1152 | + | "crypto-common 0.2.2", |
|
| 818 | 1153 | "ctutils", |
|
| 819 | 1154 | "hybrid-array", |
|
| 820 | 1155 | "module-lattice", |
|
| 821 | - | "pkcs8", |
|
| 1156 | + | "pkcs8 0.11.0", |
|
| 822 | 1157 | "shake", |
|
| 823 | - | "signature", |
|
| 1158 | + | "signature 3.0.0", |
|
| 824 | 1159 | ] |
|
| 825 | 1160 | ||
| 826 | 1161 | [[package]] |
|
| … | 4 unchanged lines |
||
| 831 | 1166 | dependencies = [ |
|
| 832 | 1167 | "ctutils", |
|
| 833 | 1168 | "hybrid-array", |
|
| 1169 | + | "num-traits", |
|
| 1170 | + | ] |
|
| 1171 | + | ||
| 1172 | + | [[package]] |
|
| 1173 | + | name = "num-bigint-dig" |
|
| 1174 | + | version = "0.8.6" |
|
| 1175 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1176 | + | checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" |
|
| 1177 | + | dependencies = [ |
|
| 1178 | + | "lazy_static", |
|
| 1179 | + | "libm", |
|
| 1180 | + | "num-integer", |
|
| 1181 | + | "num-iter", |
|
| 834 | 1182 | "num-traits", |
|
| 1183 | + | "rand 0.8.8", |
|
| 1184 | + | "smallvec", |
|
| 1185 | + | "zeroize", |
|
| 835 | 1186 | ] |
|
| 836 | 1187 | ||
| 837 | 1188 | [[package]] |
|
| 1189 | + | name = "num-integer" |
|
| 1190 | + | version = "0.1.47" |
|
| 1191 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1192 | + | checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" |
|
| 1193 | + | dependencies = [ |
|
| 1194 | + | "num-traits", |
|
| 1195 | + | ] |
|
| 1196 | + | ||
| 1197 | + | [[package]] |
|
| 1198 | + | name = "num-iter" |
|
| 1199 | + | version = "0.1.46" |
|
| 1200 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1201 | + | checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" |
|
| 1202 | + | dependencies = [ |
|
| 1203 | + | "num-integer", |
|
| 1204 | + | "num-traits", |
|
| 1205 | + | ] |
|
| 1206 | + | ||
| 1207 | + | [[package]] |
|
| 838 | 1208 | name = "num-traits" |
|
| 839 | 1209 | version = "0.2.19" |
|
| 840 | 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 841 | 1211 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" |
|
| 842 | 1212 | dependencies = [ |
|
| 843 | 1213 | "autocfg", |
|
| 1214 | + | "libm", |
|
| 844 | 1215 | ] |
|
| 845 | 1216 | ||
| 846 | 1217 | [[package]] |
|
| … | 32 unchanged lines |
||
| 879 | 1250 | "rustls", |
|
| 880 | 1251 | "rustls-pemfile", |
|
| 881 | 1252 | "serde_json", |
|
| 882 | - | "sha2", |
|
| 1253 | + | "sha2 0.11.0", |
|
| 883 | 1254 | "similar", |
|
| 884 | 1255 | "tempfile", |
|
| 885 | 1256 | "thiserror 1.0.69", |
|
| … | 3 unchanged lines |
||
| 889 | 1260 | ] |
|
| 890 | 1261 | ||
| 891 | 1262 | [[package]] |
|
| 1263 | + | name = "parking" |
|
| 1264 | + | version = "2.2.1" |
|
| 1265 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1266 | + | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" |
|
| 1267 | + | ||
| 1268 | + | [[package]] |
|
| 1269 | + | name = "parking_lot" |
|
| 1270 | + | version = "0.12.5" |
|
| 1271 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1272 | + | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" |
|
| 1273 | + | dependencies = [ |
|
| 1274 | + | "lock_api", |
|
| 1275 | + | "parking_lot_core", |
|
| 1276 | + | ] |
|
| 1277 | + | ||
| 1278 | + | [[package]] |
|
| 1279 | + | name = "parking_lot_core" |
|
| 1280 | + | version = "0.9.12" |
|
| 1281 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1282 | + | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" |
|
| 1283 | + | dependencies = [ |
|
| 1284 | + | "cfg-if", |
|
| 1285 | + | "libc", |
|
| 1286 | + | "redox_syscall 0.5.18", |
|
| 1287 | + | "smallvec", |
|
| 1288 | + | "windows-link", |
|
| 1289 | + | ] |
|
| 1290 | + | ||
| 1291 | + | [[package]] |
|
| 1292 | + | name = "pem-rfc7468" |
|
| 1293 | + | version = "0.7.0" |
|
| 1294 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1295 | + | checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" |
|
| 1296 | + | dependencies = [ |
|
| 1297 | + | "base64ct", |
|
| 1298 | + | ] |
|
| 1299 | + | ||
| 1300 | + | [[package]] |
|
| 892 | 1301 | name = "percent-encoding" |
|
| 893 | 1302 | version = "2.3.2" |
|
| 894 | 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 4 unchanged lines |
||
| 899 | 1308 | version = "0.2.17" |
|
| 900 | 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 901 | 1310 | checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" |
|
| 1311 | + | ||
| 1312 | + | [[package]] |
|
| 1313 | + | name = "pkcs1" |
|
| 1314 | + | version = "0.7.5" |
|
| 1315 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1316 | + | checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" |
|
| 1317 | + | dependencies = [ |
|
| 1318 | + | "der 0.7.10", |
|
| 1319 | + | "pkcs8 0.10.2", |
|
| 1320 | + | "spki 0.7.3", |
|
| 1321 | + | ] |
|
| 902 | 1322 | ||
| 903 | 1323 | [[package]] |
|
| 904 | 1324 | name = "pkcs8" |
|
| 1325 | + | version = "0.10.2" |
|
| 1326 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1327 | + | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" |
|
| 1328 | + | dependencies = [ |
|
| 1329 | + | "der 0.7.10", |
|
| 1330 | + | "spki 0.7.3", |
|
| 1331 | + | ] |
|
| 1332 | + | ||
| 1333 | + | [[package]] |
|
| 1334 | + | name = "pkcs8" |
|
| 905 | 1335 | version = "0.11.0" |
|
| 906 | 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 907 | 1337 | checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" |
|
| 908 | 1338 | dependencies = [ |
|
| 909 | - | "der", |
|
| 910 | - | "spki", |
|
| 1339 | + | "der 0.8.2", |
|
| 1340 | + | "spki 0.8.0", |
|
| 911 | 1341 | ] |
|
| 912 | 1342 | ||
| 913 | 1343 | [[package]] |
|
| … | 3 unchanged lines |
||
| 917 | 1347 | checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" |
|
| 918 | 1348 | ||
| 919 | 1349 | [[package]] |
|
| 1350 | + | name = "plain" |
|
| 1351 | + | version = "0.2.3" |
|
| 1352 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1353 | + | checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" |
|
| 1354 | + | ||
| 1355 | + | [[package]] |
|
| 920 | 1356 | name = "portable-atomic" |
|
| 921 | 1357 | version = "1.15.0" |
|
| 922 | 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 174 unchanged lines |
||
| 1097 | 1533 | ] |
|
| 1098 | 1534 | ||
| 1099 | 1535 | [[package]] |
|
| 1536 | + | name = "redox_syscall" |
|
| 1537 | + | version = "0.5.18" |
|
| 1538 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1539 | + | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" |
|
| 1540 | + | dependencies = [ |
|
| 1541 | + | "bitflags", |
|
| 1542 | + | ] |
|
| 1543 | + | ||
| 1544 | + | [[package]] |
|
| 1545 | + | name = "redox_syscall" |
|
| 1546 | + | version = "0.9.4" |
|
| 1547 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1548 | + | checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" |
|
| 1549 | + | dependencies = [ |
|
| 1550 | + | "bitflags", |
|
| 1551 | + | ] |
|
| 1552 | + | ||
| 1553 | + | [[package]] |
|
| 1100 | 1554 | name = "regex-automata" |
|
| 1101 | 1555 | version = "0.4.18" |
|
| 1102 | 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 22 unchanged lines |
||
| 1125 | 1579 | "libc", |
|
| 1126 | 1580 | "untrusted", |
|
| 1127 | 1581 | "windows-sys 0.52.0", |
|
| 1582 | + | ] |
|
| 1583 | + | ||
| 1584 | + | [[package]] |
|
| 1585 | + | name = "rsa" |
|
| 1586 | + | version = "0.9.10" |
|
| 1587 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1588 | + | checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" |
|
| 1589 | + | dependencies = [ |
|
| 1590 | + | "const-oid 0.9.6", |
|
| 1591 | + | "digest 0.10.7", |
|
| 1592 | + | "num-bigint-dig", |
|
| 1593 | + | "num-integer", |
|
| 1594 | + | "num-traits", |
|
| 1595 | + | "pkcs1", |
|
| 1596 | + | "pkcs8 0.10.2", |
|
| 1597 | + | "rand_core 0.6.4", |
|
| 1598 | + | "signature 2.2.0", |
|
| 1599 | + | "spki 0.7.3", |
|
| 1600 | + | "subtle", |
|
| 1601 | + | "zeroize", |
|
| 1128 | 1602 | ] |
|
| 1129 | 1603 | ||
| 1130 | 1604 | [[package]] |
|
| … | 15 unchanged lines |
||
| 1146 | 1620 | "errno", |
|
| 1147 | 1621 | "libc", |
|
| 1148 | 1622 | "linux-raw-sys", |
|
| 1149 | - | "windows-sys 0.52.0", |
|
| 1623 | + | "windows-sys 0.61.2", |
|
| 1150 | 1624 | ] |
|
| 1151 | 1625 | ||
| 1152 | 1626 | [[package]] |
|
| … | 59 unchanged lines |
||
| 1212 | 1686 | ] |
|
| 1213 | 1687 | ||
| 1214 | 1688 | [[package]] |
|
| 1689 | + | name = "ryu" |
|
| 1690 | + | version = "1.0.23" |
|
| 1691 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1692 | + | checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" |
|
| 1693 | + | ||
| 1694 | + | [[package]] |
|
| 1215 | 1695 | name = "same-file" |
|
| 1216 | 1696 | version = "1.0.6" |
|
| 1217 | 1697 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 3 unchanged lines |
||
| 1221 | 1701 | ] |
|
| 1222 | 1702 | ||
| 1223 | 1703 | [[package]] |
|
| 1704 | + | name = "scopeguard" |
|
| 1705 | + | version = "1.2.0" |
|
| 1706 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1707 | + | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" |
|
| 1708 | + | ||
| 1709 | + | [[package]] |
|
| 1224 | 1710 | name = "semver" |
|
| 1225 | 1711 | version = "1.0.28" |
|
| 1226 | 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 40 unchanged lines |
||
| 1267 | 1753 | "serde", |
|
| 1268 | 1754 | "serde_core", |
|
| 1269 | 1755 | "zmij", |
|
| 1756 | + | ] |
|
| 1757 | + | ||
| 1758 | + | [[package]] |
|
| 1759 | + | name = "serde_spanned" |
|
| 1760 | + | version = "0.6.9" |
|
| 1761 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1762 | + | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" |
|
| 1763 | + | dependencies = [ |
|
| 1764 | + | "serde", |
|
| 1765 | + | ] |
|
| 1766 | + | ||
| 1767 | + | [[package]] |
|
| 1768 | + | name = "serde_urlencoded" |
|
| 1769 | + | version = "0.7.1" |
|
| 1770 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1771 | + | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" |
|
| 1772 | + | dependencies = [ |
|
| 1773 | + | "form_urlencoded", |
|
| 1774 | + | "itoa", |
|
| 1775 | + | "ryu", |
|
| 1776 | + | "serde", |
|
| 1777 | + | ] |
|
| 1778 | + | ||
| 1779 | + | [[package]] |
|
| 1780 | + | name = "sha1" |
|
| 1781 | + | version = "0.10.7" |
|
| 1782 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1783 | + | checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" |
|
| 1784 | + | dependencies = [ |
|
| 1785 | + | "cfg-if", |
|
| 1786 | + | "cpufeatures 0.2.17", |
|
| 1787 | + | "digest 0.10.7", |
|
| 1270 | 1788 | ] |
|
| 1271 | 1789 | ||
| 1272 | 1790 | [[package]] |
|
| 1273 | 1791 | name = "sha2" |
|
| 1792 | + | version = "0.10.9" |
|
| 1793 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1794 | + | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" |
|
| 1795 | + | dependencies = [ |
|
| 1796 | + | "cfg-if", |
|
| 1797 | + | "cpufeatures 0.2.17", |
|
| 1798 | + | "digest 0.10.7", |
|
| 1799 | + | ] |
|
| 1800 | + | ||
| 1801 | + | [[package]] |
|
| 1802 | + | name = "sha2" |
|
| 1274 | 1803 | version = "0.11.0" |
|
| 1275 | 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1276 | 1805 | checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" |
|
| 1277 | 1806 | dependencies = [ |
|
| 1278 | 1807 | "cfg-if", |
|
| 1279 | - | "cpufeatures", |
|
| 1280 | - | "digest", |
|
| 1808 | + | "cpufeatures 0.3.1", |
|
| 1809 | + | "digest 0.11.3", |
|
| 1281 | 1810 | ] |
|
| 1282 | 1811 | ||
| 1283 | 1812 | [[package]] |
|
| … | 2 unchanged lines |
||
| 1286 | 1815 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1287 | 1816 | checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" |
|
| 1288 | 1817 | dependencies = [ |
|
| 1289 | - | "digest", |
|
| 1818 | + | "digest 0.11.3", |
|
| 1290 | 1819 | "keccak", |
|
| 1291 | 1820 | "sponge-cursor", |
|
| 1292 | 1821 | ] |
|
| … | 16 unchanged lines |
||
| 1309 | 1838 | ||
| 1310 | 1839 | [[package]] |
|
| 1311 | 1840 | name = "signature" |
|
| 1841 | + | version = "2.2.0" |
|
| 1842 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1843 | + | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" |
|
| 1844 | + | dependencies = [ |
|
| 1845 | + | "digest 0.10.7", |
|
| 1846 | + | "rand_core 0.6.4", |
|
| 1847 | + | ] |
|
| 1848 | + | ||
| 1849 | + | [[package]] |
|
| 1850 | + | name = "signature" |
|
| 1312 | 1851 | version = "3.0.0" |
|
| 1313 | 1852 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1314 | 1853 | checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" |
|
| 1315 | 1854 | dependencies = [ |
|
| 1316 | - | "digest", |
|
| 1855 | + | "digest 0.11.3", |
|
| 1317 | 1856 | "rand_core 0.10.1", |
|
| 1318 | 1857 | ] |
|
| 1319 | 1858 | ||
| … | 20 unchanged lines |
||
| 1340 | 1879 | version = "1.16.2" |
|
| 1341 | 1880 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1342 | 1881 | checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" |
|
| 1882 | + | dependencies = [ |
|
| 1883 | + | "serde", |
|
| 1884 | + | ] |
|
| 1343 | 1885 | ||
| 1344 | 1886 | [[package]] |
|
| 1345 | 1887 | name = "socket2" |
|
| … | 12 unchanged lines |
||
| 1358 | 1900 | "anyhow", |
|
| 1359 | 1901 | "clap", |
|
| 1360 | 1902 | "console", |
|
| 1903 | + | "hex", |
|
| 1361 | 1904 | "indicatif", |
|
| 1362 | 1905 | "libc", |
|
| 1363 | 1906 | "mimalloc", |
|
| … | 3 unchanged lines |
||
| 1367 | 1910 | "spectra-api", |
|
| 1368 | 1911 | "spectra-core", |
|
| 1369 | 1912 | "spectra-mcp", |
|
| 1913 | + | "sqlx", |
|
| 1370 | 1914 | "tempfile", |
|
| 1371 | 1915 | "tokio", |
|
| 1372 | 1916 | ] |
|
| … | 11 unchanged lines |
||
| 1384 | 1928 | version = "0.1.0" |
|
| 1385 | 1929 | dependencies = [ |
|
| 1386 | 1930 | "anyhow", |
|
| 1931 | + | "blake3", |
|
| 1932 | + | "hex", |
|
| 1387 | 1933 | "oxy-core", |
|
| 1934 | + | "rand 0.8.8", |
|
| 1388 | 1935 | "serde", |
|
| 1389 | 1936 | "serde_json", |
|
| 1937 | + | "sqlx", |
|
| 1938 | + | "tempfile", |
|
| 1390 | 1939 | "thiserror 2.0.21", |
|
| 1940 | + | "tokio", |
|
| 1941 | + | "toml", |
|
| 1942 | + | "tracing", |
|
| 1391 | 1943 | ] |
|
| 1392 | 1944 | ||
| 1393 | 1945 | [[package]] |
|
| … | 5 unchanged lines |
||
| 1399 | 1951 | ] |
|
| 1400 | 1952 | ||
| 1401 | 1953 | [[package]] |
|
| 1954 | + | name = "spin" |
|
| 1955 | + | version = "0.9.9" |
|
| 1956 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1957 | + | checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" |
|
| 1958 | + | dependencies = [ |
|
| 1959 | + | "lock_api", |
|
| 1960 | + | ] |
|
| 1961 | + | ||
| 1962 | + | [[package]] |
|
| 1402 | 1963 | name = "spki" |
|
| 1964 | + | version = "0.7.3" |
|
| 1965 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1966 | + | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" |
|
| 1967 | + | dependencies = [ |
|
| 1968 | + | "base64ct", |
|
| 1969 | + | "der 0.7.10", |
|
| 1970 | + | ] |
|
| 1971 | + | ||
| 1972 | + | [[package]] |
|
| 1973 | + | name = "spki" |
|
| 1403 | 1974 | version = "0.8.0" |
|
| 1404 | 1975 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1405 | 1976 | checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" |
|
| 1406 | 1977 | dependencies = [ |
|
| 1407 | 1978 | "base64ct", |
|
| 1408 | - | "der", |
|
| 1979 | + | "der 0.8.2", |
|
| 1409 | 1980 | ] |
|
| 1410 | 1981 | ||
| 1411 | 1982 | [[package]] |
|
| … | 3 unchanged lines |
||
| 1415 | 1986 | checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" |
|
| 1416 | 1987 | ||
| 1417 | 1988 | [[package]] |
|
| 1989 | + | name = "sqlx" |
|
| 1990 | + | version = "0.8.6" |
|
| 1991 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1992 | + | checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" |
|
| 1993 | + | dependencies = [ |
|
| 1994 | + | "sqlx-core", |
|
| 1995 | + | "sqlx-macros", |
|
| 1996 | + | "sqlx-mysql", |
|
| 1997 | + | "sqlx-postgres", |
|
| 1998 | + | "sqlx-sqlite", |
|
| 1999 | + | ] |
|
| 2000 | + | ||
| 2001 | + | [[package]] |
|
| 2002 | + | name = "sqlx-core" |
|
| 2003 | + | version = "0.8.6" |
|
| 2004 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2005 | + | checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" |
|
| 2006 | + | dependencies = [ |
|
| 2007 | + | "base64", |
|
| 2008 | + | "bytes", |
|
| 2009 | + | "crc", |
|
| 2010 | + | "crossbeam-queue", |
|
| 2011 | + | "either", |
|
| 2012 | + | "event-listener", |
|
| 2013 | + | "futures-core", |
|
| 2014 | + | "futures-intrusive", |
|
| 2015 | + | "futures-io", |
|
| 2016 | + | "futures-util", |
|
| 2017 | + | "hashbrown 0.15.5", |
|
| 2018 | + | "hashlink", |
|
| 2019 | + | "indexmap", |
|
| 2020 | + | "log", |
|
| 2021 | + | "memchr", |
|
| 2022 | + | "once_cell", |
|
| 2023 | + | "percent-encoding", |
|
| 2024 | + | "serde", |
|
| 2025 | + | "serde_json", |
|
| 2026 | + | "sha2 0.10.9", |
|
| 2027 | + | "smallvec", |
|
| 2028 | + | "thiserror 2.0.21", |
|
| 2029 | + | "tokio", |
|
| 2030 | + | "tokio-stream", |
|
| 2031 | + | "tracing", |
|
| 2032 | + | "url", |
|
| 2033 | + | ] |
|
| 2034 | + | ||
| 2035 | + | [[package]] |
|
| 2036 | + | name = "sqlx-macros" |
|
| 2037 | + | version = "0.8.6" |
|
| 2038 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2039 | + | checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" |
|
| 2040 | + | dependencies = [ |
|
| 2041 | + | "proc-macro2", |
|
| 2042 | + | "quote", |
|
| 2043 | + | "sqlx-core", |
|
| 2044 | + | "sqlx-macros-core", |
|
| 2045 | + | "syn 2.0.119", |
|
| 2046 | + | ] |
|
| 2047 | + | ||
| 2048 | + | [[package]] |
|
| 2049 | + | name = "sqlx-macros-core" |
|
| 2050 | + | version = "0.8.6" |
|
| 2051 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2052 | + | checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" |
|
| 2053 | + | dependencies = [ |
|
| 2054 | + | "dotenvy", |
|
| 2055 | + | "either", |
|
| 2056 | + | "heck", |
|
| 2057 | + | "hex", |
|
| 2058 | + | "once_cell", |
|
| 2059 | + | "proc-macro2", |
|
| 2060 | + | "quote", |
|
| 2061 | + | "serde", |
|
| 2062 | + | "serde_json", |
|
| 2063 | + | "sha2 0.10.9", |
|
| 2064 | + | "sqlx-core", |
|
| 2065 | + | "sqlx-sqlite", |
|
| 2066 | + | "syn 2.0.119", |
|
| 2067 | + | "tokio", |
|
| 2068 | + | "url", |
|
| 2069 | + | ] |
|
| 2070 | + | ||
| 2071 | + | [[package]] |
|
| 2072 | + | name = "sqlx-mysql" |
|
| 2073 | + | version = "0.8.6" |
|
| 2074 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2075 | + | checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" |
|
| 2076 | + | dependencies = [ |
|
| 2077 | + | "atoi", |
|
| 2078 | + | "base64", |
|
| 2079 | + | "bitflags", |
|
| 2080 | + | "byteorder", |
|
| 2081 | + | "bytes", |
|
| 2082 | + | "crc", |
|
| 2083 | + | "digest 0.10.7", |
|
| 2084 | + | "dotenvy", |
|
| 2085 | + | "either", |
|
| 2086 | + | "futures-channel", |
|
| 2087 | + | "futures-core", |
|
| 2088 | + | "futures-io", |
|
| 2089 | + | "futures-util", |
|
| 2090 | + | "generic-array", |
|
| 2091 | + | "hex", |
|
| 2092 | + | "hkdf", |
|
| 2093 | + | "hmac", |
|
| 2094 | + | "itoa", |
|
| 2095 | + | "log", |
|
| 2096 | + | "md-5", |
|
| 2097 | + | "memchr", |
|
| 2098 | + | "once_cell", |
|
| 2099 | + | "percent-encoding", |
|
| 2100 | + | "rand 0.8.8", |
|
| 2101 | + | "rsa", |
|
| 2102 | + | "sha1", |
|
| 2103 | + | "sha2 0.10.9", |
|
| 2104 | + | "smallvec", |
|
| 2105 | + | "sqlx-core", |
|
| 2106 | + | "stringprep", |
|
| 2107 | + | "thiserror 2.0.21", |
|
| 2108 | + | "tracing", |
|
| 2109 | + | "whoami", |
|
| 2110 | + | ] |
|
| 2111 | + | ||
| 2112 | + | [[package]] |
|
| 2113 | + | name = "sqlx-postgres" |
|
| 2114 | + | version = "0.8.6" |
|
| 2115 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2116 | + | checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" |
|
| 2117 | + | dependencies = [ |
|
| 2118 | + | "atoi", |
|
| 2119 | + | "base64", |
|
| 2120 | + | "bitflags", |
|
| 2121 | + | "byteorder", |
|
| 2122 | + | "crc", |
|
| 2123 | + | "dotenvy", |
|
| 2124 | + | "etcetera", |
|
| 2125 | + | "futures-channel", |
|
| 2126 | + | "futures-core", |
|
| 2127 | + | "futures-util", |
|
| 2128 | + | "hex", |
|
| 2129 | + | "hkdf", |
|
| 2130 | + | "hmac", |
|
| 2131 | + | "home", |
|
| 2132 | + | "itoa", |
|
| 2133 | + | "log", |
|
| 2134 | + | "md-5", |
|
| 2135 | + | "memchr", |
|
| 2136 | + | "once_cell", |
|
| 2137 | + | "rand 0.8.8", |
|
| 2138 | + | "serde", |
|
| 2139 | + | "serde_json", |
|
| 2140 | + | "sha2 0.10.9", |
|
| 2141 | + | "smallvec", |
|
| 2142 | + | "sqlx-core", |
|
| 2143 | + | "stringprep", |
|
| 2144 | + | "thiserror 2.0.21", |
|
| 2145 | + | "tracing", |
|
| 2146 | + | "whoami", |
|
| 2147 | + | ] |
|
| 2148 | + | ||
| 2149 | + | [[package]] |
|
| 2150 | + | name = "sqlx-sqlite" |
|
| 2151 | + | version = "0.8.6" |
|
| 2152 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2153 | + | checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" |
|
| 2154 | + | dependencies = [ |
|
| 2155 | + | "atoi", |
|
| 2156 | + | "flume", |
|
| 2157 | + | "futures-channel", |
|
| 2158 | + | "futures-core", |
|
| 2159 | + | "futures-executor", |
|
| 2160 | + | "futures-intrusive", |
|
| 2161 | + | "futures-util", |
|
| 2162 | + | "libsqlite3-sys", |
|
| 2163 | + | "log", |
|
| 2164 | + | "percent-encoding", |
|
| 2165 | + | "serde", |
|
| 2166 | + | "serde_urlencoded", |
|
| 2167 | + | "sqlx-core", |
|
| 2168 | + | "thiserror 2.0.21", |
|
| 2169 | + | "tracing", |
|
| 2170 | + | "url", |
|
| 2171 | + | ] |
|
| 2172 | + | ||
| 2173 | + | [[package]] |
|
| 1418 | 2174 | name = "stable_deref_trait" |
|
| 1419 | 2175 | version = "1.2.1" |
|
| 1420 | 2176 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1421 | 2177 | checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" |
|
| 1422 | 2178 | ||
| 1423 | 2179 | [[package]] |
|
| 2180 | + | name = "stringprep" |
|
| 2181 | + | version = "0.1.5" |
|
| 2182 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2183 | + | checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" |
|
| 2184 | + | dependencies = [ |
|
| 2185 | + | "unicode-bidi", |
|
| 2186 | + | "unicode-normalization", |
|
| 2187 | + | "unicode-properties", |
|
| 2188 | + | ] |
|
| 2189 | + | ||
| 2190 | + | [[package]] |
|
| 1424 | 2191 | name = "strsim" |
|
| 1425 | 2192 | version = "0.11.1" |
|
| 1426 | 2193 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 48 unchanged lines |
||
| 1475 | 2242 | "getrandom 0.4.3", |
|
| 1476 | 2243 | "once_cell", |
|
| 1477 | 2244 | "rustix", |
|
| 1478 | - | "windows-sys 0.52.0", |
|
| 2245 | + | "windows-sys 0.61.2", |
|
| 1479 | 2246 | ] |
|
| 1480 | 2247 | ||
| 1481 | 2248 | [[package]] |
|
| … | 47 unchanged lines |
||
| 1529 | 2296 | ] |
|
| 1530 | 2297 | ||
| 1531 | 2298 | [[package]] |
|
| 2299 | + | name = "tinyvec" |
|
| 2300 | + | version = "1.13.3" |
|
| 2301 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2302 | + | checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" |
|
| 2303 | + | ||
| 2304 | + | [[package]] |
|
| 1532 | 2305 | name = "tokio" |
|
| 1533 | 2306 | version = "1.53.1" |
|
| 1534 | 2307 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 18 unchanged lines |
||
| 1553 | 2326 | "proc-macro2", |
|
| 1554 | 2327 | "quote", |
|
| 1555 | 2328 | "syn 3.0.6", |
|
| 2329 | + | ] |
|
| 2330 | + | ||
| 2331 | + | [[package]] |
|
| 2332 | + | name = "tokio-stream" |
|
| 2333 | + | version = "0.1.19" |
|
| 2334 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2335 | + | checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" |
|
| 2336 | + | dependencies = [ |
|
| 2337 | + | "futures-core", |
|
| 2338 | + | "pin-project-lite", |
|
| 2339 | + | "tokio", |
|
| 2340 | + | ] |
|
| 2341 | + | ||
| 2342 | + | [[package]] |
|
| 2343 | + | name = "toml" |
|
| 2344 | + | version = "0.8.23" |
|
| 2345 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2346 | + | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" |
|
| 2347 | + | dependencies = [ |
|
| 2348 | + | "serde", |
|
| 2349 | + | "serde_spanned", |
|
| 2350 | + | "toml_datetime", |
|
| 2351 | + | "toml_edit", |
|
| 2352 | + | ] |
|
| 2353 | + | ||
| 2354 | + | [[package]] |
|
| 2355 | + | name = "toml_datetime" |
|
| 2356 | + | version = "0.6.11" |
|
| 2357 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2358 | + | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" |
|
| 2359 | + | dependencies = [ |
|
| 2360 | + | "serde", |
|
| 1556 | 2361 | ] |
|
| 1557 | 2362 | ||
| 1558 | 2363 | [[package]] |
|
| 2364 | + | name = "toml_edit" |
|
| 2365 | + | version = "0.22.27" |
|
| 2366 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2367 | + | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" |
|
| 2368 | + | dependencies = [ |
|
| 2369 | + | "indexmap", |
|
| 2370 | + | "serde", |
|
| 2371 | + | "serde_spanned", |
|
| 2372 | + | "toml_datetime", |
|
| 2373 | + | "toml_write", |
|
| 2374 | + | "winnow", |
|
| 2375 | + | ] |
|
| 2376 | + | ||
| 2377 | + | [[package]] |
|
| 2378 | + | name = "toml_write" |
|
| 2379 | + | version = "0.1.2" |
|
| 2380 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2381 | + | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" |
|
| 2382 | + | ||
| 2383 | + | [[package]] |
|
| 2384 | + | name = "tracing" |
|
| 2385 | + | version = "0.1.44" |
|
| 2386 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2387 | + | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" |
|
| 2388 | + | dependencies = [ |
|
| 2389 | + | "log", |
|
| 2390 | + | "pin-project-lite", |
|
| 2391 | + | "tracing-attributes", |
|
| 2392 | + | "tracing-core", |
|
| 2393 | + | ] |
|
| 2394 | + | ||
| 2395 | + | [[package]] |
|
| 2396 | + | name = "tracing-attributes" |
|
| 2397 | + | version = "0.1.31" |
|
| 2398 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2399 | + | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" |
|
| 2400 | + | dependencies = [ |
|
| 2401 | + | "proc-macro2", |
|
| 2402 | + | "quote", |
|
| 2403 | + | "syn 2.0.119", |
|
| 2404 | + | ] |
|
| 2405 | + | ||
| 2406 | + | [[package]] |
|
| 2407 | + | name = "tracing-core" |
|
| 2408 | + | version = "0.1.36" |
|
| 2409 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2410 | + | checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" |
|
| 2411 | + | dependencies = [ |
|
| 2412 | + | "once_cell", |
|
| 2413 | + | ] |
|
| 2414 | + | ||
| 2415 | + | [[package]] |
|
| 1559 | 2416 | name = "typenum" |
|
| 1560 | 2417 | version = "1.20.1" |
|
| 1561 | 2418 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 4 unchanged lines |
||
| 1566 | 2423 | version = "0.1.4" |
|
| 1567 | 2424 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1568 | 2425 | checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" |
|
| 2426 | + | ||
| 2427 | + | [[package]] |
|
| 2428 | + | name = "unicode-bidi" |
|
| 2429 | + | version = "0.3.18" |
|
| 2430 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2431 | + | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" |
|
| 1569 | 2432 | ||
| 1570 | 2433 | [[package]] |
|
| 1571 | 2434 | name = "unicode-ident" |
|
| 1572 | 2435 | version = "1.0.26" |
|
| 1573 | 2436 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1574 | 2437 | checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" |
|
| 2438 | + | ||
| 2439 | + | [[package]] |
|
| 2440 | + | name = "unicode-normalization" |
|
| 2441 | + | version = "0.1.25" |
|
| 2442 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2443 | + | checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" |
|
| 2444 | + | dependencies = [ |
|
| 2445 | + | "tinyvec", |
|
| 2446 | + | ] |
|
| 2447 | + | ||
| 2448 | + | [[package]] |
|
| 2449 | + | name = "unicode-properties" |
|
| 2450 | + | version = "0.1.4" |
|
| 2451 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2452 | + | checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" |
|
| 1575 | 2453 | ||
| 1576 | 2454 | [[package]] |
|
| 1577 | 2455 | name = "unicode-width" |
|
| … | 48 unchanged lines |
||
| 1626 | 2504 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" |
|
| 1627 | 2505 | ||
| 1628 | 2506 | [[package]] |
|
| 2507 | + | name = "vcpkg" |
|
| 2508 | + | version = "0.2.15" |
|
| 2509 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2510 | + | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" |
|
| 2511 | + | ||
| 2512 | + | [[package]] |
|
| 2513 | + | name = "version_check" |
|
| 2514 | + | version = "0.9.5" |
|
| 2515 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2516 | + | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" |
|
| 2517 | + | ||
| 2518 | + | [[package]] |
|
| 1629 | 2519 | name = "wait-timeout" |
|
| 1630 | 2520 | version = "0.2.1" |
|
| 1631 | 2521 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 28 unchanged lines |
||
| 1660 | 2550 | ] |
|
| 1661 | 2551 | ||
| 1662 | 2552 | [[package]] |
|
| 2553 | + | name = "wasite" |
|
| 2554 | + | version = "0.1.0" |
|
| 2555 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2556 | + | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" |
|
| 2557 | + | ||
| 2558 | + | [[package]] |
|
| 1663 | 2559 | name = "wasm-bindgen" |
|
| 1664 | 2560 | version = "0.2.129" |
|
| 1665 | 2561 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 67 unchanged lines |
||
| 1733 | 2629 | ] |
|
| 1734 | 2630 | ||
| 1735 | 2631 | [[package]] |
|
| 2632 | + | name = "whoami" |
|
| 2633 | + | version = "1.6.1" |
|
| 2634 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2635 | + | checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" |
|
| 2636 | + | dependencies = [ |
|
| 2637 | + | "libredox", |
|
| 2638 | + | "wasite", |
|
| 2639 | + | ] |
|
| 2640 | + | ||
| 2641 | + | [[package]] |
|
| 1736 | 2642 | name = "winapi-util" |
|
| 1737 | 2643 | version = "0.1.11" |
|
| 1738 | 2644 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1739 | 2645 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" |
|
| 1740 | 2646 | dependencies = [ |
|
| 1741 | - | "windows-sys 0.52.0", |
|
| 2647 | + | "windows-sys 0.61.2", |
|
| 1742 | 2648 | ] |
|
| 1743 | 2649 | ||
| 1744 | 2650 | [[package]] |
|
| … | 4 unchanged lines |
||
| 1749 | 2655 | ||
| 1750 | 2656 | [[package]] |
|
| 1751 | 2657 | name = "windows-sys" |
|
| 2658 | + | version = "0.48.0" |
|
| 2659 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2660 | + | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" |
|
| 2661 | + | dependencies = [ |
|
| 2662 | + | "windows-targets 0.48.5", |
|
| 2663 | + | ] |
|
| 2664 | + | ||
| 2665 | + | [[package]] |
|
| 2666 | + | name = "windows-sys" |
|
| 1752 | 2667 | version = "0.52.0" |
|
| 1753 | 2668 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1754 | 2669 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" |
|
| 1755 | 2670 | dependencies = [ |
|
| 1756 | - | "windows-targets", |
|
| 2671 | + | "windows-targets 0.52.6", |
|
| 1757 | 2672 | ] |
|
| 1758 | 2673 | ||
| 1759 | 2674 | [[package]] |
|
| … | 2 unchanged lines |
||
| 1762 | 2677 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1763 | 2678 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" |
|
| 1764 | 2679 | dependencies = [ |
|
| 1765 | - | "windows-targets", |
|
| 2680 | + | "windows-targets 0.52.6", |
|
| 1766 | 2681 | ] |
|
| 1767 | 2682 | ||
| 1768 | 2683 | [[package]] |
|
| … | 3 unchanged lines |
||
| 1772 | 2687 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" |
|
| 1773 | 2688 | dependencies = [ |
|
| 1774 | 2689 | "windows-link", |
|
| 2690 | + | ] |
|
| 2691 | + | ||
| 2692 | + | [[package]] |
|
| 2693 | + | name = "windows-targets" |
|
| 2694 | + | version = "0.48.5" |
|
| 2695 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2696 | + | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" |
|
| 2697 | + | dependencies = [ |
|
| 2698 | + | "windows_aarch64_gnullvm 0.48.5", |
|
| 2699 | + | "windows_aarch64_msvc 0.48.5", |
|
| 2700 | + | "windows_i686_gnu 0.48.5", |
|
| 2701 | + | "windows_i686_msvc 0.48.5", |
|
| 2702 | + | "windows_x86_64_gnu 0.48.5", |
|
| 2703 | + | "windows_x86_64_gnullvm 0.48.5", |
|
| 2704 | + | "windows_x86_64_msvc 0.48.5", |
|
| 1775 | 2705 | ] |
|
| 1776 | 2706 | ||
| 1777 | 2707 | [[package]] |
|
| … | 2 unchanged lines |
||
| 1780 | 2710 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1781 | 2711 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" |
|
| 1782 | 2712 | dependencies = [ |
|
| 1783 | - | "windows_aarch64_gnullvm", |
|
| 1784 | - | "windows_aarch64_msvc", |
|
| 1785 | - | "windows_i686_gnu", |
|
| 2713 | + | "windows_aarch64_gnullvm 0.52.6", |
|
| 2714 | + | "windows_aarch64_msvc 0.52.6", |
|
| 2715 | + | "windows_i686_gnu 0.52.6", |
|
| 1786 | 2716 | "windows_i686_gnullvm", |
|
| 1787 | - | "windows_i686_msvc", |
|
| 1788 | - | "windows_x86_64_gnu", |
|
| 1789 | - | "windows_x86_64_gnullvm", |
|
| 1790 | - | "windows_x86_64_msvc", |
|
| 2717 | + | "windows_i686_msvc 0.52.6", |
|
| 2718 | + | "windows_x86_64_gnu 0.52.6", |
|
| 2719 | + | "windows_x86_64_gnullvm 0.52.6", |
|
| 2720 | + | "windows_x86_64_msvc 0.52.6", |
|
| 1791 | 2721 | ] |
|
| 2722 | + | ||
| 2723 | + | [[package]] |
|
| 2724 | + | name = "windows_aarch64_gnullvm" |
|
| 2725 | + | version = "0.48.5" |
|
| 2726 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2727 | + | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" |
|
| 1792 | 2728 | ||
| 1793 | 2729 | [[package]] |
|
| 1794 | 2730 | name = "windows_aarch64_gnullvm" |
|
| … | 3 unchanged lines |
||
| 1798 | 2734 | ||
| 1799 | 2735 | [[package]] |
|
| 1800 | 2736 | name = "windows_aarch64_msvc" |
|
| 2737 | + | version = "0.48.5" |
|
| 2738 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2739 | + | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" |
|
| 2740 | + | ||
| 2741 | + | [[package]] |
|
| 2742 | + | name = "windows_aarch64_msvc" |
|
| 1801 | 2743 | version = "0.52.6" |
|
| 1802 | 2744 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1803 | 2745 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" |
|
| 2746 | + | ||
| 2747 | + | [[package]] |
|
| 2748 | + | name = "windows_i686_gnu" |
|
| 2749 | + | version = "0.48.5" |
|
| 2750 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2751 | + | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" |
|
| 1804 | 2752 | ||
| 1805 | 2753 | [[package]] |
|
| 1806 | 2754 | name = "windows_i686_gnu" |
|
| … | 6 unchanged lines |
||
| 1813 | 2761 | version = "0.52.6" |
|
| 1814 | 2762 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1815 | 2763 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" |
|
| 2764 | + | ||
| 2765 | + | [[package]] |
|
| 2766 | + | name = "windows_i686_msvc" |
|
| 2767 | + | version = "0.48.5" |
|
| 2768 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2769 | + | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" |
|
| 1816 | 2770 | ||
| 1817 | 2771 | [[package]] |
|
| 1818 | 2772 | name = "windows_i686_msvc" |
|
| … | 3 unchanged lines |
||
| 1822 | 2776 | ||
| 1823 | 2777 | [[package]] |
|
| 1824 | 2778 | name = "windows_x86_64_gnu" |
|
| 2779 | + | version = "0.48.5" |
|
| 2780 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2781 | + | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" |
|
| 2782 | + | ||
| 2783 | + | [[package]] |
|
| 2784 | + | name = "windows_x86_64_gnu" |
|
| 1825 | 2785 | version = "0.52.6" |
|
| 1826 | 2786 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1827 | 2787 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" |
|
| 1828 | 2788 | ||
| 1829 | 2789 | [[package]] |
|
| 1830 | 2790 | name = "windows_x86_64_gnullvm" |
|
| 2791 | + | version = "0.48.5" |
|
| 2792 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2793 | + | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" |
|
| 2794 | + | ||
| 2795 | + | [[package]] |
|
| 2796 | + | name = "windows_x86_64_gnullvm" |
|
| 1831 | 2797 | version = "0.52.6" |
|
| 1832 | 2798 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1833 | 2799 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" |
|
| 2800 | + | ||
| 2801 | + | [[package]] |
|
| 2802 | + | name = "windows_x86_64_msvc" |
|
| 2803 | + | version = "0.48.5" |
|
| 2804 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2805 | + | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" |
|
| 1834 | 2806 | ||
| 1835 | 2807 | [[package]] |
|
| 1836 | 2808 | name = "windows_x86_64_msvc" |
|
| 1837 | 2809 | version = "0.52.6" |
|
| 1838 | 2810 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1839 | 2811 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" |
|
| 2812 | + | ||
| 2813 | + | [[package]] |
|
| 2814 | + | name = "winnow" |
|
| 2815 | + | version = "0.7.15" |
|
| 2816 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2817 | + | checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" |
|
| 2818 | + | dependencies = [ |
|
| 2819 | + | "memchr", |
|
| 2820 | + | ] |
|
| 1840 | 2821 | ||
| 1841 | 2822 | [[package]] |
|
| 1842 | 2823 | name = "wit-bindgen" |
|
| … | 152 unchanged lines |
modified Cargo.toml +2 −0
| … | 66 unchanged lines |
||
| 67 | 67 | clap.workspace = true |
|
| 68 | 68 | serde_json.workspace = true |
|
| 69 | 69 | tokio.workspace = true |
|
| 70 | + | hex.workspace = true |
|
| 70 | 71 | console = "0.15" |
|
| 71 | 72 | indicatif = "0.17" |
|
| 72 | 73 | libc = "0.2" |
|
| … | 1 unchanged lines |
||
| 74 | 75 | ||
| 75 | 76 | [dev-dependencies] |
|
| 76 | 77 | tempfile.workspace = true |
|
| 78 | + | sqlx.workspace = true |
|
| 77 | 79 | ||
| 78 | 80 | [profile.release] |
|
| 79 | 81 | debug = 1 |
modified Makefile +7 −3
| 1 | 1 | .PHONY: build check test fmt clippy changelog run ts install deps-check |
|
| 2 | 2 | ||
| 3 | + | # Spectra's own packages. `--workspace` and `--all` also reach the pinned |
|
| 4 | + | # Oxygen in deps/ (a path dependency), whose tests and lints are its own. |
|
| 5 | + | PKGS = -p spectra -p spectra-core -p spectra-mcp -p spectra-api -p radar |
|
| 6 | + | ||
| 3 | 7 | build: |
|
| 4 | 8 | cargo build --release |
|
| 5 | 9 | ||
| … | 4 unchanged lines |
||
| 10 | 14 | scripts/changelog-check.sh |
|
| 11 | 15 | ||
| 12 | 16 | fmt: |
|
| 13 | - | cargo fmt -p spectra -p spectra-core -p spectra-mcp -p spectra-api -p radar -- --check |
|
| 17 | + | cargo fmt $(PKGS) -- --check |
|
| 14 | 18 | ||
| 15 | 19 | clippy: |
|
| 16 | - | cargo clippy --workspace --all-targets -- -D warnings |
|
| 20 | + | cargo clippy $(PKGS) --all-targets -- -D warnings |
|
| 17 | 21 | ||
| 18 | 22 | test: |
|
| 19 | - | cargo test --workspace |
|
| 23 | + | cargo test $(PKGS) |
|
| 20 | 24 | ||
| 21 | 25 | run: |
|
| 22 | 26 | cargo run -- --data data serve |
|
| … | 16 unchanged lines |
modified crates/spectra-core/Cargo.toml +11 −0
| … | 10 unchanged lines |
||
| 11 | 11 | thiserror.workspace = true |
|
| 12 | 12 | serde.workspace = true |
|
| 13 | 13 | serde_json.workspace = true |
|
| 14 | + | sqlx.workspace = true |
|
| 15 | + | tokio.workspace = true |
|
| 16 | + | blake3.workspace = true |
|
| 17 | + | rand.workspace = true |
|
| 18 | + | hex.workspace = true |
|
| 19 | + | tracing.workspace = true |
|
| 20 | + | toml.workspace = true |
|
| 21 | + | ||
| 22 | + | [dev-dependencies] |
|
| 23 | + | tempfile.workspace = true |
|
| 24 | + | rand.workspace = true |
modified crates/spectra-core/src/lib.rs +7 −0
| … | 4 unchanged lines |
||
| 5 | 5 | //! Identities, delegation, composite signatures, deterministic CBOR and |
|
| 6 | 6 | //! hashing come from `oxy-core`; nothing here reimplements them. |
|
| 7 | 7 | ||
| 8 | + | pub mod event; |
|
| 9 | + | pub mod hlc; |
|
| 10 | + | pub mod ids; |
|
| 11 | + | pub mod keys; |
|
| 12 | + | pub mod ledger; |
|
| 13 | + | pub mod projections; |
|
| 14 | + | ||
| 8 | 15 | /// The ledger's format version: the frozen event envelope (spec §9.1). A |
|
| 9 | 16 | /// database written with a newer format is refused, never guessed at. |
|
| 10 | 17 | pub const LEDGER_FORMAT: u32 = 1; |
modified docs/plan.md +5 −3
| … | 8 unchanged lines |
||
| 9 | 9 | - Gate every commit on `make check` (changelog, fmt, clippy with warnings as errors, tests). |
|
| 10 | 10 | - Every code change adds a CHANGELOG.md line under `## Unreleased`, or sets `NO_CHANGELOG=1`. |
|
| 11 | 11 | - Never weaken a security goal of spec §5 to make a test pass. |
|
| 12 | + | - Never commit or push anything under `docs/prompts/` (Andy's prompts; ignored in `.oxyignore`). Name the paths in every commit. |
|
| 12 | 13 | - Never act as Andy: owner-only steps (delegating to an agent key, the acceptance run) wait for him. |
|
| 13 | 14 | ||
| 14 | 15 | ## Items |
|
| 15 | 16 | ||
| 16 | - | - [ ] #1 Project skeleton in Oxygen's shape |
|
| 17 | - | - [ ] #2 CI on OxygenFlow |
|
| 18 | - | - [ ] #3 The ledger |
|
| 17 | + | - [x] #1 Project skeleton in Oxygen's shape — done 2026-11-02: workspace, deps/oxygen pinned, make check, deps-check (212 crates, all vendored). |
|
| 18 | + | - [x] #2 CI on OxygenFlow — done 2026-11-02: run #1 passed fmt, clippy, test, build with no image rebuild. |
|
| 19 | + | - [x] #3 The ledger — done 2026-11-02: chain, frozen envelope, HLC, signed checkpoints every 256 events, verify/check/rebuild; append 0.15 ms (release), 10,000 random events with rebuild and verify 1.3 s. |
|
| 19 | 20 | - [ ] #4 Identities instead of tokens |
|
| 20 | 21 | - [ ] #5 Projects, presence and sessions |
|
| 21 | 22 | - [ ] #6 Channels, threads, messages, inbox and search |
|
| … | 8 unchanged lines |
||
| 30 | 31 | ## Log |
|
| 31 | 32 | ||
| 32 | 33 | - 2026-11-02 plan created; sprint adjusted to spec Draft 0.3 (identities and the ledger). |
|
| 34 | + | - 2026-11-02 #1 and #2 done (CI green on run #1); #3 done. Root disk was full: build trees go to /mnt/development/spectra-target (CARGO_TARGET_DIR). |
modified src/cli/mod.rs +61 −1
| … | 1 unchanged lines |
||
| 2 | 2 | //! glyphs on a UTF-8 terminal, plain when piped, with `--plain` or with |
|
| 3 | 3 | //! `NO_COLOR`; `--json` prints one document on stdout and nothing else. |
|
| 4 | 4 | ||
| 5 | + | pub mod admin; |
|
| 5 | 6 | pub mod format; |
|
| 6 | 7 | pub mod json; |
|
| 7 | 8 | pub mod ui; |
|
| 8 | 9 | ||
| 9 | - | use anyhow::Result; |
|
| 10 | + | use anyhow::{bail, Result}; |
|
| 10 | 11 | use clap::{Parser, Subcommand}; |
|
| 12 | + | use std::path::PathBuf; |
|
| 11 | 13 | ||
| 12 | 14 | #[derive(Parser)] |
|
| 13 | 15 | #[command(name = "spectra", version = env!("SPECTRA_VERSION_LONG"), about = "A control plane for AI coding agents")] |
|
| … | 1 unchanged lines |
||
| 15 | 17 | /// Plain output: no colour or glyphs (also NO_COLOR, or when not a terminal) |
|
| 16 | 18 | #[arg(long, global = true)] |
|
| 17 | 19 | pub plain: bool, |
|
| 20 | + | /// The data directory (default: $XDG_DATA_HOME/spectra, else ~/.local/share/spectra) |
|
| 21 | + | #[arg(long, global = true, env = "SPECTRA_DATA")] |
|
| 22 | + | pub data: Option<PathBuf>, |
|
| 18 | 23 | #[command(subcommand)] |
|
| 19 | 24 | pub command: Command, |
|
| 20 | 25 | } |
|
| … | 6 unchanged lines |
||
| 27 | 32 | #[arg(long)] |
|
| 28 | 33 | json: bool, |
|
| 29 | 34 | }, |
|
| 35 | + | /// The ledger's own checks: verify, check, rebuild |
|
| 36 | + | Admin { |
|
| 37 | + | #[command(subcommand)] |
|
| 38 | + | command: Admin, |
|
| 39 | + | }, |
|
| 40 | + | } |
|
| 41 | + | ||
| 42 | + | #[derive(Subcommand)] |
|
| 43 | + | pub enum Admin { |
|
| 44 | + | /// Recompute the hash chain and check every checkpoint and proof (exit 1 on any finding) |
|
| 45 | + | Verify { |
|
| 46 | + | /// Machine-readable output |
|
| 47 | + | #[arg(long)] |
|
| 48 | + | json: bool, |
|
| 49 | + | }, |
|
| 50 | + | /// Whether the projections equal a rebuild from the ledger |
|
| 51 | + | Check, |
|
| 52 | + | /// Replay the ledger into every projection |
|
| 53 | + | Rebuild, |
|
| 54 | + | } |
|
| 55 | + | ||
| 56 | + | /// What every command needs: where the data lives. |
|
| 57 | + | pub struct Ctx { |
|
| 58 | + | pub data: PathBuf, |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | impl Ctx { |
|
| 62 | + | /// The ledger's database, which must exist. |
|
| 63 | + | pub fn db(&self) -> Result<PathBuf> { |
|
| 64 | + | let db = self.data.join("spectra.db"); |
|
| 65 | + | if !db.exists() { |
|
| 66 | + | bail!("no Spectra in {} (run `spectra init`, or pass --data)", self.data.display()); |
|
| 67 | + | } |
|
| 68 | + | Ok(db) |
|
| 69 | + | } |
|
| 70 | + | } |
|
| 71 | + | ||
| 72 | + | fn default_data() -> PathBuf { |
|
| 73 | + | if let Ok(x) = std::env::var("XDG_DATA_HOME") { |
|
| 74 | + | return PathBuf::from(x).join("spectra"); |
|
| 75 | + | } |
|
| 76 | + | let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); |
|
| 77 | + | PathBuf::from(home).join(".local").join("share").join("spectra") |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | fn runtime() -> Result<tokio::runtime::Runtime> { |
|
| 81 | + | Ok(tokio::runtime::Builder::new_multi_thread().enable_all().build()?) |
|
| 30 | 82 | } |
|
| 31 | 83 | ||
| 32 | 84 | pub fn run() -> Result<i32> { |
|
| 33 | 85 | let cli = Cli::parse(); |
|
| 34 | 86 | ui::init(cli.plain); |
|
| 87 | + | let ctx = Ctx { data: cli.data.unwrap_or_else(default_data) }; |
|
| 35 | 88 | match cli.command { |
|
| 36 | 89 | Command::Version { json } => version(json), |
|
| 90 | + | Command::Admin { command } => runtime()?.block_on(async { |
|
| 91 | + | match command { |
|
| 92 | + | Admin::Verify { json } => admin::verify(&ctx, json).await, |
|
| 93 | + | Admin::Check => admin::check(&ctx).await, |
|
| 94 | + | Admin::Rebuild => admin::rebuild(&ctx).await, |
|
| 95 | + | } |
|
| 96 | + | }), |
|
| 37 | 97 | } |
|
| 38 | 98 | } |
|
| 39 | 99 | ||
| … | 16 unchanged lines |