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 tonic::Request;
13use wire::ProtocolError;
14
15use super::{
16 HostedGrpcClient,
17 helpers::{
18 status_to_protocol_error, to_protocol_grant, to_protocol_namespace, to_protocol_repository,
19 },
20};
21
22macro_rules! authed_call {
32 ($self:ident, $rpc:ident, $msg:expr) => {{
33 let mut request = Request::new($msg);
34 $self.apply_auth(&mut request)?;
35 $self
36 .user
37 .$rpc(request)
38 .await
39 .map_err(status_to_protocol_error)?
40 .into_inner()
41 }};
42}
43
44fn default_spool_settings_request() -> grpc::heddle::v1::SpoolSettings {
45 use grpc::heddle::v1::{
46 SpoolBootstrapKind, SpoolBootstrapSyncDirection, SpoolChildPolicy, SpoolInitialTooling,
47 SpoolSettings, SpoolStateVisibility, SpoolSyncBehavior, SpoolVisibility, SpoolWritePolicy,
48 };
49
50 SpoolSettings {
51 visibility: SpoolVisibility::Private as i32,
52 default_state_visibility: SpoolStateVisibility::Internal as i32,
53 bootstrap_kind: SpoolBootstrapKind::Empty as i32,
54 bootstrap_source: String::new(),
55 write_policy: SpoolWritePolicy::Developers as i32,
56 child_policy: SpoolChildPolicy::Maintainers as i32,
57 initial_tooling: Some(SpoolInitialTooling::default()),
58 sync_behavior: SpoolSyncBehavior::Manual as i32,
59 bootstrap_sync_direction: SpoolBootstrapSyncDirection::Pull as i32,
60 description: String::new(),
61 }
62}
63
64impl HostedGrpcClient {
65 pub async fn begin_login(
66 &mut self,
67 username: &str,
68 ) -> Result<(String, String, u64), ProtocolError> {
69 let request = Request::new(BeginWebAuthnAuthenticationRequest {
70 username: username.to_string(),
71 });
72 let response = self
73 .auth
74 .begin_web_authn_authentication(request)
75 .await
76 .map_err(status_to_protocol_error)?
77 .into_inner();
78 let expires_at_secs = response
79 .expires_at
80 .as_ref()
81 .map(|t| t.seconds.max(0) as u64)
82 .unwrap_or(0);
83 Ok((response.challenge_id, response.challenge, expires_at_secs))
84 }
85
86 pub async fn get_current_user_namespace(
87 &mut self,
88 ) -> Result<wire::HostedNamespaceInfo, ProtocolError> {
89 let namespace = authed_call!(
90 self,
91 get_current_user_namespace,
92 GetCurrentUserNamespaceRequest {}
93 );
94 Ok(to_protocol_namespace(namespace))
95 }
96
97 pub async fn list_namespaces(
98 &mut self,
99 ) -> Result<Vec<wire::HostedNamespaceInfo>, ProtocolError> {
100 let response = authed_call!(self, list_namespaces, ListNamespacesRequest {});
101 Ok(response
102 .namespaces
103 .into_iter()
104 .map(to_protocol_namespace)
105 .collect())
106 }
107
108 pub async fn create_namespace(
109 &mut self,
110 kind: &str,
111 slug: &str,
112 parent_path: Option<&str>,
113 display_name: Option<String>,
114 ) -> Result<wire::HostedNamespaceInfo, ProtocolError> {
115 let namespace = authed_call!(
116 self,
117 create_namespace,
118 grpc::heddle::v1::CreateNamespaceRequest {
119 kind: parse_namespace_kind_arg(kind)? as i32,
120 slug: slug.to_string(),
121 parent_path: parent_path.unwrap_or_default().to_string(),
122 display_name: display_name.unwrap_or_default(),
123 settings: Some(default_spool_settings_request()),
124 client_operation_id: String::new(),
125 }
126 );
127 Ok(to_protocol_namespace(namespace))
128 }
129
130 pub async fn create_repository(
131 &mut self,
132 namespace_path: &str,
133 slug: &str,
134 ) -> Result<wire::HostedRepositoryInfo, ProtocolError> {
135 let repo = authed_call!(
136 self,
137 create_repository,
138 CreateRepositoryRequest {
139 namespace_path: namespace_path.to_string(),
140 slug: slug.to_string(),
141 client_operation_id: String::new(),
142 }
143 );
144 Ok(to_protocol_repository(repo))
145 }
146
147 pub async fn list_repositories(
148 &mut self,
149 namespace_path: Option<&str>,
150 ) -> Result<Vec<wire::HostedRepositoryInfo>, ProtocolError> {
151 let response = authed_call!(
152 self,
153 list_repositories,
154 ListRepositoriesRequest {
155 namespace_path: namespace_path.unwrap_or_default().to_string(),
156 }
157 );
158 Ok(response
159 .repositories
160 .into_iter()
161 .map(to_protocol_repository)
162 .collect())
163 }
164
165 pub async fn update_namespace(
166 &mut self,
167 full_path: &str,
168 new_slug: Option<&str>,
169 display_name: Option<Option<String>>,
170 ) -> Result<wire::HostedNamespaceInfo, ProtocolError> {
171 let (display_name, clear_display_name) = match display_name {
172 Some(Some(value)) => (value, false),
173 Some(None) => (String::new(), true),
174 None => (String::new(), false),
175 };
176 let namespace = authed_call!(
177 self,
178 update_namespace,
179 UpdateNamespaceRequest {
180 full_path: full_path.to_string(),
181 new_slug: new_slug.unwrap_or_default().to_string(),
182 display_name,
183 clear_display_name,
184 client_operation_id: String::new(),
185 }
186 );
187 Ok(to_protocol_namespace(namespace))
188 }
189
190 pub async fn delete_namespace(&mut self, full_path: &str) -> Result<(), ProtocolError> {
191 authed_call!(
192 self,
193 delete_namespace,
194 DeleteNamespaceRequest {
195 full_path: full_path.to_string(),
196 client_operation_id: String::new(),
197 }
198 );
199 Ok(())
200 }
201
202 pub async fn update_repository(
203 &mut self,
204 full_path: &str,
205 new_slug: &str,
206 ) -> Result<wire::HostedRepositoryInfo, ProtocolError> {
207 let repo = authed_call!(
208 self,
209 update_repository,
210 UpdateRepositoryRequest {
211 full_path: full_path.to_string(),
212 new_slug: new_slug.to_string(),
213 client_operation_id: String::new(),
214 }
215 );
216 Ok(to_protocol_repository(repo))
217 }
218
219 pub async fn delete_repository(&mut self, full_path: &str) -> Result<(), ProtocolError> {
220 authed_call!(
221 self,
222 delete_repository,
223 DeleteRepositoryRequest {
224 full_path: full_path.to_string(),
225 client_operation_id: String::new(),
226 }
227 );
228 Ok(())
229 }
230
231 pub async fn create_grant(
232 &mut self,
233 subject: &str,
234 role: &str,
235 namespace_path: Option<&str>,
236 repo_path: Option<&str>,
237 ) -> Result<wire::HostedGrantInfo, ProtocolError> {
238 let target = build_target_ref(namespace_path, repo_path)?;
239 let grant = authed_call!(
240 self,
241 create_grant,
242 CreateGrantRequest {
243 subject: subject.to_string(),
244 role: parse_hosted_role_arg(role)? as i32,
245 target,
246 client_operation_id: String::new(),
247 }
248 );
249 Ok(to_protocol_grant(grant))
250 }
251
252 pub async fn list_grants(
253 &mut self,
254 resource: Option<&str>,
255 ) -> Result<Vec<wire::HostedGrantInfo>, ProtocolError> {
256 let response = authed_call!(
257 self,
258 list_grants,
259 ListGrantsRequest {
260 resource: resource.unwrap_or_default().to_string(),
261 }
262 );
263 Ok(response.grants.into_iter().map(to_protocol_grant).collect())
264 }
265
266 pub async fn update_grant(
267 &mut self,
268 subject: &str,
269 role: &str,
270 namespace_path: Option<&str>,
271 repo_path: Option<&str>,
272 ) -> Result<wire::HostedGrantInfo, ProtocolError> {
273 let target = build_target_ref(namespace_path, repo_path)?;
274 let grant = authed_call!(
275 self,
276 update_grant,
277 UpdateGrantRequest {
278 subject: subject.to_string(),
279 role: parse_hosted_role_arg(role)? as i32,
280 target,
281 client_operation_id: String::new(),
282 }
283 );
284 Ok(to_protocol_grant(grant))
285 }
286
287 pub async fn delete_grant(
288 &mut self,
289 subject: &str,
290 namespace_path: Option<&str>,
291 repo_path: Option<&str>,
292 ) -> Result<(), ProtocolError> {
293 let target = build_target_ref(namespace_path, repo_path)?;
294 authed_call!(
295 self,
296 delete_grant,
297 DeleteGrantRequest {
298 subject: subject.to_string(),
299 target,
300 client_operation_id: String::new(),
301 }
302 );
303 Ok(())
304 }
305
306 pub async fn create_invitation(
309 &mut self,
310 email: &str,
311 namespace_path: &str,
312 role: &str,
313 ) -> Result<ProtoInvitation, ProtocolError> {
314 let invitation = authed_call!(
315 self,
316 create_invitation,
317 CreateInvitationRequest {
318 email: email.to_string(),
319 namespace_path: namespace_path.to_string(),
320 role: parse_hosted_role_arg(role)? as i32,
321 expires_at: None,
322 metadata: String::new(),
323 client_operation_id: String::new(),
324 }
325 );
326 Ok(invitation)
327 }
328
329 pub async fn approve_thread(
334 &mut self,
335 repo_path: &str,
336 source_thread: &str,
337 target_thread: &str,
338 source_state: &str,
339 note: Option<&str>,
340 ) -> Result<ThreadApproval, ProtocolError> {
341 Ok(authed_call!(
342 self,
343 approve_thread,
344 ApproveThreadRequest {
345 repo_path: repo_path.to_string(),
346 source_thread: source_thread.to_string(),
347 target_thread: target_thread.to_string(),
348 source_state: objects::object::ChangeId::parse(source_state)
349 .map(|id| id.as_bytes().to_vec())
350 .unwrap_or_default(),
351 note: note.unwrap_or_default().to_string(),
352 client_operation_id: String::new(),
353 }
354 ))
355 }
356
357 pub async fn revoke_approval(&mut self, id: &str) -> Result<(), ProtocolError> {
358 authed_call!(
359 self,
360 revoke_approval,
361 RevokeApprovalRequest {
362 id: id.to_string(),
363 client_operation_id: String::new(),
364 }
365 );
366 Ok(())
367 }
368
369 pub async fn list_thread_approvals(
370 &mut self,
371 repo_path: &str,
372 source_thread: &str,
373 target_thread: &str,
374 ) -> Result<Vec<ThreadApproval>, ProtocolError> {
375 Ok(authed_call!(
376 self,
377 list_thread_approvals,
378 ListThreadApprovalsRequest {
379 repo_path: repo_path.to_string(),
380 source_thread: source_thread.to_string(),
381 target_thread: target_thread.to_string(),
382 }
383 )
384 .approvals)
385 }
386
387 #[allow(clippy::too_many_arguments)]
392 pub async fn check_merge_eligibility(
393 &mut self,
394 repo_path: &str,
395 source_thread: &str,
396 target_thread: &str,
397 source_state: &str,
398 gated_action: &str,
399 changed_paths: Vec<String>,
400 author_user_id: Option<&str>,
401 ) -> Result<CheckMergeEligibilityResponse, ProtocolError> {
402 Ok(authed_call!(
403 self,
404 check_merge_eligibility,
405 CheckMergeEligibilityRequest {
406 repo_path: repo_path.to_string(),
407 source_thread: source_thread.to_string(),
408 target_thread: target_thread.to_string(),
409 source_state: objects::object::ChangeId::parse(source_state)
410 .map(|id| id.as_bytes().to_vec())
411 .unwrap_or_default(),
412 gated_action: gated_action.to_string(),
413 changed_paths,
414 author_user_id: author_user_id.unwrap_or_default().to_string(),
415 }
416 ))
417 }
418
419 pub async fn grant_support_access(
423 &mut self,
424 operator_email: &str,
425 namespace_path: Option<&str>,
426 repo_path: Option<&str>,
427 ttl_seconds: u32,
428 reason: &str,
429 client_operation_id: String,
430 ) -> Result<SupportAccessGrant, ProtocolError> {
431 let target = build_target_ref(namespace_path, repo_path)?;
432 Ok(authed_call!(
433 self,
434 grant_support_access,
435 GrantSupportAccessRequest {
436 operator_email: operator_email.to_string(),
437 target,
438 ttl_seconds: Some(prost_types::Duration {
439 seconds: i64::from(ttl_seconds),
440 nanos: 0,
441 }),
442 reason: reason.to_string(),
443 client_operation_id,
444 }
445 ))
446 }
447
448 pub async fn list_support_access_grants(
449 &mut self,
450 namespace_path: Option<&str>,
451 repo_path: Option<&str>,
452 include_inactive: bool,
453 ) -> Result<Vec<SupportAccessGrant>, ProtocolError> {
454 let target = build_target_ref(namespace_path, repo_path)?;
455 Ok(authed_call!(
456 self,
457 list_support_access_grants,
458 ListSupportAccessGrantsRequest {
459 target,
460 include_inactive,
461 }
462 )
463 .grants)
464 }
465
466 pub async fn revoke_support_access(
467 &mut self,
468 id: &str,
469 client_operation_id: String,
470 ) -> Result<(), ProtocolError> {
471 authed_call!(
472 self,
473 revoke_support_access,
474 RevokeSupportAccessRequest {
475 id: id.to_string(),
476 client_operation_id,
477 }
478 );
479 Ok(())
480 }
481}
482
483fn build_target_ref(
487 namespace_path: Option<&str>,
488 repo_path: Option<&str>,
489) -> Result<Option<GrantTargetRef>, ProtocolError> {
490 match (
491 namespace_path.filter(|s| !s.is_empty()),
492 repo_path.filter(|s| !s.is_empty()),
493 ) {
494 (Some(ns), None) => Ok(Some(GrantTargetRef {
495 target: Some(GrantTargetKind::NamespacePath(ns.to_string())),
496 })),
497 (None, Some(rp)) => Ok(Some(GrantTargetRef {
498 target: Some(GrantTargetKind::RepoPath(rp.to_string())),
499 })),
500 _ => Err(ProtocolError::InvalidState(
501 "exactly one of namespace_path or repo_path must be set".into(),
502 )),
503 }
504}
505
506fn parse_namespace_kind_arg(value: &str) -> Result<grpc::heddle::v1::NamespaceKind, ProtocolError> {
510 use grpc::heddle::v1::NamespaceKind;
511 match value.trim().to_ascii_lowercase().as_str() {
512 "user" => Ok(NamespaceKind::User),
513 "namespace" | "org" => Ok(NamespaceKind::Org),
514 "team" => Ok(NamespaceKind::Team),
515 other => Err(ProtocolError::InvalidState(format!(
516 "invalid namespace kind '{other}': expected user|namespace|team"
517 ))),
518 }
519}
520
521fn parse_hosted_role_arg(value: &str) -> Result<grpc::heddle::v1::HostedRole, ProtocolError> {
523 use grpc::heddle::v1::HostedRole;
524 match value.trim().to_ascii_lowercase().as_str() {
525 "reader" => Ok(HostedRole::Reader),
526 "developer" => Ok(HostedRole::Developer),
527 "maintainer" => Ok(HostedRole::Maintainer),
528 "admin" => Ok(HostedRole::Admin),
529 "owner" => Ok(HostedRole::Owner),
530 other => Err(ProtocolError::InvalidState(format!(
531 "invalid role '{other}': expected reader|developer|maintainer|admin|owner"
532 ))),
533 }
534}