1use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "server")]
12pub mod crypto;
13#[cfg(feature = "server")]
14pub mod db;
15pub mod oauth;
16#[cfg(feature = "server")]
17pub mod service;
18
19pub use opensession_core::trace::{
21 Agent, Content, ContentBlock, Event, EventType, Session, SessionContext, Stats,
22};
23
24#[derive(Debug, Deserialize)]
28#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
29#[cfg_attr(feature = "ts", ts(export))]
30pub struct RegisterRequest {
31 pub nickname: String,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
36#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
37#[cfg_attr(feature = "ts", ts(export))]
38pub struct AuthRegisterRequest {
39 pub email: String,
40 pub password: String,
41 pub nickname: String,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
46#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
47#[cfg_attr(feature = "ts", ts(export))]
48pub struct LoginRequest {
49 pub email: String,
50 pub password: String,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
55#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
56#[cfg_attr(feature = "ts", ts(export))]
57pub struct AuthTokenResponse {
58 pub access_token: String,
59 pub refresh_token: String,
60 pub expires_in: u64,
61 pub user_id: String,
62 pub nickname: String,
63}
64
65#[derive(Debug, Serialize, Deserialize)]
67#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
68#[cfg_attr(feature = "ts", ts(export))]
69pub struct RefreshRequest {
70 pub refresh_token: String,
71}
72
73#[derive(Debug, Serialize, Deserialize)]
75#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
76#[cfg_attr(feature = "ts", ts(export))]
77pub struct LogoutRequest {
78 pub refresh_token: String,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
83#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
84#[cfg_attr(feature = "ts", ts(export))]
85pub struct ChangePasswordRequest {
86 pub current_password: String,
87 pub new_password: String,
88}
89
90#[derive(Debug, Serialize)]
92#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
93#[cfg_attr(feature = "ts", ts(export))]
94pub struct RegisterResponse {
95 pub user_id: String,
96 pub nickname: String,
97 pub api_key: String,
98 pub is_admin: bool,
99}
100
101#[derive(Debug, Serialize, Deserialize)]
103#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
104#[cfg_attr(feature = "ts", ts(export))]
105pub struct VerifyResponse {
106 pub user_id: String,
107 pub nickname: String,
108}
109
110#[derive(Debug, Serialize, Deserialize)]
112#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
113#[cfg_attr(feature = "ts", ts(export))]
114pub struct UserSettingsResponse {
115 pub user_id: String,
116 pub nickname: String,
117 pub api_key: String,
118 pub is_admin: bool,
119 pub created_at: String,
120 pub email: Option<String>,
121 pub avatar_url: Option<String>,
122 #[serde(default)]
124 pub oauth_providers: Vec<oauth::LinkedProvider>,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub github_username: Option<String>,
128}
129
130#[derive(Debug, Serialize, Deserialize)]
132#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
133#[cfg_attr(feature = "ts", ts(export))]
134pub struct OkResponse {
135 pub ok: bool,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
140#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
141#[cfg_attr(feature = "ts", ts(export))]
142pub struct RegenerateKeyResponse {
143 pub api_key: String,
144}
145
146#[derive(Debug, Serialize)]
148#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
149#[cfg_attr(feature = "ts", ts(export))]
150pub struct OAuthLinkResponse {
151 pub url: String,
152}
153
154#[derive(Debug, Serialize, Deserialize)]
158#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
159#[cfg_attr(feature = "ts", ts(export))]
160pub struct UploadRequest {
161 #[cfg_attr(feature = "ts", ts(type = "any"))]
162 pub session: serde_json::Value, pub team_id: Option<String>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub body_url: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub linked_session_ids: Option<Vec<String>>,
168}
169
170#[derive(Debug, Serialize, Deserialize)]
172#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
173#[cfg_attr(feature = "ts", ts(export))]
174pub struct UploadResponse {
175 pub id: String,
176 pub url: String,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
182#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
183#[cfg_attr(feature = "ts", ts(export))]
184pub struct SessionSummary {
185 pub id: String,
186 pub user_id: Option<String>,
187 pub nickname: Option<String>,
188 pub team_id: String,
189 pub tool: String,
190 pub agent_provider: Option<String>,
191 pub agent_model: Option<String>,
192 pub title: Option<String>,
193 pub description: Option<String>,
194 pub tags: Option<String>,
196 pub created_at: String,
197 pub uploaded_at: String,
198 pub message_count: i64,
199 pub task_count: i64,
200 pub event_count: i64,
201 pub duration_seconds: i64,
202 pub total_input_tokens: i64,
203 pub total_output_tokens: i64,
204}
205
206#[derive(Debug, Serialize, Deserialize)]
208#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
209#[cfg_attr(feature = "ts", ts(export))]
210pub struct SessionListResponse {
211 pub sessions: Vec<SessionSummary>,
212 pub total: i64,
213 pub page: u32,
214 pub per_page: u32,
215}
216
217#[derive(Debug, Deserialize)]
219#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
220#[cfg_attr(feature = "ts", ts(export))]
221pub struct SessionListQuery {
222 #[serde(default = "default_page")]
223 pub page: u32,
224 #[serde(default = "default_per_page")]
225 pub per_page: u32,
226 pub search: Option<String>,
227 pub tool: Option<String>,
228 pub team_id: Option<String>,
229 pub sort: Option<String>,
231 pub time_range: Option<String>,
233}
234
235fn default_page() -> u32 {
236 1
237}
238fn default_per_page() -> u32 {
239 20
240}
241
242#[derive(Debug, Serialize, Deserialize)]
244#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
245#[cfg_attr(feature = "ts", ts(export))]
246pub struct SessionDetail {
247 #[serde(flatten)]
248 #[cfg_attr(feature = "ts", ts(flatten))]
249 pub summary: SessionSummary,
250 pub team_name: Option<String>,
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
252 pub linked_sessions: Vec<SessionLink>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
258#[cfg_attr(feature = "ts", ts(export))]
259pub struct SessionLink {
260 pub session_id: String,
261 pub linked_session_id: String,
262 pub link_type: String,
263 pub created_at: String,
264}
265
266#[derive(Debug, Serialize, Deserialize)]
270#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
271#[cfg_attr(feature = "ts", ts(export))]
272pub struct CreateTeamRequest {
273 pub name: String,
274 pub description: Option<String>,
275 pub is_public: Option<bool>,
276}
277
278#[derive(Debug, Serialize, Deserialize)]
280#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
281#[cfg_attr(feature = "ts", ts(export))]
282pub struct TeamResponse {
283 pub id: String,
284 pub name: String,
285 pub description: Option<String>,
286 pub is_public: bool,
287 pub created_by: String,
288 pub created_at: String,
289}
290
291#[derive(Debug, Serialize, Deserialize)]
293#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
294#[cfg_attr(feature = "ts", ts(export))]
295pub struct ListTeamsResponse {
296 pub teams: Vec<TeamResponse>,
297}
298
299#[derive(Debug, Serialize, Deserialize)]
301#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
302#[cfg_attr(feature = "ts", ts(export))]
303pub struct TeamDetailResponse {
304 #[serde(flatten)]
305 #[cfg_attr(feature = "ts", ts(flatten))]
306 pub team: TeamResponse,
307 pub member_count: i64,
308 pub sessions: Vec<SessionSummary>,
309}
310
311#[derive(Debug, Serialize, Deserialize)]
313#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
314#[cfg_attr(feature = "ts", ts(export))]
315pub struct UpdateTeamRequest {
316 pub name: Option<String>,
317 pub description: Option<String>,
318 pub is_public: Option<bool>,
319}
320
321#[derive(Debug, Deserialize)]
323#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
324#[cfg_attr(feature = "ts", ts(export))]
325pub struct TeamStatsQuery {
326 pub time_range: Option<String>,
328}
329
330#[derive(Debug, Serialize)]
332#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
333#[cfg_attr(feature = "ts", ts(export))]
334pub struct TeamStatsResponse {
335 pub team_id: String,
336 pub time_range: String,
337 pub totals: TeamStatsTotals,
338 pub by_user: Vec<UserStats>,
339 pub by_tool: Vec<ToolStats>,
340}
341
342#[derive(Debug, Serialize)]
344#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
345#[cfg_attr(feature = "ts", ts(export))]
346pub struct TeamStatsTotals {
347 pub session_count: i64,
348 pub message_count: i64,
349 pub event_count: i64,
350 pub tool_call_count: i64,
351 pub duration_seconds: i64,
352 pub total_input_tokens: i64,
353 pub total_output_tokens: i64,
354}
355
356#[derive(Debug, Serialize)]
358#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
359#[cfg_attr(feature = "ts", ts(export))]
360pub struct UserStats {
361 pub user_id: String,
362 pub nickname: String,
363 pub session_count: i64,
364 pub message_count: i64,
365 pub event_count: i64,
366 pub duration_seconds: i64,
367 pub total_input_tokens: i64,
368 pub total_output_tokens: i64,
369}
370
371#[derive(Debug, Serialize)]
373#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
374#[cfg_attr(feature = "ts", ts(export))]
375pub struct ToolStats {
376 pub tool: String,
377 pub session_count: i64,
378 pub message_count: i64,
379 pub event_count: i64,
380 pub duration_seconds: i64,
381 pub total_input_tokens: i64,
382 pub total_output_tokens: i64,
383}
384
385impl From<opensession_core::stats::SessionAggregate> for TeamStatsTotals {
388 fn from(a: opensession_core::stats::SessionAggregate) -> Self {
389 Self {
390 session_count: a.session_count as i64,
391 message_count: a.message_count as i64,
392 event_count: a.event_count as i64,
393 tool_call_count: a.tool_call_count as i64,
394 duration_seconds: a.duration_seconds as i64,
395 total_input_tokens: a.total_input_tokens as i64,
396 total_output_tokens: a.total_output_tokens as i64,
397 }
398 }
399}
400
401impl From<(String, opensession_core::stats::SessionAggregate)> for ToolStats {
402 fn from((tool, a): (String, opensession_core::stats::SessionAggregate)) -> Self {
403 Self {
404 tool,
405 session_count: a.session_count as i64,
406 message_count: a.message_count as i64,
407 event_count: a.event_count as i64,
408 duration_seconds: a.duration_seconds as i64,
409 total_input_tokens: a.total_input_tokens as i64,
410 total_output_tokens: a.total_output_tokens as i64,
411 }
412 }
413}
414
415#[derive(Debug, Deserialize)]
419#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
420#[cfg_attr(feature = "ts", ts(export))]
421pub struct InviteRequest {
422 pub email: Option<String>,
423 pub oauth_provider: Option<String>,
425 pub oauth_provider_username: Option<String>,
427 pub role: Option<String>,
428}
429
430#[derive(Debug, Serialize)]
432#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
433#[cfg_attr(feature = "ts", ts(export))]
434pub struct InvitationResponse {
435 pub id: String,
436 pub team_id: String,
437 pub team_name: String,
438 pub email: Option<String>,
439 pub oauth_provider: Option<String>,
440 pub oauth_provider_username: Option<String>,
441 pub invited_by_nickname: String,
442 pub role: String,
443 pub status: String,
444 pub created_at: String,
445}
446
447#[derive(Debug, Serialize)]
449#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
450#[cfg_attr(feature = "ts", ts(export))]
451pub struct ListInvitationsResponse {
452 pub invitations: Vec<InvitationResponse>,
453}
454
455#[derive(Debug, Serialize)]
457#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
458#[cfg_attr(feature = "ts", ts(export))]
459pub struct AcceptInvitationResponse {
460 pub team_id: String,
461 pub role: String,
462}
463
464#[derive(Debug, Serialize, Deserialize)]
468#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
469#[cfg_attr(feature = "ts", ts(export))]
470pub struct AddMemberRequest {
471 pub nickname: String,
472}
473
474#[derive(Debug, Serialize, Deserialize)]
476#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
477#[cfg_attr(feature = "ts", ts(export))]
478pub struct MemberResponse {
479 pub user_id: String,
480 pub nickname: String,
481 pub role: String,
482 pub joined_at: String,
483}
484
485#[derive(Debug, Serialize, Deserialize)]
487#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
488#[cfg_attr(feature = "ts", ts(export))]
489pub struct ListMembersResponse {
490 pub members: Vec<MemberResponse>,
491}
492
493#[derive(Debug, Serialize, Deserialize)]
497#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
498#[cfg_attr(feature = "ts", ts(export))]
499pub struct ConfigSyncResponse {
500 pub privacy: Option<SyncedPrivacyConfig>,
501 pub watchers: Option<SyncedWatcherConfig>,
502}
503
504#[derive(Debug, Serialize, Deserialize)]
506#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
507#[cfg_attr(feature = "ts", ts(export))]
508pub struct SyncedPrivacyConfig {
509 pub exclude_patterns: Option<Vec<String>>,
510 pub exclude_tools: Option<Vec<String>>,
511}
512
513#[derive(Debug, Serialize, Deserialize)]
515#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
516#[cfg_attr(feature = "ts", ts(export))]
517pub struct SyncedWatcherConfig {
518 pub claude_code: Option<bool>,
519 pub opencode: Option<bool>,
520 pub goose: Option<bool>,
521 pub aider: Option<bool>,
522 pub cursor: Option<bool>,
523}
524
525#[derive(Debug, Deserialize)]
529pub struct SyncPullQuery {
530 pub team_id: String,
531 pub since: Option<String>,
533 pub limit: Option<u32>,
535}
536
537#[derive(Debug, Serialize, Deserialize)]
539pub struct SyncPullResponse {
540 pub sessions: Vec<SessionSummary>,
541 pub next_cursor: Option<String>,
543 pub has_more: bool,
544}
545
546#[derive(Debug, Deserialize)]
550#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
551#[cfg_attr(feature = "ts", ts(export))]
552pub struct StreamEventsRequest {
553 #[cfg_attr(feature = "ts", ts(type = "any"))]
554 pub agent: Option<serde_json::Value>,
555 #[cfg_attr(feature = "ts", ts(type = "any"))]
556 pub context: Option<serde_json::Value>,
557 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
558 pub events: Vec<serde_json::Value>,
559}
560
561#[derive(Debug, Serialize, Deserialize)]
563#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
564#[cfg_attr(feature = "ts", ts(export))]
565pub struct StreamEventsResponse {
566 pub accepted: usize,
567}
568
569#[derive(Debug, Serialize, Deserialize)]
573#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
574#[cfg_attr(feature = "ts", ts(export))]
575pub struct HealthResponse {
576 pub status: String,
577 pub version: String,
578}
579
580#[derive(Debug, Clone)]
587#[non_exhaustive]
588pub enum ServiceError {
589 BadRequest(String),
590 Unauthorized(String),
591 Forbidden(String),
592 NotFound(String),
593 Conflict(String),
594 Internal(String),
595}
596
597impl ServiceError {
598 pub fn status_code(&self) -> u16 {
600 match self {
601 Self::BadRequest(_) => 400,
602 Self::Unauthorized(_) => 401,
603 Self::Forbidden(_) => 403,
604 Self::NotFound(_) => 404,
605 Self::Conflict(_) => 409,
606 Self::Internal(_) => 500,
607 }
608 }
609
610 pub fn message(&self) -> &str {
612 match self {
613 Self::BadRequest(m)
614 | Self::Unauthorized(m)
615 | Self::Forbidden(m)
616 | Self::NotFound(m)
617 | Self::Conflict(m)
618 | Self::Internal(m) => m,
619 }
620 }
621
622 pub fn from_db<E: std::fmt::Display>(context: &str) -> impl FnOnce(E) -> Self + '_ {
624 move |e| Self::Internal(format!("{context}: {e}"))
625 }
626}
627
628impl std::fmt::Display for ServiceError {
629 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630 write!(f, "{}", self.message())
631 }
632}
633
634impl std::error::Error for ServiceError {}
635
636#[derive(Debug, Serialize)]
640#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
641#[cfg_attr(feature = "ts", ts(export))]
642pub struct ApiError {
643 pub error: String,
644}
645
646impl From<&ServiceError> for ApiError {
647 fn from(e: &ServiceError) -> Self {
648 Self {
649 error: e.message().to_string(),
650 }
651 }
652}
653
654#[cfg(all(test, feature = "ts"))]
657mod tests {
658 use super::*;
659 use std::io::Write;
660 use std::path::PathBuf;
661 use ts_rs::TS;
662
663 #[test]
665 fn export_typescript() {
666 let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
667 .join("../../packages/ui/src/api-types.generated.ts");
668
669 let cfg = ts_rs::Config::new().with_large_int("number");
670 let mut parts: Vec<String> = Vec::new();
671 parts.push("// AUTO-GENERATED by opensession-api-types — DO NOT EDIT".to_string());
672 parts.push(
673 "// Regenerate with: cargo test -p opensession-api-types -- export_typescript"
674 .to_string(),
675 );
676 parts.push(String::new());
677
678 macro_rules! collect_ts {
680 ($($t:ty),+ $(,)?) => {
681 $(
682 let decl = <$t>::decl(&cfg);
683 let decl = decl
685 .replacen("type ", "export interface ", 1)
686 .replace(" = {", " {")
687 .trim_end_matches(';')
688 .to_string();
689 parts.push(decl);
690 parts.push(String::new());
691 )+
692 };
693 }
694
695 collect_ts!(
696 RegisterRequest,
698 AuthRegisterRequest,
699 LoginRequest,
700 AuthTokenResponse,
701 RefreshRequest,
702 LogoutRequest,
703 ChangePasswordRequest,
704 RegisterResponse,
705 VerifyResponse,
706 UserSettingsResponse,
707 OkResponse,
708 RegenerateKeyResponse,
709 OAuthLinkResponse,
710 UploadResponse,
712 SessionSummary,
713 SessionListResponse,
714 SessionListQuery,
715 SessionDetail,
716 SessionLink,
717 CreateTeamRequest,
719 TeamResponse,
720 ListTeamsResponse,
721 TeamDetailResponse,
722 UpdateTeamRequest,
723 TeamStatsQuery,
724 TeamStatsResponse,
725 TeamStatsTotals,
726 UserStats,
727 ToolStats,
728 AddMemberRequest,
730 MemberResponse,
731 ListMembersResponse,
732 InviteRequest,
734 InvitationResponse,
735 ListInvitationsResponse,
736 AcceptInvitationResponse,
737 oauth::AuthProvidersResponse,
739 oauth::OAuthProviderInfo,
740 oauth::LinkedProvider,
741 HealthResponse,
743 ApiError,
744 );
745
746 let content = parts.join("\n");
747
748 if let Some(parent) = out_dir.parent() {
750 std::fs::create_dir_all(parent).ok();
751 }
752 let mut file = std::fs::File::create(&out_dir)
753 .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_dir.display(), e));
754 file.write_all(content.as_bytes())
755 .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_dir.display(), e));
756
757 println!("Generated TypeScript types at: {}", out_dir.display());
758 }
759}