1use super::{
2 error::IdentityError,
3 types::{DialectId, IdentityRole},
4};
5use crate::capability::{CasResult, StateEntry, StateStore, StateWrite, Version};
6use bytes::Bytes;
7use serde::{Deserialize, Serialize};
8use std::time::SystemTime;
9
10const STATE_SCHEMA: u16 = 2;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct IdentityTarget {
16 pub model: String,
17 pub dialect: DialectId,
18 pub origin: Option<String>,
21}
22
23impl IdentityTarget {
24 pub fn new(model: impl Into<String>, dialect: DialectId) -> Result<Self, IdentityError> {
25 let model = model.into();
26 if model.trim().is_empty() {
27 return Err(IdentityError::InvalidIdentity(
28 "target model is invalid".into(),
29 ));
30 }
31 Ok(Self {
32 model,
33 dialect,
34 origin: None,
35 })
36 }
37
38 pub fn with_origin(mut self, origin: impl Into<String>) -> Result<Self, IdentityError> {
39 let origin = origin.into();
40 if origin.trim().is_empty() {
41 return Err(IdentityError::InvalidIdentity(
42 "identity origin is empty".into(),
43 ));
44 }
45 self.origin = Some(origin);
46 Ok(self)
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53pub enum OpaqueField {
54 ClaudeThinkingSignature,
55 ClaudeRedactedThinkingData,
56 GeminiPartThoughtSignature,
57 ResponsesReasoningEncryptedContent,
58}
59
60impl OpaqueField {
61 pub fn dialect(self) -> DialectId {
62 match self {
63 Self::ClaudeThinkingSignature | Self::ClaudeRedactedThinkingData => DialectId::Claude,
64 Self::GeminiPartThoughtSignature => DialectId::Gemini,
65 Self::ResponsesReasoningEncryptedContent => DialectId::OpenAi,
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct OpaqueSignature {
74 pub field: OpaqueField,
75 pub value: String,
76 pub origin: String,
77 pub model: String,
78}
79
80impl OpaqueSignature {
81 pub fn new(
82 field: OpaqueField,
83 value: impl Into<String>,
84 origin: impl Into<String>,
85 model: impl Into<String>,
86 ) -> Result<Self, IdentityError> {
87 let value = value.into();
88 let origin = origin.into();
89 let model = model.into();
90 if value.is_empty() || origin.trim().is_empty() || model.trim().is_empty() {
91 return Err(IdentityError::InvalidIdentity(
92 "opaque signature declaration is invalid".into(),
93 ));
94 }
95 Ok(Self {
96 field,
97 value,
98 origin,
99 model,
100 })
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct IdentityStateRecord {
107 pub schema: u16,
108 pub role: IdentityRole,
109 pub target: IdentityTarget,
110 pub original_item_id: Option<String>,
111 pub original_call_id: Option<String>,
112 pub client_item_id: Option<String>,
113 pub client_call_id: Option<String>,
114 pub tool_name: Option<String>,
115 pub response_id: Option<String>,
116 pub conversation_id: Option<String>,
117 pub opaque_signature: Option<OpaqueSignature>,
118}
119
120impl IdentityStateRecord {
121 pub fn new(role: IdentityRole, target: IdentityTarget) -> Self {
122 Self {
123 schema: STATE_SCHEMA,
124 role,
125 target,
126 original_item_id: None,
127 original_call_id: None,
128 client_item_id: None,
129 client_call_id: None,
130 tool_name: None,
131 response_id: None,
132 conversation_id: None,
133 opaque_signature: None,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Default, PartialEq, Eq)]
141pub struct LateIdentityFacts {
142 pub original_item_id: Option<String>,
143 pub original_call_id: Option<String>,
144 pub client_item_id: Option<String>,
145 pub client_call_id: Option<String>,
146 pub tool_name: Option<String>,
147 pub response_id: Option<String>,
148 pub conversation_id: Option<String>,
149 pub opaque_signature: Option<OpaqueSignature>,
150}
151
152impl LateIdentityFacts {
153 fn apply(self, record: &mut IdentityStateRecord) -> Result<(), IdentityError> {
154 merge(
155 &mut record.original_item_id,
156 self.original_item_id,
157 "original_item_id",
158 )?;
159 merge(
160 &mut record.original_call_id,
161 self.original_call_id,
162 "original_call_id",
163 )?;
164 merge(
165 &mut record.client_item_id,
166 self.client_item_id,
167 "client_item_id",
168 )?;
169 merge(
170 &mut record.client_call_id,
171 self.client_call_id,
172 "client_call_id",
173 )?;
174 merge(&mut record.tool_name, self.tool_name, "tool_name")?;
175 merge(&mut record.response_id, self.response_id, "response_id")?;
176 merge(
177 &mut record.conversation_id,
178 self.conversation_id,
179 "conversation_id",
180 )?;
181 if let Some(signature) = self.opaque_signature {
182 if let Some(existing) = &record.opaque_signature {
183 if existing != &signature {
184 return Err(IdentityError::InvalidIdentity(
185 "opaque signature changed after emission".into(),
186 ));
187 }
188 } else {
189 record.opaque_signature = Some(signature);
190 }
191 }
192 Ok(())
193 }
194}
195
196fn merge(
197 slot: &mut Option<String>,
198 incoming: Option<String>,
199 field: &str,
200) -> Result<(), IdentityError> {
201 let Some(incoming) = incoming else {
202 return Ok(());
203 };
204 if let Some(existing) = slot {
205 if existing != &incoming {
206 return Err(IdentityError::InvalidIdentity(format!(
207 "{field} changed after emission"
208 )));
209 }
210 } else {
211 *slot = Some(incoming);
212 }
213 Ok(())
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct IdentityStateSnapshot {
218 pub record: IdentityStateRecord,
219 pub version: Version,
220 pub expires_at: Option<SystemTime>,
221}
222
223pub struct IdentityStateStore<S> {
225 store: S,
226}
227
228impl<S> IdentityStateStore<S> {
229 pub fn new(store: S) -> Self {
230 Self { store }
231 }
232 pub fn inner(&self) -> &S {
233 &self.store
234 }
235}
236
237impl<S: StateStore> IdentityStateStore<S> {
238 pub async fn read(
241 &self,
242 scope: &S::Scope,
243 key: &str,
244 ) -> Result<IdentityStateSnapshot, IdentityError> {
245 let Some(entry) = self
246 .store
247 .get(scope, key)
248 .await
249 .map_err(IdentityError::StateStore)?
250 else {
251 return Err(IdentityError::MissingState);
252 };
253 decode_entry(entry)
254 }
255
256 pub async fn save(
259 &self,
260 scope: &S::Scope,
261 key: &str,
262 expected: Option<Version>,
263 record: &IdentityStateRecord,
264 expires_at: Option<SystemTime>,
265 ) -> Result<Version, IdentityError> {
266 let payload = serde_json::to_vec(record).map_err(IdentityError::StateEncoding)?;
267 let result = self
268 .store
269 .compare_exchange(
270 scope,
271 key,
272 expected,
273 Some(StateWrite {
274 payload: Bytes::from(payload),
275 expires_at,
276 }),
277 )
278 .await
279 .map_err(IdentityError::StateStore)?;
280 match result {
281 CasResult::Applied(Some(version)) => Ok(version),
282 CasResult::Applied(None) | CasResult::Conflict => Err(IdentityError::Conflict),
283 }
284 }
285
286 pub async fn update_late(
289 &self,
290 scope: &S::Scope,
291 key: &str,
292 expected: Version,
293 facts: LateIdentityFacts,
294 ) -> Result<IdentityStateSnapshot, IdentityError> {
295 let current = self.read(scope, key).await?;
296 if current.version != expected {
297 return Err(IdentityError::Conflict);
298 }
299 let mut record = current.record;
300 facts.apply(&mut record)?;
301
302 let version = self
303 .save(scope, key, Some(expected), &record, current.expires_at)
304 .await?;
305 Ok(IdentityStateSnapshot {
306 record,
307 version,
308 expires_at: current.expires_at,
309 })
310 }
311}
312
313fn decode_entry(entry: StateEntry) -> Result<IdentityStateSnapshot, IdentityError> {
314 let record: IdentityStateRecord =
317 serde_json::from_slice(&entry.payload).map_err(IdentityError::StateEncoding)?;
318
319 Ok(IdentityStateSnapshot {
320 record,
321 version: entry.version,
322 expires_at: entry.expires_at,
323 })
324}