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 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub score_plugin: Option<String>,
329}
330
331#[derive(Debug, Serialize, Deserialize)]
333#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
334#[cfg_attr(feature = "ts", ts(export))]
335pub struct UploadResponse {
336 pub id: String,
337 pub url: String,
338 #[serde(default)]
339 pub session_score: i64,
340 #[serde(default = "default_score_plugin")]
341 pub score_plugin: String,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
347#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
348#[cfg_attr(feature = "ts", ts(export))]
349pub struct SessionSummary {
350 pub id: String,
351 pub user_id: Option<String>,
352 pub nickname: Option<String>,
353 pub team_id: String,
354 pub tool: String,
355 pub agent_provider: Option<String>,
356 pub agent_model: Option<String>,
357 pub title: Option<String>,
358 pub description: Option<String>,
359 pub tags: Option<String>,
361 pub created_at: String,
362 pub uploaded_at: String,
363 pub message_count: i64,
364 pub task_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 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub git_remote: Option<String>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub git_branch: Option<String>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub git_commit: Option<String>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub git_repo_name: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub pr_number: Option<i64>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub pr_url: Option<String>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub working_directory: Option<String>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub files_modified: Option<String>,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub files_read: Option<String>,
387 #[serde(default)]
388 pub has_errors: bool,
389 #[serde(default = "default_max_active_agents")]
390 pub max_active_agents: i64,
391 #[serde(default)]
392 pub session_score: i64,
393 #[serde(default = "default_score_plugin")]
394 pub score_plugin: String,
395}
396
397#[derive(Debug, Serialize, Deserialize)]
399#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
400#[cfg_attr(feature = "ts", ts(export))]
401pub struct SessionListResponse {
402 pub sessions: Vec<SessionSummary>,
403 pub total: i64,
404 pub page: u32,
405 pub per_page: u32,
406}
407
408#[derive(Debug, Deserialize)]
410#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
411#[cfg_attr(feature = "ts", ts(export))]
412pub struct SessionListQuery {
413 #[serde(default = "default_page")]
414 pub page: u32,
415 #[serde(default = "default_per_page")]
416 pub per_page: u32,
417 pub search: Option<String>,
418 pub tool: Option<String>,
419 pub team_id: Option<String>,
420 pub sort: Option<SortOrder>,
422 pub time_range: Option<TimeRange>,
424}
425
426impl SessionListQuery {
427 pub fn is_public_feed_cacheable(
429 &self,
430 has_auth_header: bool,
431 has_session_cookie: bool,
432 ) -> bool {
433 !has_auth_header
434 && !has_session_cookie
435 && self.team_id.is_none()
436 && self.search.as_deref().is_none_or(|s| s.trim().is_empty())
437 && self.page <= 10
438 && self.per_page <= 50
439 }
440}
441
442#[cfg(test)]
443mod session_list_query_tests {
444 use super::*;
445
446 fn base_query() -> SessionListQuery {
447 SessionListQuery {
448 page: 1,
449 per_page: 20,
450 search: None,
451 tool: None,
452 team_id: None,
453 sort: None,
454 time_range: None,
455 }
456 }
457
458 #[test]
459 fn public_feed_cacheable_when_anonymous_default_feed() {
460 let q = base_query();
461 assert!(q.is_public_feed_cacheable(false, false));
462 }
463
464 #[test]
465 fn public_feed_not_cacheable_with_auth_or_cookie() {
466 let q = base_query();
467 assert!(!q.is_public_feed_cacheable(true, false));
468 assert!(!q.is_public_feed_cacheable(false, true));
469 }
470
471 #[test]
472 fn public_feed_not_cacheable_for_team_or_search_or_large_page() {
473 let mut q = base_query();
474 q.team_id = Some("team-1".into());
475 assert!(!q.is_public_feed_cacheable(false, false));
476
477 let mut q = base_query();
478 q.search = Some("hello".into());
479 assert!(!q.is_public_feed_cacheable(false, false));
480
481 let mut q = base_query();
482 q.page = 11;
483 assert!(!q.is_public_feed_cacheable(false, false));
484
485 let mut q = base_query();
486 q.per_page = 100;
487 assert!(!q.is_public_feed_cacheable(false, false));
488 }
489}
490
491fn default_page() -> u32 {
492 1
493}
494fn default_per_page() -> u32 {
495 20
496}
497fn default_max_active_agents() -> i64 {
498 1
499}
500
501fn default_score_plugin() -> String {
502 opensession_core::scoring::DEFAULT_SCORE_PLUGIN.to_string()
503}
504
505#[derive(Debug, Serialize, Deserialize)]
507#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
508#[cfg_attr(feature = "ts", ts(export))]
509pub struct SessionDetail {
510 #[serde(flatten)]
511 #[cfg_attr(feature = "ts", ts(flatten))]
512 pub summary: SessionSummary,
513 pub team_name: Option<String>,
514 #[serde(default, skip_serializing_if = "Vec::is_empty")]
515 pub linked_sessions: Vec<SessionLink>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
520#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
521#[cfg_attr(feature = "ts", ts(export))]
522pub struct SessionLink {
523 pub session_id: String,
524 pub linked_session_id: String,
525 pub link_type: LinkType,
526 pub created_at: String,
527}
528
529#[derive(Debug, Serialize, Deserialize)]
533#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
534#[cfg_attr(feature = "ts", ts(export))]
535pub struct CreateTeamRequest {
536 pub name: String,
537 pub description: Option<String>,
538 pub is_public: Option<bool>,
539}
540
541#[derive(Debug, Serialize, Deserialize)]
543#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
544#[cfg_attr(feature = "ts", ts(export))]
545pub struct TeamResponse {
546 pub id: String,
547 pub name: String,
548 pub description: Option<String>,
549 pub is_public: bool,
550 pub created_by: String,
551 pub created_at: String,
552}
553
554#[derive(Debug, Serialize, Deserialize)]
556#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
557#[cfg_attr(feature = "ts", ts(export))]
558pub struct ListTeamsResponse {
559 pub teams: Vec<TeamResponse>,
560}
561
562#[derive(Debug, Serialize, Deserialize)]
564#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
565#[cfg_attr(feature = "ts", ts(export))]
566pub struct TeamDetailResponse {
567 #[serde(flatten)]
568 #[cfg_attr(feature = "ts", ts(flatten))]
569 pub team: TeamResponse,
570 pub member_count: i64,
571 pub sessions: Vec<SessionSummary>,
572}
573
574#[derive(Debug, Serialize, Deserialize)]
576#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
577#[cfg_attr(feature = "ts", ts(export))]
578pub struct UpdateTeamRequest {
579 pub name: Option<String>,
580 pub description: Option<String>,
581 pub is_public: Option<bool>,
582}
583
584#[derive(Debug, Deserialize)]
586#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
587#[cfg_attr(feature = "ts", ts(export))]
588pub struct TeamStatsQuery {
589 pub time_range: Option<TimeRange>,
591}
592
593#[derive(Debug, Serialize)]
595#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
596#[cfg_attr(feature = "ts", ts(export))]
597pub struct TeamStatsResponse {
598 pub team_id: String,
599 pub time_range: TimeRange,
600 pub totals: TeamStatsTotals,
601 pub by_user: Vec<UserStats>,
602 pub by_tool: Vec<ToolStats>,
603}
604
605#[derive(Debug, Serialize)]
607#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
608#[cfg_attr(feature = "ts", ts(export))]
609pub struct TeamStatsTotals {
610 pub session_count: i64,
611 pub message_count: i64,
612 pub event_count: i64,
613 pub tool_call_count: i64,
614 pub duration_seconds: i64,
615 pub total_input_tokens: i64,
616 pub total_output_tokens: i64,
617}
618
619#[derive(Debug, Serialize)]
621#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
622#[cfg_attr(feature = "ts", ts(export))]
623pub struct UserStats {
624 pub user_id: String,
625 pub nickname: 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)]
636#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
637#[cfg_attr(feature = "ts", ts(export))]
638pub struct ToolStats {
639 pub tool: String,
640 pub session_count: i64,
641 pub message_count: i64,
642 pub event_count: i64,
643 pub duration_seconds: i64,
644 pub total_input_tokens: i64,
645 pub total_output_tokens: i64,
646}
647
648#[derive(Debug, Serialize, Deserialize)]
650#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
651#[cfg_attr(feature = "ts", ts(export))]
652pub struct CreateTeamInviteKeyRequest {
653 pub role: Option<TeamRole>,
654 pub expires_in_days: Option<u32>,
656}
657
658#[derive(Debug, Serialize, Deserialize)]
661#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
662#[cfg_attr(feature = "ts", ts(export))]
663pub struct CreateTeamInviteKeyResponse {
664 pub key_id: String,
665 pub invite_key: String,
666 pub role: TeamRole,
667 pub expires_at: 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 TeamInviteKeySummary {
675 pub id: String,
676 pub role: TeamRole,
677 pub created_by_nickname: String,
678 pub created_at: String,
679 pub expires_at: String,
680 pub used_at: Option<String>,
681 pub revoked_at: Option<String>,
682}
683
684#[derive(Debug, Serialize, Deserialize)]
686#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
687#[cfg_attr(feature = "ts", ts(export))]
688pub struct ListTeamInviteKeysResponse {
689 pub keys: Vec<TeamInviteKeySummary>,
690}
691
692#[derive(Debug, Serialize, Deserialize)]
694#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
695#[cfg_attr(feature = "ts", ts(export))]
696pub struct JoinTeamWithKeyRequest {
697 pub invite_key: String,
698}
699
700#[derive(Debug, Serialize, Deserialize)]
702#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
703#[cfg_attr(feature = "ts", ts(export))]
704pub struct JoinTeamWithKeyResponse {
705 pub team_id: String,
706 pub team_name: String,
707 pub role: TeamRole,
708}
709
710impl From<opensession_core::stats::SessionAggregate> for TeamStatsTotals {
713 fn from(a: opensession_core::stats::SessionAggregate) -> Self {
714 Self {
715 session_count: saturating_i64(a.session_count),
716 message_count: saturating_i64(a.message_count),
717 event_count: saturating_i64(a.event_count),
718 tool_call_count: saturating_i64(a.tool_call_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
726impl From<(String, opensession_core::stats::SessionAggregate)> for ToolStats {
727 fn from((tool, a): (String, opensession_core::stats::SessionAggregate)) -> Self {
728 Self {
729 tool,
730 session_count: saturating_i64(a.session_count),
731 message_count: saturating_i64(a.message_count),
732 event_count: saturating_i64(a.event_count),
733 duration_seconds: saturating_i64(a.duration_seconds),
734 total_input_tokens: saturating_i64(a.total_input_tokens),
735 total_output_tokens: saturating_i64(a.total_output_tokens),
736 }
737 }
738}
739
740#[derive(Debug, Serialize, Deserialize)]
744#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
745#[cfg_attr(feature = "ts", ts(export))]
746pub struct InviteRequest {
747 pub email: Option<String>,
748 pub oauth_provider: Option<String>,
750 pub oauth_provider_username: Option<String>,
752 pub role: Option<TeamRole>,
753}
754
755#[derive(Debug, Serialize, Deserialize)]
757#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
758#[cfg_attr(feature = "ts", ts(export))]
759pub struct InvitationResponse {
760 pub id: String,
761 pub team_id: String,
762 pub team_name: String,
763 pub email: Option<String>,
764 pub oauth_provider: Option<String>,
765 pub oauth_provider_username: Option<String>,
766 pub invited_by_nickname: String,
767 pub role: TeamRole,
768 pub status: InvitationStatus,
769 pub created_at: String,
770}
771
772#[derive(Debug, Serialize, Deserialize)]
774#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
775#[cfg_attr(feature = "ts", ts(export))]
776pub struct ListInvitationsResponse {
777 pub invitations: Vec<InvitationResponse>,
778}
779
780#[derive(Debug, Serialize, Deserialize)]
782#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
783#[cfg_attr(feature = "ts", ts(export))]
784pub struct AcceptInvitationResponse {
785 pub team_id: String,
786 pub role: TeamRole,
787}
788
789#[derive(Debug, Serialize, Deserialize)]
793#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
794#[cfg_attr(feature = "ts", ts(export))]
795pub struct AddMemberRequest {
796 pub nickname: String,
797}
798
799#[derive(Debug, Serialize, Deserialize)]
801#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
802#[cfg_attr(feature = "ts", ts(export))]
803pub struct MemberResponse {
804 pub user_id: String,
805 pub nickname: String,
806 pub role: TeamRole,
807 pub joined_at: String,
808}
809
810#[derive(Debug, Serialize, Deserialize)]
812#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
813#[cfg_attr(feature = "ts", ts(export))]
814pub struct ListMembersResponse {
815 pub members: Vec<MemberResponse>,
816}
817
818#[derive(Debug, Serialize, Deserialize)]
822#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
823#[cfg_attr(feature = "ts", ts(export))]
824pub struct ConfigSyncResponse {
825 pub privacy: Option<SyncedPrivacyConfig>,
826 pub watchers: Option<SyncedWatcherConfig>,
827}
828
829#[derive(Debug, Serialize, Deserialize)]
831#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
832#[cfg_attr(feature = "ts", ts(export))]
833pub struct SyncedPrivacyConfig {
834 pub exclude_patterns: Option<Vec<String>>,
835 pub exclude_tools: Option<Vec<String>>,
836}
837
838#[derive(Debug, Serialize, Deserialize)]
840#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
841#[cfg_attr(feature = "ts", ts(export))]
842pub struct SyncedWatcherConfig {
843 pub claude_code: Option<bool>,
844 pub opencode: Option<bool>,
845 pub cursor: Option<bool>,
846}
847
848#[derive(Debug, Deserialize)]
852pub struct SyncPullQuery {
853 pub team_id: String,
854 pub since: Option<String>,
856 pub limit: Option<u32>,
858}
859
860#[derive(Debug, Serialize, Deserialize)]
862pub struct SyncPullResponse {
863 pub sessions: Vec<SessionSummary>,
864 pub next_cursor: Option<String>,
866 pub has_more: bool,
867}
868
869#[derive(Debug, Deserialize)]
873#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
874#[cfg_attr(feature = "ts", ts(export))]
875pub struct StreamEventsRequest {
876 #[cfg_attr(feature = "ts", ts(type = "any"))]
877 pub agent: Option<Agent>,
878 #[cfg_attr(feature = "ts", ts(type = "any"))]
879 pub context: Option<SessionContext>,
880 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
881 pub events: Vec<Event>,
882}
883
884#[derive(Debug, Serialize, Deserialize)]
886#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
887#[cfg_attr(feature = "ts", ts(export))]
888pub struct StreamEventsResponse {
889 pub accepted: usize,
890}
891
892#[derive(Debug, Serialize, Deserialize)]
896#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
897#[cfg_attr(feature = "ts", ts(export))]
898pub struct HealthResponse {
899 pub status: String,
900 pub version: String,
901}
902
903#[derive(Debug, Clone)]
910#[non_exhaustive]
911pub enum ServiceError {
912 BadRequest(String),
913 Unauthorized(String),
914 Forbidden(String),
915 NotFound(String),
916 Conflict(String),
917 Internal(String),
918}
919
920impl ServiceError {
921 pub fn status_code(&self) -> u16 {
923 match self {
924 Self::BadRequest(_) => 400,
925 Self::Unauthorized(_) => 401,
926 Self::Forbidden(_) => 403,
927 Self::NotFound(_) => 404,
928 Self::Conflict(_) => 409,
929 Self::Internal(_) => 500,
930 }
931 }
932
933 pub fn message(&self) -> &str {
935 match self {
936 Self::BadRequest(m)
937 | Self::Unauthorized(m)
938 | Self::Forbidden(m)
939 | Self::NotFound(m)
940 | Self::Conflict(m)
941 | Self::Internal(m) => m,
942 }
943 }
944
945 pub fn from_db<E: std::fmt::Display>(context: &str) -> impl FnOnce(E) -> Self + '_ {
947 move |e| Self::Internal(format!("{context}: {e}"))
948 }
949}
950
951impl std::fmt::Display for ServiceError {
952 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
953 write!(f, "{}", self.message())
954 }
955}
956
957impl std::error::Error for ServiceError {}
958
959#[derive(Debug, Serialize)]
963#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
964#[cfg_attr(feature = "ts", ts(export))]
965pub struct ApiError {
966 pub error: String,
967}
968
969impl From<&ServiceError> for ApiError {
970 fn from(e: &ServiceError) -> Self {
971 Self {
972 error: e.message().to_string(),
973 }
974 }
975}
976
977#[cfg(all(test, feature = "ts"))]
980mod tests {
981 use super::*;
982 use std::io::Write;
983 use std::path::PathBuf;
984 use ts_rs::TS;
985
986 #[test]
988 fn export_typescript() {
989 let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
990 .join("../../packages/ui/src/api-types.generated.ts");
991
992 let cfg = ts_rs::Config::new().with_large_int("number");
993 let mut parts: Vec<String> = Vec::new();
994 parts.push("// AUTO-GENERATED by opensession-api — DO NOT EDIT".to_string());
995 parts.push(
996 "// Regenerate with: cargo test -p opensession-api -- export_typescript".to_string(),
997 );
998 parts.push(String::new());
999
1000 macro_rules! collect_ts {
1004 ($($t:ty),+ $(,)?) => {
1005 $(
1006 let decl = <$t>::decl(&cfg);
1007 let decl = if decl.contains(" = {") {
1008 decl
1010 .replacen("type ", "export interface ", 1)
1011 .replace(" = {", " {")
1012 .trim_end_matches(';')
1013 .to_string()
1014 } else {
1015 decl
1017 .replacen("type ", "export type ", 1)
1018 .trim_end_matches(';')
1019 .to_string()
1020 };
1021 parts.push(decl);
1022 parts.push(String::new());
1023 )+
1024 };
1025 }
1026
1027 collect_ts!(
1028 TeamRole,
1030 InvitationStatus,
1031 SortOrder,
1032 TimeRange,
1033 LinkType,
1034 RegisterRequest,
1036 AuthRegisterRequest,
1037 LoginRequest,
1038 AuthTokenResponse,
1039 RefreshRequest,
1040 LogoutRequest,
1041 ChangePasswordRequest,
1042 RegisterResponse,
1043 VerifyResponse,
1044 UserSettingsResponse,
1045 OkResponse,
1046 RegenerateKeyResponse,
1047 OAuthLinkResponse,
1048 UploadResponse,
1050 SessionSummary,
1051 SessionListResponse,
1052 SessionListQuery,
1053 SessionDetail,
1054 SessionLink,
1055 CreateTeamRequest,
1057 TeamResponse,
1058 ListTeamsResponse,
1059 TeamDetailResponse,
1060 UpdateTeamRequest,
1061 TeamStatsQuery,
1062 TeamStatsResponse,
1063 TeamStatsTotals,
1064 UserStats,
1065 ToolStats,
1066 CreateTeamInviteKeyRequest,
1067 CreateTeamInviteKeyResponse,
1068 TeamInviteKeySummary,
1069 ListTeamInviteKeysResponse,
1070 JoinTeamWithKeyRequest,
1071 JoinTeamWithKeyResponse,
1072 AddMemberRequest,
1074 MemberResponse,
1075 ListMembersResponse,
1076 InviteRequest,
1078 InvitationResponse,
1079 ListInvitationsResponse,
1080 AcceptInvitationResponse,
1081 oauth::AuthProvidersResponse,
1083 oauth::OAuthProviderInfo,
1084 oauth::LinkedProvider,
1085 HealthResponse,
1087 ApiError,
1088 );
1089
1090 let content = parts.join("\n");
1091
1092 if let Some(parent) = out_dir.parent() {
1094 std::fs::create_dir_all(parent).ok();
1095 }
1096 let mut file = std::fs::File::create(&out_dir)
1097 .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_dir.display(), e));
1098 file.write_all(content.as_bytes())
1099 .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_dir.display(), e));
1100
1101 println!("Generated TypeScript types at: {}", out_dir.display());
1102 }
1103}