Skip to main content

heddle_client/grpc_hosted/
user.rs

1use grpc::heddle::v1::{
2    ApproveThreadRequest, BeginWebAuthnAuthenticationRequest, CheckMergeEligibilityRequest,
3    CheckMergeEligibilityResponse, CreateGrantRequest, CreateInvitationRequest,
4    CreateRepositoryRequest, DeleteGrantRequest, DeleteNamespaceRequest, DeleteRepositoryRequest,
5    GetCurrentUserNamespaceRequest, GrantSupportAccessRequest, GrantTargetRef,
6    Invitation as ProtoInvitation, ListGrantsRequest, ListNamespacesRequest,
7    ListRepositoriesRequest, ListSupportAccessGrantsRequest, ListThreadApprovalsRequest,
8    RevokeApprovalRequest, RevokeSupportAccessRequest, SupportAccessGrant, ThreadApproval,
9    UpdateGrantRequest, UpdateNamespaceRequest, UpdateRepositoryRequest,
10    grant_target_ref::Target as GrantTargetKind,
11};
12use proto::ProtocolError;
13use tonic::Request;
14
15use super::{
16    HostedGrpcClient,
17    helpers::{
18        status_to_protocol_error, to_protocol_grant, to_protocol_namespace, to_protocol_repository,
19    },
20};
21
22impl HostedGrpcClient {
23    pub async fn begin_login(
24        &mut self,
25        username: &str,
26    ) -> Result<(String, String, u64), ProtocolError> {
27        let request = Request::new(BeginWebAuthnAuthenticationRequest {
28            username: username.to_string(),
29        });
30        let response = self
31            .auth
32            .begin_web_authn_authentication(request)
33            .await
34            .map_err(status_to_protocol_error)?
35            .into_inner();
36        let expires_at_secs = response
37            .expires_at
38            .as_ref()
39            .map(|t| t.seconds.max(0) as u64)
40            .unwrap_or(0);
41        Ok((response.challenge_id, response.challenge, expires_at_secs))
42    }
43
44    pub async fn get_current_user_namespace(
45        &mut self,
46    ) -> Result<proto::HostedNamespaceInfo, ProtocolError> {
47        let mut request = Request::new(GetCurrentUserNamespaceRequest {});
48        self.apply_auth(&mut request)?;
49        let namespace = self
50            .user
51            .get_current_user_namespace(request)
52            .await
53            .map_err(status_to_protocol_error)?
54            .into_inner();
55        Ok(to_protocol_namespace(namespace))
56    }
57
58    pub async fn list_namespaces(
59        &mut self,
60    ) -> Result<Vec<proto::HostedNamespaceInfo>, ProtocolError> {
61        let mut request = Request::new(ListNamespacesRequest {});
62        self.apply_auth(&mut request)?;
63        let response = self
64            .user
65            .list_namespaces(request)
66            .await
67            .map_err(status_to_protocol_error)?
68            .into_inner();
69        Ok(response
70            .namespaces
71            .into_iter()
72            .map(to_protocol_namespace)
73            .collect())
74    }
75
76    pub async fn create_namespace(
77        &mut self,
78        kind: &str,
79        slug: &str,
80        parent_path: Option<&str>,
81        display_name: Option<String>,
82    ) -> Result<proto::HostedNamespaceInfo, ProtocolError> {
83        let mut request = Request::new(grpc::heddle::v1::CreateNamespaceRequest {
84            kind: parse_namespace_kind_arg(kind)? as i32,
85            slug: slug.to_string(),
86            parent_path: parent_path.unwrap_or_default().to_string(),
87            display_name: display_name.unwrap_or_default(),
88            client_operation_id: String::new(),
89        });
90        self.apply_auth(&mut request)?;
91        let namespace = self
92            .user
93            .create_namespace(request)
94            .await
95            .map_err(status_to_protocol_error)?
96            .into_inner();
97        Ok(to_protocol_namespace(namespace))
98    }
99
100    pub async fn create_repository(
101        &mut self,
102        namespace_path: &str,
103        slug: &str,
104    ) -> Result<proto::HostedRepositoryInfo, ProtocolError> {
105        let mut request = Request::new(CreateRepositoryRequest {
106            namespace_path: namespace_path.to_string(),
107            slug: slug.to_string(),
108            client_operation_id: String::new(),
109        });
110        self.apply_auth(&mut request)?;
111        let repo = self
112            .user
113            .create_repository(request)
114            .await
115            .map_err(status_to_protocol_error)?
116            .into_inner();
117        Ok(to_protocol_repository(repo))
118    }
119
120    pub async fn list_repositories(
121        &mut self,
122        namespace_path: Option<&str>,
123    ) -> Result<Vec<proto::HostedRepositoryInfo>, ProtocolError> {
124        let mut request = Request::new(ListRepositoriesRequest {
125            namespace_path: namespace_path.unwrap_or_default().to_string(),
126        });
127        self.apply_auth(&mut request)?;
128        let response = self
129            .user
130            .list_repositories(request)
131            .await
132            .map_err(status_to_protocol_error)?
133            .into_inner();
134        Ok(response
135            .repositories
136            .into_iter()
137            .map(to_protocol_repository)
138            .collect())
139    }
140
141    pub async fn update_namespace(
142        &mut self,
143        full_path: &str,
144        new_slug: Option<&str>,
145        display_name: Option<Option<String>>,
146    ) -> Result<proto::HostedNamespaceInfo, ProtocolError> {
147        let (display_name, clear_display_name) = match display_name {
148            Some(Some(value)) => (value, false),
149            Some(None) => (String::new(), true),
150            None => (String::new(), false),
151        };
152        let mut request = Request::new(UpdateNamespaceRequest {
153            full_path: full_path.to_string(),
154            new_slug: new_slug.unwrap_or_default().to_string(),
155            display_name,
156            clear_display_name,
157            client_operation_id: String::new(),
158        });
159        self.apply_auth(&mut request)?;
160        let namespace = self
161            .user
162            .update_namespace(request)
163            .await
164            .map_err(status_to_protocol_error)?
165            .into_inner();
166        Ok(to_protocol_namespace(namespace))
167    }
168
169    pub async fn delete_namespace(&mut self, full_path: &str) -> Result<(), ProtocolError> {
170        let mut request = Request::new(DeleteNamespaceRequest {
171            full_path: full_path.to_string(),
172            client_operation_id: String::new(),
173        });
174        self.apply_auth(&mut request)?;
175        self.user
176            .delete_namespace(request)
177            .await
178            .map_err(status_to_protocol_error)?;
179        Ok(())
180    }
181
182    pub async fn update_repository(
183        &mut self,
184        full_path: &str,
185        new_slug: &str,
186    ) -> Result<proto::HostedRepositoryInfo, ProtocolError> {
187        let mut request = Request::new(UpdateRepositoryRequest {
188            full_path: full_path.to_string(),
189            new_slug: new_slug.to_string(),
190            client_operation_id: String::new(),
191        });
192        self.apply_auth(&mut request)?;
193        let repo = self
194            .user
195            .update_repository(request)
196            .await
197            .map_err(status_to_protocol_error)?
198            .into_inner();
199        Ok(to_protocol_repository(repo))
200    }
201
202    pub async fn delete_repository(&mut self, full_path: &str) -> Result<(), ProtocolError> {
203        let mut request = Request::new(DeleteRepositoryRequest {
204            full_path: full_path.to_string(),
205            client_operation_id: String::new(),
206        });
207        self.apply_auth(&mut request)?;
208        self.user
209            .delete_repository(request)
210            .await
211            .map_err(status_to_protocol_error)?;
212        Ok(())
213    }
214
215    pub async fn create_grant(
216        &mut self,
217        subject: &str,
218        role: &str,
219        namespace_path: Option<&str>,
220        repo_path: Option<&str>,
221    ) -> Result<proto::HostedGrantInfo, ProtocolError> {
222        let target = build_target_ref(namespace_path, repo_path)?;
223        let mut request = Request::new(CreateGrantRequest {
224            subject: subject.to_string(),
225            role: parse_hosted_role_arg(role)? as i32,
226            target,
227            client_operation_id: String::new(),
228        });
229        self.apply_auth(&mut request)?;
230        let grant = self
231            .user
232            .create_grant(request)
233            .await
234            .map_err(status_to_protocol_error)?
235            .into_inner();
236        Ok(to_protocol_grant(grant))
237    }
238
239    pub async fn list_grants(
240        &mut self,
241        resource: Option<&str>,
242    ) -> Result<Vec<proto::HostedGrantInfo>, ProtocolError> {
243        let mut request = Request::new(ListGrantsRequest {
244            resource: resource.unwrap_or_default().to_string(),
245        });
246        self.apply_auth(&mut request)?;
247        let response = self
248            .user
249            .list_grants(request)
250            .await
251            .map_err(status_to_protocol_error)?
252            .into_inner();
253        Ok(response.grants.into_iter().map(to_protocol_grant).collect())
254    }
255
256    pub async fn update_grant(
257        &mut self,
258        subject: &str,
259        role: &str,
260        namespace_path: Option<&str>,
261        repo_path: Option<&str>,
262    ) -> Result<proto::HostedGrantInfo, ProtocolError> {
263        let target = build_target_ref(namespace_path, repo_path)?;
264        let mut request = Request::new(UpdateGrantRequest {
265            subject: subject.to_string(),
266            role: parse_hosted_role_arg(role)? as i32,
267            target,
268            client_operation_id: String::new(),
269        });
270        self.apply_auth(&mut request)?;
271        let grant = self
272            .user
273            .update_grant(request)
274            .await
275            .map_err(status_to_protocol_error)?
276            .into_inner();
277        Ok(to_protocol_grant(grant))
278    }
279
280    pub async fn delete_grant(
281        &mut self,
282        subject: &str,
283        namespace_path: Option<&str>,
284        repo_path: Option<&str>,
285    ) -> Result<(), ProtocolError> {
286        let target = build_target_ref(namespace_path, repo_path)?;
287        let mut request = Request::new(DeleteGrantRequest {
288            subject: subject.to_string(),
289            target,
290            client_operation_id: String::new(),
291        });
292        self.apply_auth(&mut request)?;
293        self.user
294            .delete_grant(request)
295            .await
296            .map_err(status_to_protocol_error)?;
297        Ok(())
298    }
299
300    /// Track D — create a pending invitation. Returns the raw proto type
301    /// to keep the surface narrow until we settle on a domain shape.
302    pub async fn create_invitation(
303        &mut self,
304        email: &str,
305        namespace_path: &str,
306        role: &str,
307    ) -> Result<ProtoInvitation, ProtocolError> {
308        let mut request = Request::new(CreateInvitationRequest {
309            email: email.to_string(),
310            namespace_path: namespace_path.to_string(),
311            role: parse_hosted_role_arg(role)? as i32,
312            expires_at: None,
313            metadata: String::new(),
314            client_operation_id: String::new(),
315        });
316        self.apply_auth(&mut request)?;
317        let invitation = self
318            .user
319            .create_invitation(request)
320            .await
321            .map_err(status_to_protocol_error)?
322            .into_inner();
323        Ok(invitation)
324    }
325
326    /// Record an approval for `(source_thread → target_thread)` at
327    /// the source's current `source_state`. The server's gate decides
328    /// later whether this approval *counts* against any matching
329    /// policy's requirements.
330    pub async fn approve_thread(
331        &mut self,
332        repo_path: &str,
333        source_thread: &str,
334        target_thread: &str,
335        source_state: &str,
336        note: Option<&str>,
337    ) -> Result<ThreadApproval, ProtocolError> {
338        let mut request = Request::new(ApproveThreadRequest {
339            repo_path: repo_path.to_string(),
340            source_thread: source_thread.to_string(),
341            target_thread: target_thread.to_string(),
342            source_state: objects::object::ChangeId::parse(source_state)
343                .map(|id| id.as_bytes().to_vec())
344                .unwrap_or_default(),
345            note: note.unwrap_or_default().to_string(),
346            client_operation_id: String::new(),
347        });
348        self.apply_auth(&mut request)?;
349        Ok(self
350            .user
351            .approve_thread(request)
352            .await
353            .map_err(status_to_protocol_error)?
354            .into_inner())
355    }
356
357    pub async fn revoke_approval(&mut self, id: &str) -> Result<(), ProtocolError> {
358        let mut request = Request::new(RevokeApprovalRequest {
359            id: id.to_string(),
360            client_operation_id: String::new(),
361        });
362        self.apply_auth(&mut request)?;
363        self.user
364            .revoke_approval(request)
365            .await
366            .map_err(status_to_protocol_error)?;
367        Ok(())
368    }
369
370    pub async fn list_thread_approvals(
371        &mut self,
372        repo_path: &str,
373        source_thread: &str,
374        target_thread: &str,
375    ) -> Result<Vec<ThreadApproval>, ProtocolError> {
376        let mut request = Request::new(ListThreadApprovalsRequest {
377            repo_path: repo_path.to_string(),
378            source_thread: source_thread.to_string(),
379            target_thread: target_thread.to_string(),
380        });
381        self.apply_auth(&mut request)?;
382        Ok(self
383            .user
384            .list_thread_approvals(request)
385            .await
386            .map_err(status_to_protocol_error)?
387            .into_inner()
388            .approvals)
389    }
390
391    /// Ask the server "can <source> merge into <target> at
392    /// <source_state>, given the diff touches `changed_paths`?" The
393    /// reply lists every unmet requirement and the approvals that
394    /// counted as valid.
395    #[allow(clippy::too_many_arguments)]
396    pub async fn check_merge_eligibility(
397        &mut self,
398        repo_path: &str,
399        source_thread: &str,
400        target_thread: &str,
401        source_state: &str,
402        gated_action: &str,
403        changed_paths: Vec<String>,
404        author_user_id: Option<&str>,
405    ) -> Result<CheckMergeEligibilityResponse, ProtocolError> {
406        let mut request = Request::new(CheckMergeEligibilityRequest {
407            repo_path: repo_path.to_string(),
408            source_thread: source_thread.to_string(),
409            target_thread: target_thread.to_string(),
410            source_state: objects::object::ChangeId::parse(source_state)
411                .map(|id| id.as_bytes().to_vec())
412                .unwrap_or_default(),
413            gated_action: gated_action.to_string(),
414            changed_paths,
415            author_user_id: author_user_id.unwrap_or_default().to_string(),
416        });
417        self.apply_auth(&mut request)?;
418        Ok(self
419            .user
420            .check_merge_eligibility(request)
421            .await
422            .map_err(status_to_protocol_error)?
423            .into_inner())
424    }
425
426    /// Phase C: grant a Heddle staff member temporary admin on a
427    /// namespace or repo. Exactly one of `namespace_path` or
428    /// `repo_path` should be set.
429    pub async fn grant_support_access(
430        &mut self,
431        operator_email: &str,
432        namespace_path: Option<&str>,
433        repo_path: Option<&str>,
434        ttl_seconds: u32,
435        reason: &str,
436        client_operation_id: String,
437    ) -> Result<SupportAccessGrant, ProtocolError> {
438        let target = build_target_ref(namespace_path, repo_path)?;
439        let mut request = Request::new(GrantSupportAccessRequest {
440            operator_email: operator_email.to_string(),
441            target,
442            ttl_seconds: Some(prost_types::Duration {
443                seconds: i64::from(ttl_seconds),
444                nanos: 0,
445            }),
446            reason: reason.to_string(),
447            client_operation_id,
448        });
449        self.apply_auth(&mut request)?;
450        Ok(self
451            .user
452            .grant_support_access(request)
453            .await
454            .map_err(status_to_protocol_error)?
455            .into_inner())
456    }
457
458    pub async fn list_support_access_grants(
459        &mut self,
460        namespace_path: Option<&str>,
461        repo_path: Option<&str>,
462        include_inactive: bool,
463    ) -> Result<Vec<SupportAccessGrant>, ProtocolError> {
464        let target = build_target_ref(namespace_path, repo_path)?;
465        let mut request = Request::new(ListSupportAccessGrantsRequest {
466            target,
467            include_inactive,
468        });
469        self.apply_auth(&mut request)?;
470        Ok(self
471            .user
472            .list_support_access_grants(request)
473            .await
474            .map_err(status_to_protocol_error)?
475            .into_inner()
476            .grants)
477    }
478
479    pub async fn revoke_support_access(
480        &mut self,
481        id: &str,
482        client_operation_id: String,
483    ) -> Result<(), ProtocolError> {
484        let mut request = Request::new(RevokeSupportAccessRequest {
485            id: id.to_string(),
486            client_operation_id,
487        });
488        self.apply_auth(&mut request)?;
489        self.user
490            .revoke_support_access(request)
491            .await
492            .map_err(status_to_protocol_error)?;
493        Ok(())
494    }
495}
496
497/// Build a `GrantTargetRef` oneof from CLI-style optional path args.
498/// Caller layer enforces that at most one of `namespace_path` /
499/// `repo_path` is set; this helper is just the wire-format adapter.
500fn build_target_ref(
501    namespace_path: Option<&str>,
502    repo_path: Option<&str>,
503) -> Result<Option<GrantTargetRef>, ProtocolError> {
504    match (
505        namespace_path.filter(|s| !s.is_empty()),
506        repo_path.filter(|s| !s.is_empty()),
507    ) {
508        (Some(ns), None) => Ok(Some(GrantTargetRef {
509            target: Some(GrantTargetKind::NamespacePath(ns.to_string())),
510        })),
511        (None, Some(rp)) => Ok(Some(GrantTargetRef {
512            target: Some(GrantTargetKind::RepoPath(rp.to_string())),
513        })),
514        _ => Err(ProtocolError::InvalidState(
515            "exactly one of namespace_path or repo_path must be set".into(),
516        )),
517    }
518}
519
520/// Parse a CLI-supplied namespace kind string ("user" / "namespace" /
521/// "team", with "org" accepted as an alias for "namespace") into the
522/// proto `NamespaceKind` enum.
523fn parse_namespace_kind_arg(value: &str) -> Result<grpc::heddle::v1::NamespaceKind, ProtocolError> {
524    use grpc::heddle::v1::NamespaceKind;
525    match value.trim().to_ascii_lowercase().as_str() {
526        "user" => Ok(NamespaceKind::User),
527        "namespace" | "org" => Ok(NamespaceKind::Org),
528        "team" => Ok(NamespaceKind::Team),
529        other => Err(ProtocolError::InvalidState(format!(
530            "invalid namespace kind '{other}': expected user|namespace|team"
531        ))),
532    }
533}
534
535/// Parse a CLI-supplied role name into the proto `HostedRole` enum.
536fn parse_hosted_role_arg(value: &str) -> Result<grpc::heddle::v1::HostedRole, ProtocolError> {
537    use grpc::heddle::v1::HostedRole;
538    match value.trim().to_ascii_lowercase().as_str() {
539        "reader" => Ok(HostedRole::Reader),
540        "developer" => Ok(HostedRole::Developer),
541        "maintainer" => Ok(HostedRole::Maintainer),
542        "admin" => Ok(HostedRole::Admin),
543        "owner" => Ok(HostedRole::Owner),
544        other => Err(ProtocolError::InvalidState(format!(
545            "invalid role '{other}': expected reader|developer|maintainer|admin|owner"
546        ))),
547    }
548}