1use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "backend")]
12pub mod crypto;
13#[cfg(feature = "backend")]
14pub mod db;
15pub mod deploy;
16pub mod oauth;
17#[cfg(feature = "backend")]
18pub mod service;
19
20pub use opensession_core::trace::{
22 Agent, Content, ContentBlock, Event, EventType, Session, SessionContext, Stats,
23};
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(rename_all = "snake_case")]
30#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
31#[cfg_attr(feature = "ts", ts(export))]
32pub enum TeamRole {
33 Admin,
34 Member,
35}
36
37impl TeamRole {
38 pub fn as_str(&self) -> &str {
39 match self {
40 Self::Admin => "admin",
41 Self::Member => "member",
42 }
43 }
44}
45
46impl std::fmt::Display for TeamRole {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.write_str(self.as_str())
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
56#[cfg_attr(feature = "ts", ts(export))]
57pub enum InvitationStatus {
58 Pending,
59 Accepted,
60 Declined,
61}
62
63impl InvitationStatus {
64 pub fn as_str(&self) -> &str {
65 match self {
66 Self::Pending => "pending",
67 Self::Accepted => "accepted",
68 Self::Declined => "declined",
69 }
70 }
71}
72
73impl std::fmt::Display for InvitationStatus {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.write_str(self.as_str())
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
81#[serde(rename_all = "snake_case")]
82#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
83#[cfg_attr(feature = "ts", ts(export))]
84pub enum SortOrder {
85 #[default]
86 Recent,
87 Popular,
88 Longest,
89}
90
91impl SortOrder {
92 pub fn as_str(&self) -> &str {
93 match self {
94 Self::Recent => "recent",
95 Self::Popular => "popular",
96 Self::Longest => "longest",
97 }
98 }
99}
100
101impl std::fmt::Display for SortOrder {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 f.write_str(self.as_str())
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
109#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
110#[cfg_attr(feature = "ts", ts(export))]
111pub enum TimeRange {
112 #[serde(rename = "24h")]
113 Hours24,
114 #[serde(rename = "7d")]
115 Days7,
116 #[serde(rename = "30d")]
117 Days30,
118 #[default]
119 #[serde(rename = "all")]
120 All,
121}
122
123impl TimeRange {
124 pub fn as_str(&self) -> &str {
125 match self {
126 Self::Hours24 => "24h",
127 Self::Days7 => "7d",
128 Self::Days30 => "30d",
129 Self::All => "all",
130 }
131 }
132}
133
134impl std::fmt::Display for TimeRange {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.write_str(self.as_str())
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
142#[serde(rename_all = "snake_case")]
143#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
144#[cfg_attr(feature = "ts", ts(export))]
145pub enum LinkType {
146 Handoff,
147 Related,
148 Parent,
149 Child,
150}
151
152impl LinkType {
153 pub fn as_str(&self) -> &str {
154 match self {
155 Self::Handoff => "handoff",
156 Self::Related => "related",
157 Self::Parent => "parent",
158 Self::Child => "child",
159 }
160 }
161}
162
163impl std::fmt::Display for LinkType {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.write_str(self.as_str())
166 }
167}
168
169pub fn saturating_i64(v: u64) -> i64 {
173 i64::try_from(v).unwrap_or(i64::MAX)
174}
175
176#[derive(Debug, Deserialize)]
180#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
181#[cfg_attr(feature = "ts", ts(export))]
182pub struct RegisterRequest {
183 pub nickname: String,
184}
185
186#[derive(Debug, Serialize, Deserialize)]
188#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
189#[cfg_attr(feature = "ts", ts(export))]
190pub struct AuthRegisterRequest {
191 pub email: String,
192 pub password: String,
193 pub nickname: String,
194}
195
196#[derive(Debug, Serialize, Deserialize)]
198#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
199#[cfg_attr(feature = "ts", ts(export))]
200pub struct LoginRequest {
201 pub email: String,
202 pub password: String,
203}
204
205#[derive(Debug, Serialize, Deserialize)]
207#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
208#[cfg_attr(feature = "ts", ts(export))]
209pub struct AuthTokenResponse {
210 pub access_token: String,
211 pub refresh_token: String,
212 pub expires_in: u64,
213 pub user_id: String,
214 pub nickname: String,
215}
216
217#[derive(Debug, Serialize, Deserialize)]
219#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
220#[cfg_attr(feature = "ts", ts(export))]
221pub struct RefreshRequest {
222 pub refresh_token: String,
223}
224
225#[derive(Debug, Serialize, Deserialize)]
227#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
228#[cfg_attr(feature = "ts", ts(export))]
229pub struct LogoutRequest {
230 pub refresh_token: String,
231}
232
233#[derive(Debug, Serialize, Deserialize)]
235#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
236#[cfg_attr(feature = "ts", ts(export))]
237pub struct ChangePasswordRequest {
238 pub current_password: String,
239 pub new_password: String,
240}
241
242#[derive(Debug, Serialize)]
244#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
245#[cfg_attr(feature = "ts", ts(export))]
246pub struct RegisterResponse {
247 pub user_id: String,
248 pub nickname: String,
249 pub api_key: String,
250}
251
252#[derive(Debug, Serialize, Deserialize)]
254#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
255#[cfg_attr(feature = "ts", ts(export))]
256pub struct VerifyResponse {
257 pub user_id: String,
258 pub nickname: String,
259}
260
261#[derive(Debug, Serialize, Deserialize)]
263#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
264#[cfg_attr(feature = "ts", ts(export))]
265pub struct UserSettingsResponse {
266 pub user_id: String,
267 pub nickname: String,
268 pub api_key: String,
269 pub created_at: String,
270 pub email: Option<String>,
271 pub avatar_url: Option<String>,
272 #[serde(default)]
274 pub oauth_providers: Vec<oauth::LinkedProvider>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub github_username: Option<String>,
278}
279
280#[derive(Debug, Serialize, Deserialize)]
282#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
283#[cfg_attr(feature = "ts", ts(export))]
284pub struct OkResponse {
285 pub ok: bool,
286}
287
288#[derive(Debug, Serialize, Deserialize)]
290#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
291#[cfg_attr(feature = "ts", ts(export))]
292pub struct RegenerateKeyResponse {
293 pub api_key: String,
294}
295
296#[derive(Debug, Serialize)]
298#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
299#[cfg_attr(feature = "ts", ts(export))]
300pub struct OAuthLinkResponse {
301 pub url: String,
302}
303
304#[derive(Debug, Serialize, Deserialize)]
308pub struct UploadRequest {
309 pub session: Session,
310 pub team_id: Option<String>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub body_url: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub linked_session_ids: Option<Vec<String>>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub git_remote: Option<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub git_branch: Option<String>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub git_commit: Option<String>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub git_repo_name: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub pr_number: Option<i64>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub pr_url: Option<String>,
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 UploadResponse {
334 pub id: String,
335 pub url: String,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
341#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
342#[cfg_attr(feature = "ts", ts(export))]
343pub struct SessionSummary {
344 pub id: String,
345 pub user_id: Option<String>,
346 pub nickname: Option<String>,
347 pub team_id: String,
348 pub tool: String,
349 pub agent_provider: Option<String>,
350 pub agent_model: Option<String>,
351 pub title: Option<String>,
352 pub description: Option<String>,
353 pub tags: Option<String>,
355 pub created_at: String,
356 pub uploaded_at: String,
357 pub message_count: i64,
358 pub task_count: i64,
359 pub event_count: i64,
360 pub duration_seconds: i64,
361 pub total_input_tokens: i64,
362 pub total_output_tokens: i64,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub git_remote: Option<String>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub git_branch: Option<String>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub git_commit: Option<String>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub git_repo_name: Option<String>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub pr_number: Option<i64>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub pr_url: Option<String>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub working_directory: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub files_modified: Option<String>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub files_read: Option<String>,
381 #[serde(default)]
382 pub has_errors: bool,
383 #[serde(default = "default_max_active_agents")]
384 pub max_active_agents: i64,
385}
386
387#[derive(Debug, Serialize, Deserialize)]
389#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
390#[cfg_attr(feature = "ts", ts(export))]
391pub struct SessionListResponse {
392 pub sessions: Vec<SessionSummary>,
393 pub total: i64,
394 pub page: u32,
395 pub per_page: u32,
396}
397
398#[derive(Debug, Deserialize)]
400#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
401#[cfg_attr(feature = "ts", ts(export))]
402pub struct SessionListQuery {
403 #[serde(default = "default_page")]
404 pub page: u32,
405 #[serde(default = "default_per_page")]
406 pub per_page: u32,
407 pub search: Option<String>,
408 pub tool: Option<String>,
409 pub team_id: Option<String>,
410 pub sort: Option<SortOrder>,
412 pub time_range: Option<TimeRange>,
414}
415
416impl SessionListQuery {
417 pub fn is_public_feed_cacheable(
419 &self,
420 has_auth_header: bool,
421 has_session_cookie: bool,
422 ) -> bool {
423 !has_auth_header
424 && !has_session_cookie
425 && self.team_id.is_none()
426 && self.search.as_deref().is_none_or(|s| s.trim().is_empty())
427 && self.page <= 10
428 && self.per_page <= 50
429 }
430}
431
432#[cfg(test)]
433mod session_list_query_tests {
434 use super::*;
435
436 fn base_query() -> SessionListQuery {
437 SessionListQuery {
438 page: 1,
439 per_page: 20,
440 search: None,
441 tool: None,
442 team_id: None,
443 sort: None,
444 time_range: None,
445 }
446 }
447
448 #[test]
449 fn public_feed_cacheable_when_anonymous_default_feed() {
450 let q = base_query();
451 assert!(q.is_public_feed_cacheable(false, false));
452 }
453
454 #[test]
455 fn public_feed_not_cacheable_with_auth_or_cookie() {
456 let q = base_query();
457 assert!(!q.is_public_feed_cacheable(true, false));
458 assert!(!q.is_public_feed_cacheable(false, true));
459 }
460
461 #[test]
462 fn public_feed_not_cacheable_for_team_or_search_or_large_page() {
463 let mut q = base_query();
464 q.team_id = Some("team-1".into());
465 assert!(!q.is_public_feed_cacheable(false, false));
466
467 let mut q = base_query();
468 q.search = Some("hello".into());
469 assert!(!q.is_public_feed_cacheable(false, false));
470
471 let mut q = base_query();
472 q.page = 11;
473 assert!(!q.is_public_feed_cacheable(false, false));
474
475 let mut q = base_query();
476 q.per_page = 100;
477 assert!(!q.is_public_feed_cacheable(false, false));
478 }
479}
480
481fn default_page() -> u32 {
482 1
483}
484fn default_per_page() -> u32 {
485 20
486}
487fn default_max_active_agents() -> i64 {
488 1
489}
490
491#[derive(Debug, Serialize, Deserialize)]
493#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
494#[cfg_attr(feature = "ts", ts(export))]
495pub struct SessionDetail {
496 #[serde(flatten)]
497 #[cfg_attr(feature = "ts", ts(flatten))]
498 pub summary: SessionSummary,
499 pub team_name: Option<String>,
500 #[serde(default, skip_serializing_if = "Vec::is_empty")]
501 pub linked_sessions: Vec<SessionLink>,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
506#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
507#[cfg_attr(feature = "ts", ts(export))]
508pub struct SessionLink {
509 pub session_id: String,
510 pub linked_session_id: String,
511 pub link_type: LinkType,
512 pub created_at: String,
513}
514
515#[derive(Debug, Serialize, Deserialize)]
519#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
520#[cfg_attr(feature = "ts", ts(export))]
521pub struct CreateTeamRequest {
522 pub name: String,
523 pub description: Option<String>,
524 pub is_public: Option<bool>,
525}
526
527#[derive(Debug, Serialize, Deserialize)]
529#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
530#[cfg_attr(feature = "ts", ts(export))]
531pub struct TeamResponse {
532 pub id: String,
533 pub name: String,
534 pub description: Option<String>,
535 pub is_public: bool,
536 pub created_by: String,
537 pub created_at: String,
538}
539
540#[derive(Debug, Serialize, Deserialize)]
542#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
543#[cfg_attr(feature = "ts", ts(export))]
544pub struct ListTeamsResponse {
545 pub teams: Vec<TeamResponse>,
546}
547
548#[derive(Debug, Serialize, Deserialize)]
550#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
551#[cfg_attr(feature = "ts", ts(export))]
552pub struct TeamDetailResponse {
553 #[serde(flatten)]
554 #[cfg_attr(feature = "ts", ts(flatten))]
555 pub team: TeamResponse,
556 pub member_count: i64,
557 pub sessions: Vec<SessionSummary>,
558}
559
560#[derive(Debug, Serialize, Deserialize)]
562#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
563#[cfg_attr(feature = "ts", ts(export))]
564pub struct UpdateTeamRequest {
565 pub name: Option<String>,
566 pub description: Option<String>,
567 pub is_public: Option<bool>,
568}
569
570#[derive(Debug, Deserialize)]
572#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
573#[cfg_attr(feature = "ts", ts(export))]
574pub struct TeamStatsQuery {
575 pub time_range: Option<TimeRange>,
577}
578
579#[derive(Debug, Serialize)]
581#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
582#[cfg_attr(feature = "ts", ts(export))]
583pub struct TeamStatsResponse {
584 pub team_id: String,
585 pub time_range: TimeRange,
586 pub totals: TeamStatsTotals,
587 pub by_user: Vec<UserStats>,
588 pub by_tool: Vec<ToolStats>,
589}
590
591#[derive(Debug, Serialize)]
593#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
594#[cfg_attr(feature = "ts", ts(export))]
595pub struct TeamStatsTotals {
596 pub session_count: i64,
597 pub message_count: i64,
598 pub event_count: i64,
599 pub tool_call_count: i64,
600 pub duration_seconds: i64,
601 pub total_input_tokens: i64,
602 pub total_output_tokens: i64,
603}
604
605#[derive(Debug, Serialize)]
607#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
608#[cfg_attr(feature = "ts", ts(export))]
609pub struct UserStats {
610 pub user_id: String,
611 pub nickname: String,
612 pub session_count: i64,
613 pub message_count: i64,
614 pub event_count: i64,
615 pub duration_seconds: i64,
616 pub total_input_tokens: i64,
617 pub total_output_tokens: i64,
618}
619
620#[derive(Debug, Serialize)]
622#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
623#[cfg_attr(feature = "ts", ts(export))]
624pub struct ToolStats {
625 pub tool: String,
626 pub session_count: i64,
627 pub message_count: i64,
628 pub event_count: i64,
629 pub duration_seconds: i64,
630 pub total_input_tokens: i64,
631 pub total_output_tokens: i64,
632}
633
634#[derive(Debug, Serialize, Deserialize)]
636#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
637#[cfg_attr(feature = "ts", ts(export))]
638pub struct CreateTeamInviteKeyRequest {
639 pub role: Option<TeamRole>,
640 pub expires_in_days: Option<u32>,
642}
643
644#[derive(Debug, Serialize, Deserialize)]
647#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
648#[cfg_attr(feature = "ts", ts(export))]
649pub struct CreateTeamInviteKeyResponse {
650 pub key_id: String,
651 pub invite_key: String,
652 pub role: TeamRole,
653 pub expires_at: String,
654}
655
656#[derive(Debug, Serialize, Deserialize)]
658#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
659#[cfg_attr(feature = "ts", ts(export))]
660pub struct TeamInviteKeySummary {
661 pub id: String,
662 pub role: TeamRole,
663 pub created_by_nickname: String,
664 pub created_at: String,
665 pub expires_at: String,
666 pub used_at: Option<String>,
667 pub revoked_at: Option<String>,
668}
669
670#[derive(Debug, Serialize, Deserialize)]
672#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
673#[cfg_attr(feature = "ts", ts(export))]
674pub struct ListTeamInviteKeysResponse {
675 pub keys: Vec<TeamInviteKeySummary>,
676}
677
678#[derive(Debug, Serialize, Deserialize)]
680#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
681#[cfg_attr(feature = "ts", ts(export))]
682pub struct JoinTeamWithKeyRequest {
683 pub invite_key: String,
684}
685
686#[derive(Debug, Serialize, Deserialize)]
688#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
689#[cfg_attr(feature = "ts", ts(export))]
690pub struct JoinTeamWithKeyResponse {
691 pub team_id: String,
692 pub team_name: String,
693 pub role: TeamRole,
694}
695
696impl From<opensession_core::stats::SessionAggregate> for TeamStatsTotals {
699 fn from(a: opensession_core::stats::SessionAggregate) -> Self {
700 Self {
701 session_count: saturating_i64(a.session_count),
702 message_count: saturating_i64(a.message_count),
703 event_count: saturating_i64(a.event_count),
704 tool_call_count: saturating_i64(a.tool_call_count),
705 duration_seconds: saturating_i64(a.duration_seconds),
706 total_input_tokens: saturating_i64(a.total_input_tokens),
707 total_output_tokens: saturating_i64(a.total_output_tokens),
708 }
709 }
710}
711
712impl From<(String, opensession_core::stats::SessionAggregate)> for ToolStats {
713 fn from((tool, a): (String, opensession_core::stats::SessionAggregate)) -> Self {
714 Self {
715 tool,
716 session_count: saturating_i64(a.session_count),
717 message_count: saturating_i64(a.message_count),
718 event_count: saturating_i64(a.event_count),
719 duration_seconds: saturating_i64(a.duration_seconds),
720 total_input_tokens: saturating_i64(a.total_input_tokens),
721 total_output_tokens: saturating_i64(a.total_output_tokens),
722 }
723 }
724}
725
726#[derive(Debug, Serialize, Deserialize)]
730#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
731#[cfg_attr(feature = "ts", ts(export))]
732pub struct InviteRequest {
733 pub email: Option<String>,
734 pub oauth_provider: Option<String>,
736 pub oauth_provider_username: Option<String>,
738 pub role: Option<TeamRole>,
739}
740
741#[derive(Debug, Serialize, Deserialize)]
743#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
744#[cfg_attr(feature = "ts", ts(export))]
745pub struct InvitationResponse {
746 pub id: String,
747 pub team_id: String,
748 pub team_name: String,
749 pub email: Option<String>,
750 pub oauth_provider: Option<String>,
751 pub oauth_provider_username: Option<String>,
752 pub invited_by_nickname: String,
753 pub role: TeamRole,
754 pub status: InvitationStatus,
755 pub created_at: String,
756}
757
758#[derive(Debug, Serialize, Deserialize)]
760#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
761#[cfg_attr(feature = "ts", ts(export))]
762pub struct ListInvitationsResponse {
763 pub invitations: Vec<InvitationResponse>,
764}
765
766#[derive(Debug, Serialize, Deserialize)]
768#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
769#[cfg_attr(feature = "ts", ts(export))]
770pub struct AcceptInvitationResponse {
771 pub team_id: String,
772 pub role: TeamRole,
773}
774
775#[derive(Debug, Serialize, Deserialize)]
779#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
780#[cfg_attr(feature = "ts", ts(export))]
781pub struct AddMemberRequest {
782 pub nickname: String,
783}
784
785#[derive(Debug, Serialize, Deserialize)]
787#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
788#[cfg_attr(feature = "ts", ts(export))]
789pub struct MemberResponse {
790 pub user_id: String,
791 pub nickname: String,
792 pub role: TeamRole,
793 pub joined_at: String,
794}
795
796#[derive(Debug, Serialize, Deserialize)]
798#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
799#[cfg_attr(feature = "ts", ts(export))]
800pub struct ListMembersResponse {
801 pub members: Vec<MemberResponse>,
802}
803
804#[derive(Debug, Serialize, Deserialize)]
808#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
809#[cfg_attr(feature = "ts", ts(export))]
810pub struct ConfigSyncResponse {
811 pub privacy: Option<SyncedPrivacyConfig>,
812 pub watchers: Option<SyncedWatcherConfig>,
813}
814
815#[derive(Debug, Serialize, Deserialize)]
817#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
818#[cfg_attr(feature = "ts", ts(export))]
819pub struct SyncedPrivacyConfig {
820 pub exclude_patterns: Option<Vec<String>>,
821 pub exclude_tools: Option<Vec<String>>,
822}
823
824#[derive(Debug, Serialize, Deserialize)]
826#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
827#[cfg_attr(feature = "ts", ts(export))]
828pub struct SyncedWatcherConfig {
829 pub claude_code: Option<bool>,
830 pub opencode: Option<bool>,
831 pub cursor: Option<bool>,
832}
833
834#[derive(Debug, Deserialize)]
838pub struct SyncPullQuery {
839 pub team_id: String,
840 pub since: Option<String>,
842 pub limit: Option<u32>,
844}
845
846#[derive(Debug, Serialize, Deserialize)]
848pub struct SyncPullResponse {
849 pub sessions: Vec<SessionSummary>,
850 pub next_cursor: Option<String>,
852 pub has_more: bool,
853}
854
855#[derive(Debug, Deserialize)]
859#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
860#[cfg_attr(feature = "ts", ts(export))]
861pub struct StreamEventsRequest {
862 #[cfg_attr(feature = "ts", ts(type = "any"))]
863 pub agent: Option<Agent>,
864 #[cfg_attr(feature = "ts", ts(type = "any"))]
865 pub context: Option<SessionContext>,
866 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
867 pub events: Vec<Event>,
868}
869
870#[derive(Debug, Serialize, Deserialize)]
872#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
873#[cfg_attr(feature = "ts", ts(export))]
874pub struct StreamEventsResponse {
875 pub accepted: usize,
876}
877
878#[derive(Debug, Serialize, Deserialize)]
882#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
883#[cfg_attr(feature = "ts", ts(export))]
884pub struct HealthResponse {
885 pub status: String,
886 pub version: String,
887}
888
889#[derive(Debug, Clone)]
896#[non_exhaustive]
897pub enum ServiceError {
898 BadRequest(String),
899 Unauthorized(String),
900 Forbidden(String),
901 NotFound(String),
902 Conflict(String),
903 Internal(String),
904}
905
906impl ServiceError {
907 pub fn status_code(&self) -> u16 {
909 match self {
910 Self::BadRequest(_) => 400,
911 Self::Unauthorized(_) => 401,
912 Self::Forbidden(_) => 403,
913 Self::NotFound(_) => 404,
914 Self::Conflict(_) => 409,
915 Self::Internal(_) => 500,
916 }
917 }
918
919 pub fn message(&self) -> &str {
921 match self {
922 Self::BadRequest(m)
923 | Self::Unauthorized(m)
924 | Self::Forbidden(m)
925 | Self::NotFound(m)
926 | Self::Conflict(m)
927 | Self::Internal(m) => m,
928 }
929 }
930
931 pub fn from_db<E: std::fmt::Display>(context: &str) -> impl FnOnce(E) -> Self + '_ {
933 move |e| Self::Internal(format!("{context}: {e}"))
934 }
935}
936
937impl std::fmt::Display for ServiceError {
938 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
939 write!(f, "{}", self.message())
940 }
941}
942
943impl std::error::Error for ServiceError {}
944
945#[derive(Debug, Serialize)]
949#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
950#[cfg_attr(feature = "ts", ts(export))]
951pub struct ApiError {
952 pub error: String,
953}
954
955impl From<&ServiceError> for ApiError {
956 fn from(e: &ServiceError) -> Self {
957 Self {
958 error: e.message().to_string(),
959 }
960 }
961}
962
963#[cfg(all(test, feature = "ts"))]
966mod tests {
967 use super::*;
968 use std::io::Write;
969 use std::path::PathBuf;
970 use ts_rs::TS;
971
972 #[test]
974 fn export_typescript() {
975 let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
976 .join("../../packages/ui/src/api-types.generated.ts");
977
978 let cfg = ts_rs::Config::new().with_large_int("number");
979 let mut parts: Vec<String> = Vec::new();
980 parts.push("// AUTO-GENERATED by opensession-api — DO NOT EDIT".to_string());
981 parts.push(
982 "// Regenerate with: cargo test -p opensession-api -- export_typescript".to_string(),
983 );
984 parts.push(String::new());
985
986 macro_rules! collect_ts {
990 ($($t:ty),+ $(,)?) => {
991 $(
992 let decl = <$t>::decl(&cfg);
993 let decl = if decl.contains(" = {") {
994 decl
996 .replacen("type ", "export interface ", 1)
997 .replace(" = {", " {")
998 .trim_end_matches(';')
999 .to_string()
1000 } else {
1001 decl
1003 .replacen("type ", "export type ", 1)
1004 .trim_end_matches(';')
1005 .to_string()
1006 };
1007 parts.push(decl);
1008 parts.push(String::new());
1009 )+
1010 };
1011 }
1012
1013 collect_ts!(
1014 TeamRole,
1016 InvitationStatus,
1017 SortOrder,
1018 TimeRange,
1019 LinkType,
1020 RegisterRequest,
1022 AuthRegisterRequest,
1023 LoginRequest,
1024 AuthTokenResponse,
1025 RefreshRequest,
1026 LogoutRequest,
1027 ChangePasswordRequest,
1028 RegisterResponse,
1029 VerifyResponse,
1030 UserSettingsResponse,
1031 OkResponse,
1032 RegenerateKeyResponse,
1033 OAuthLinkResponse,
1034 UploadResponse,
1036 SessionSummary,
1037 SessionListResponse,
1038 SessionListQuery,
1039 SessionDetail,
1040 SessionLink,
1041 CreateTeamRequest,
1043 TeamResponse,
1044 ListTeamsResponse,
1045 TeamDetailResponse,
1046 UpdateTeamRequest,
1047 TeamStatsQuery,
1048 TeamStatsResponse,
1049 TeamStatsTotals,
1050 UserStats,
1051 ToolStats,
1052 CreateTeamInviteKeyRequest,
1053 CreateTeamInviteKeyResponse,
1054 TeamInviteKeySummary,
1055 ListTeamInviteKeysResponse,
1056 JoinTeamWithKeyRequest,
1057 JoinTeamWithKeyResponse,
1058 AddMemberRequest,
1060 MemberResponse,
1061 ListMembersResponse,
1062 InviteRequest,
1064 InvitationResponse,
1065 ListInvitationsResponse,
1066 AcceptInvitationResponse,
1067 oauth::AuthProvidersResponse,
1069 oauth::OAuthProviderInfo,
1070 oauth::LinkedProvider,
1071 HealthResponse,
1073 ApiError,
1074 );
1075
1076 let content = parts.join("\n");
1077
1078 if let Some(parent) = out_dir.parent() {
1080 std::fs::create_dir_all(parent).ok();
1081 }
1082 let mut file = std::fs::File::create(&out_dir)
1083 .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_dir.display(), e));
1084 file.write_all(content.as_bytes())
1085 .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_dir.display(), e));
1086
1087 println!("Generated TypeScript types at: {}", out_dir.display());
1088 }
1089}