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}
99
100#[derive(Debug, Serialize, Deserialize)]
102#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
103#[cfg_attr(feature = "ts", ts(export))]
104pub struct VerifyResponse {
105 pub user_id: String,
106 pub nickname: String,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
111#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
112#[cfg_attr(feature = "ts", ts(export))]
113pub struct UserSettingsResponse {
114 pub user_id: String,
115 pub nickname: String,
116 pub api_key: String,
117 pub created_at: String,
118 pub email: Option<String>,
119 pub avatar_url: Option<String>,
120 #[serde(default)]
122 pub oauth_providers: Vec<oauth::LinkedProvider>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub github_username: Option<String>,
126}
127
128#[derive(Debug, Serialize, Deserialize)]
130#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
131#[cfg_attr(feature = "ts", ts(export))]
132pub struct OkResponse {
133 pub ok: bool,
134}
135
136#[derive(Debug, Serialize, Deserialize)]
138#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
139#[cfg_attr(feature = "ts", ts(export))]
140pub struct RegenerateKeyResponse {
141 pub api_key: String,
142}
143
144#[derive(Debug, Serialize)]
146#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
147#[cfg_attr(feature = "ts", ts(export))]
148pub struct OAuthLinkResponse {
149 pub url: String,
150}
151
152#[derive(Debug, Default, Serialize, Deserialize)]
156#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
157#[cfg_attr(feature = "ts", ts(export))]
158pub struct UploadRequest {
159 #[cfg_attr(feature = "ts", ts(type = "any"))]
160 pub session: serde_json::Value, pub team_id: Option<String>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub body_url: Option<String>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub linked_session_ids: Option<Vec<String>>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub git_remote: Option<String>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub git_branch: Option<String>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub git_commit: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub git_repo_name: Option<String>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub pr_number: Option<i64>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub pr_url: Option<String>,
178}
179
180#[derive(Debug, Serialize, Deserialize)]
182#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
183#[cfg_attr(feature = "ts", ts(export))]
184pub struct UploadResponse {
185 pub id: String,
186 pub url: String,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
192#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
193#[cfg_attr(feature = "ts", ts(export))]
194pub struct SessionSummary {
195 pub id: String,
196 pub user_id: Option<String>,
197 pub nickname: Option<String>,
198 pub team_id: String,
199 pub tool: String,
200 pub agent_provider: Option<String>,
201 pub agent_model: Option<String>,
202 pub title: Option<String>,
203 pub description: Option<String>,
204 pub tags: Option<String>,
206 pub created_at: String,
207 pub uploaded_at: String,
208 pub message_count: i64,
209 pub task_count: i64,
210 pub event_count: i64,
211 pub duration_seconds: i64,
212 pub total_input_tokens: i64,
213 pub total_output_tokens: i64,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub git_remote: Option<String>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub git_branch: Option<String>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub git_commit: Option<String>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub git_repo_name: Option<String>,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub pr_number: Option<i64>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub pr_url: Option<String>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub working_directory: Option<String>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub files_modified: Option<String>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub files_read: Option<String>,
232 #[serde(default)]
233 pub has_errors: bool,
234}
235
236#[derive(Debug, Serialize, Deserialize)]
238#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
239#[cfg_attr(feature = "ts", ts(export))]
240pub struct SessionListResponse {
241 pub sessions: Vec<SessionSummary>,
242 pub total: i64,
243 pub page: u32,
244 pub per_page: u32,
245}
246
247#[derive(Debug, Deserialize)]
249#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
250#[cfg_attr(feature = "ts", ts(export))]
251pub struct SessionListQuery {
252 #[serde(default = "default_page")]
253 pub page: u32,
254 #[serde(default = "default_per_page")]
255 pub per_page: u32,
256 pub search: Option<String>,
257 pub tool: Option<String>,
258 pub team_id: Option<String>,
259 pub sort: Option<String>,
261 pub time_range: Option<String>,
263}
264
265fn default_page() -> u32 {
266 1
267}
268fn default_per_page() -> u32 {
269 20
270}
271
272#[derive(Debug, Serialize, Deserialize)]
274#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
275#[cfg_attr(feature = "ts", ts(export))]
276pub struct SessionDetail {
277 #[serde(flatten)]
278 #[cfg_attr(feature = "ts", ts(flatten))]
279 pub summary: SessionSummary,
280 pub team_name: Option<String>,
281 #[serde(default, skip_serializing_if = "Vec::is_empty")]
282 pub linked_sessions: Vec<SessionLink>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
288#[cfg_attr(feature = "ts", ts(export))]
289pub struct SessionLink {
290 pub session_id: String,
291 pub linked_session_id: String,
292 pub link_type: String,
293 pub created_at: String,
294}
295
296#[derive(Debug, Serialize, Deserialize)]
300#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
301#[cfg_attr(feature = "ts", ts(export))]
302pub struct CreateTeamRequest {
303 pub name: String,
304 pub description: Option<String>,
305 pub is_public: Option<bool>,
306}
307
308#[derive(Debug, Serialize, Deserialize)]
310#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
311#[cfg_attr(feature = "ts", ts(export))]
312pub struct TeamResponse {
313 pub id: String,
314 pub name: String,
315 pub description: Option<String>,
316 pub is_public: bool,
317 pub created_by: String,
318 pub created_at: String,
319}
320
321#[derive(Debug, Serialize, Deserialize)]
323#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
324#[cfg_attr(feature = "ts", ts(export))]
325pub struct ListTeamsResponse {
326 pub teams: Vec<TeamResponse>,
327}
328
329#[derive(Debug, Serialize, Deserialize)]
331#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
332#[cfg_attr(feature = "ts", ts(export))]
333pub struct TeamDetailResponse {
334 #[serde(flatten)]
335 #[cfg_attr(feature = "ts", ts(flatten))]
336 pub team: TeamResponse,
337 pub member_count: i64,
338 pub sessions: Vec<SessionSummary>,
339}
340
341#[derive(Debug, Serialize, Deserialize)]
343#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
344#[cfg_attr(feature = "ts", ts(export))]
345pub struct UpdateTeamRequest {
346 pub name: Option<String>,
347 pub description: Option<String>,
348 pub is_public: Option<bool>,
349}
350
351#[derive(Debug, Deserialize)]
353#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
354#[cfg_attr(feature = "ts", ts(export))]
355pub struct TeamStatsQuery {
356 pub time_range: Option<String>,
358}
359
360#[derive(Debug, Serialize)]
362#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
363#[cfg_attr(feature = "ts", ts(export))]
364pub struct TeamStatsResponse {
365 pub team_id: String,
366 pub time_range: String,
367 pub totals: TeamStatsTotals,
368 pub by_user: Vec<UserStats>,
369 pub by_tool: Vec<ToolStats>,
370}
371
372#[derive(Debug, Serialize)]
374#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
375#[cfg_attr(feature = "ts", ts(export))]
376pub struct TeamStatsTotals {
377 pub session_count: i64,
378 pub message_count: i64,
379 pub event_count: i64,
380 pub tool_call_count: i64,
381 pub duration_seconds: i64,
382 pub total_input_tokens: i64,
383 pub total_output_tokens: i64,
384}
385
386#[derive(Debug, Serialize)]
388#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
389#[cfg_attr(feature = "ts", ts(export))]
390pub struct UserStats {
391 pub user_id: String,
392 pub nickname: String,
393 pub session_count: i64,
394 pub message_count: i64,
395 pub event_count: i64,
396 pub duration_seconds: i64,
397 pub total_input_tokens: i64,
398 pub total_output_tokens: i64,
399}
400
401#[derive(Debug, Serialize)]
403#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
404#[cfg_attr(feature = "ts", ts(export))]
405pub struct ToolStats {
406 pub tool: String,
407 pub session_count: i64,
408 pub message_count: i64,
409 pub event_count: i64,
410 pub duration_seconds: i64,
411 pub total_input_tokens: i64,
412 pub total_output_tokens: i64,
413}
414
415impl From<opensession_core::stats::SessionAggregate> for TeamStatsTotals {
418 fn from(a: opensession_core::stats::SessionAggregate) -> Self {
419 Self {
420 session_count: a.session_count as i64,
421 message_count: a.message_count as i64,
422 event_count: a.event_count as i64,
423 tool_call_count: a.tool_call_count as i64,
424 duration_seconds: a.duration_seconds as i64,
425 total_input_tokens: a.total_input_tokens as i64,
426 total_output_tokens: a.total_output_tokens as i64,
427 }
428 }
429}
430
431impl From<(String, opensession_core::stats::SessionAggregate)> for ToolStats {
432 fn from((tool, a): (String, opensession_core::stats::SessionAggregate)) -> Self {
433 Self {
434 tool,
435 session_count: a.session_count as i64,
436 message_count: a.message_count as i64,
437 event_count: a.event_count as i64,
438 duration_seconds: a.duration_seconds as i64,
439 total_input_tokens: a.total_input_tokens as i64,
440 total_output_tokens: a.total_output_tokens as i64,
441 }
442 }
443}
444
445#[derive(Debug, Serialize, Deserialize)]
449#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
450#[cfg_attr(feature = "ts", ts(export))]
451pub struct InviteRequest {
452 pub email: Option<String>,
453 pub oauth_provider: Option<String>,
455 pub oauth_provider_username: Option<String>,
457 pub role: Option<String>,
458}
459
460#[derive(Debug, Serialize, Deserialize)]
462#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
463#[cfg_attr(feature = "ts", ts(export))]
464pub struct InvitationResponse {
465 pub id: String,
466 pub team_id: String,
467 pub team_name: String,
468 pub email: Option<String>,
469 pub oauth_provider: Option<String>,
470 pub oauth_provider_username: Option<String>,
471 pub invited_by_nickname: String,
472 pub role: String,
473 pub status: String,
474 pub created_at: String,
475}
476
477#[derive(Debug, Serialize, Deserialize)]
479#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
480#[cfg_attr(feature = "ts", ts(export))]
481pub struct ListInvitationsResponse {
482 pub invitations: Vec<InvitationResponse>,
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 AcceptInvitationResponse {
490 pub team_id: String,
491 pub role: String,
492}
493
494#[derive(Debug, Serialize, Deserialize)]
498#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
499#[cfg_attr(feature = "ts", ts(export))]
500pub struct AddMemberRequest {
501 pub nickname: String,
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 MemberResponse {
509 pub user_id: String,
510 pub nickname: String,
511 pub role: String,
512 pub joined_at: String,
513}
514
515#[derive(Debug, Serialize, Deserialize)]
517#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
518#[cfg_attr(feature = "ts", ts(export))]
519pub struct ListMembersResponse {
520 pub members: Vec<MemberResponse>,
521}
522
523#[derive(Debug, Serialize, Deserialize)]
527#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
528#[cfg_attr(feature = "ts", ts(export))]
529pub struct ConfigSyncResponse {
530 pub privacy: Option<SyncedPrivacyConfig>,
531 pub watchers: Option<SyncedWatcherConfig>,
532}
533
534#[derive(Debug, Serialize, Deserialize)]
536#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
537#[cfg_attr(feature = "ts", ts(export))]
538pub struct SyncedPrivacyConfig {
539 pub exclude_patterns: Option<Vec<String>>,
540 pub exclude_tools: Option<Vec<String>>,
541}
542
543#[derive(Debug, Serialize, Deserialize)]
545#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
546#[cfg_attr(feature = "ts", ts(export))]
547pub struct SyncedWatcherConfig {
548 pub claude_code: Option<bool>,
549 pub opencode: Option<bool>,
550 pub goose: Option<bool>,
551 pub aider: Option<bool>,
552 pub cursor: Option<bool>,
553}
554
555#[derive(Debug, Deserialize)]
559pub struct SyncPullQuery {
560 pub team_id: String,
561 pub since: Option<String>,
563 pub limit: Option<u32>,
565}
566
567#[derive(Debug, Serialize, Deserialize)]
569pub struct SyncPullResponse {
570 pub sessions: Vec<SessionSummary>,
571 pub next_cursor: Option<String>,
573 pub has_more: bool,
574}
575
576#[derive(Debug, Deserialize)]
580#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
581#[cfg_attr(feature = "ts", ts(export))]
582pub struct StreamEventsRequest {
583 #[cfg_attr(feature = "ts", ts(type = "any"))]
584 pub agent: Option<serde_json::Value>,
585 #[cfg_attr(feature = "ts", ts(type = "any"))]
586 pub context: Option<serde_json::Value>,
587 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
588 pub events: Vec<serde_json::Value>,
589}
590
591#[derive(Debug, Serialize, Deserialize)]
593#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
594#[cfg_attr(feature = "ts", ts(export))]
595pub struct StreamEventsResponse {
596 pub accepted: usize,
597}
598
599#[derive(Debug, Serialize, Deserialize)]
603#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
604#[cfg_attr(feature = "ts", ts(export))]
605pub struct HealthResponse {
606 pub status: String,
607 pub version: String,
608}
609
610#[derive(Debug, Clone)]
617#[non_exhaustive]
618pub enum ServiceError {
619 BadRequest(String),
620 Unauthorized(String),
621 Forbidden(String),
622 NotFound(String),
623 Conflict(String),
624 Internal(String),
625}
626
627impl ServiceError {
628 pub fn status_code(&self) -> u16 {
630 match self {
631 Self::BadRequest(_) => 400,
632 Self::Unauthorized(_) => 401,
633 Self::Forbidden(_) => 403,
634 Self::NotFound(_) => 404,
635 Self::Conflict(_) => 409,
636 Self::Internal(_) => 500,
637 }
638 }
639
640 pub fn message(&self) -> &str {
642 match self {
643 Self::BadRequest(m)
644 | Self::Unauthorized(m)
645 | Self::Forbidden(m)
646 | Self::NotFound(m)
647 | Self::Conflict(m)
648 | Self::Internal(m) => m,
649 }
650 }
651
652 pub fn from_db<E: std::fmt::Display>(context: &str) -> impl FnOnce(E) -> Self + '_ {
654 move |e| Self::Internal(format!("{context}: {e}"))
655 }
656}
657
658impl std::fmt::Display for ServiceError {
659 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
660 write!(f, "{}", self.message())
661 }
662}
663
664impl std::error::Error for ServiceError {}
665
666#[derive(Debug, Serialize)]
670#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
671#[cfg_attr(feature = "ts", ts(export))]
672pub struct ApiError {
673 pub error: String,
674}
675
676impl From<&ServiceError> for ApiError {
677 fn from(e: &ServiceError) -> Self {
678 Self {
679 error: e.message().to_string(),
680 }
681 }
682}
683
684#[cfg(all(test, feature = "ts"))]
687mod tests {
688 use super::*;
689 use std::io::Write;
690 use std::path::PathBuf;
691 use ts_rs::TS;
692
693 #[test]
695 fn export_typescript() {
696 let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
697 .join("../../packages/ui/src/api-types.generated.ts");
698
699 let cfg = ts_rs::Config::new().with_large_int("number");
700 let mut parts: Vec<String> = Vec::new();
701 parts.push("// AUTO-GENERATED by opensession-api-types — DO NOT EDIT".to_string());
702 parts.push(
703 "// Regenerate with: cargo test -p opensession-api-types -- export_typescript"
704 .to_string(),
705 );
706 parts.push(String::new());
707
708 macro_rules! collect_ts {
710 ($($t:ty),+ $(,)?) => {
711 $(
712 let decl = <$t>::decl(&cfg);
713 let decl = decl
715 .replacen("type ", "export interface ", 1)
716 .replace(" = {", " {")
717 .trim_end_matches(';')
718 .to_string();
719 parts.push(decl);
720 parts.push(String::new());
721 )+
722 };
723 }
724
725 collect_ts!(
726 RegisterRequest,
728 AuthRegisterRequest,
729 LoginRequest,
730 AuthTokenResponse,
731 RefreshRequest,
732 LogoutRequest,
733 ChangePasswordRequest,
734 RegisterResponse,
735 VerifyResponse,
736 UserSettingsResponse,
737 OkResponse,
738 RegenerateKeyResponse,
739 OAuthLinkResponse,
740 UploadResponse,
742 SessionSummary,
743 SessionListResponse,
744 SessionListQuery,
745 SessionDetail,
746 SessionLink,
747 CreateTeamRequest,
749 TeamResponse,
750 ListTeamsResponse,
751 TeamDetailResponse,
752 UpdateTeamRequest,
753 TeamStatsQuery,
754 TeamStatsResponse,
755 TeamStatsTotals,
756 UserStats,
757 ToolStats,
758 AddMemberRequest,
760 MemberResponse,
761 ListMembersResponse,
762 InviteRequest,
764 InvitationResponse,
765 ListInvitationsResponse,
766 AcceptInvitationResponse,
767 oauth::AuthProvidersResponse,
769 oauth::OAuthProviderInfo,
770 oauth::LinkedProvider,
771 HealthResponse,
773 ApiError,
774 );
775
776 let content = parts.join("\n");
777
778 if let Some(parent) = out_dir.parent() {
780 std::fs::create_dir_all(parent).ok();
781 }
782 let mut file = std::fs::File::create(&out_dir)
783 .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_dir.display(), e));
784 file.write_all(content.as_bytes())
785 .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_dir.display(), e));
786
787 println!("Generated TypeScript types at: {}", out_dir.display());
788 }
789}