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