1use core::fmt;
5use std::sync::Arc;
6
7use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
8use buffa::Message as _;
9use bytes::Bytes;
10use connectrpc::{
11 ConnectError, InboundStream, RequestContext, Response, ServiceRequest, ServiceResult,
12 ServiceStream,
13};
14use futures::{StreamExt, stream};
15use mkit_core::hash::Hash;
16use mkit_core::protocol::{AdvanceOutcome, PackKey};
17
18use super::error::recorded;
19use super::proto::mkit::transport::v1::__buffa::oneof::begin_upload_response::Result as BeginResult;
20use super::proto::mkit::transport::v1::__buffa::oneof::download_pack_response::Body as DownloadBody;
21use super::proto::mkit::transport::v1::__buffa::oneof::issue_object_url_request::Target;
22use super::proto::mkit::transport::v1::__buffa::oneof::set_repo_visibility_request::Mode as VisibilityMode;
23use super::proto::mkit::transport::v1::__buffa::oneof::upload_pack_request::Body as UploadBody;
24use super::proto::mkit::transport::v1::__buffa::oneof::upload_part_request::Msg as PartMsg;
25use super::proto::mkit::transport::v1::{
26 AdvanceOutcome as WireOutcome, AdvanceRefsRequest, AdvanceRefsResponse, BeginUploadRequest,
27 BeginUploadResponse, CompleteUploadRequest, CompleteUploadResponse, DownloadPackHeader,
28 DownloadPackRequest, DownloadPackResponse, GetReceiptRequest, GetReceiptResponse,
29 GetServerInfoRequest, GetServerInfoResponse, ListRefsRequest, ListRefsResponse, PackChunk,
30 PackExistsRequest, PackExistsResponse, ReadRefRequest, ReadRefResponse, RefEntry,
31 RefExpectation, TransportService, UpdateRefRequest, UpdateRefResponse, UploadPackRequest,
32 UploadPackResponse, UploadPartRequest, UploadPartResponse, UploadTicket,
33};
34use super::proto::mkit::transport::v1::{
35 GetAuthorityGenerationRequest, GetAuthorityGenerationResponse, GetGrantEpochRequest,
36 GetGrantEpochResponse, IssueObjectUrlRequest, IssueObjectUrlResponse, RepoVisibility,
37 SetAuthorityGenerationRequest, SetAuthorityGenerationResponse, SetGrantEpochRequest,
38 SetGrantEpochResponse, SetRepoVisibilityRequest, SetRepoVisibilityResponse,
39};
40use super::{Shared, authenticated};
41use crate::error::ServerError;
42use crate::op::RefUpdate;
43use crate::pipeline::{
44 Authenticated, DownloadChunk, HookSet, Pipeline, ServerInfo, VisibilityRequest,
45};
46use crate::refs::{DigestField, UnusedExpectedId, condition_from_wire, hash_from_slice};
47use crate::replay::{BeginUploadResult, UpdateRefResult};
48use crate::rt::{send_wrap, send_wrap_stream};
49use crate::store::{MultipartBlobStore, NamespaceStore};
50use crate::upload::UploadError;
51use crate::url_token::UrlTarget;
52
53pub struct ConnectTransport<B, N, H> {
58 pipe: Shared<Pipeline<B, N, H>>,
59}
60
61impl<B, N, H> fmt::Debug for ConnectTransport<B, N, H> {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.debug_struct("ConnectTransport").finish_non_exhaustive()
64 }
65}
66
67impl<B, N, H> ConnectTransport<B, N, H> {
68 #[must_use]
70 pub fn new(pipeline: Arc<Pipeline<B, N, H>>) -> Self {
71 Self {
72 pipe: Shared::new(pipeline),
73 }
74 }
75}
76
77fn ref_update(
80 name: Option<String>,
81 expectation: Option<buffa::EnumValue<RefExpectation>>,
82 expected_id: Option<&[u8]>,
83 new_id: Option<&[u8]>,
84 delete: bool,
85) -> Result<RefUpdate, ServerError> {
86 let expectation = expectation.map_or(0, |e| e.to_i32());
87 let expected_id = expected_id.unwrap_or_default();
88 let condition = condition_from_wire(expectation, expected_id, UnusedExpectedId::Reject)?;
89 let new = if delete {
90 if !matches!(condition, mkit_core::refs::RefWriteCondition::Match(_))
91 || !new_id.unwrap_or_default().is_empty()
92 {
93 return Err(ServerError::invalid_argument(
94 "delete requires MATCH and an empty new_id",
95 ));
96 }
97 None
98 } else {
99 Some(hash_from_slice(DigestField::NewId, new_id)?)
100 };
101 Ok(RefUpdate {
102 name: name.unwrap_or_default(),
103 condition,
104 new,
105 })
106}
107
108fn not_yet() -> ServerError {
109 ServerError::unimplemented("not implemented yet")
110}
111
112fn pack_key(pack_id: Option<&[u8]>) -> Result<PackKey, ServerError> {
113 Ok(PackKey::new(hash_from_slice(DigestField::PackId, pack_id)?))
114}
115
116fn download_message(body: DownloadBody) -> DownloadPackResponse {
117 DownloadPackResponse {
118 body: Some(body),
119 ..Default::default()
120 }
121}
122
123fn chunk_message(pack_id: &[u8], chunk: &DownloadChunk) -> DownloadPackResponse {
124 download_message(DownloadBody::Chunk(Box::new(PackChunk {
125 pack_id: Some(pack_id.to_vec()),
126 offset: Some(chunk.offset),
127 data: Some(chunk.data.to_vec()),
128 last: Some(chunk.last),
129 ..Default::default()
130 })))
131}
132
133async fn upload<B: MultipartBlobStore, N: NamespaceStore, H: HookSet>(
138 pipe: &Pipeline<B, N, H>,
139 a: &Authenticated,
140 mut requests: InboundStream<UploadPackRequest>,
141) -> Result<(), ConnectError> {
142 let header = match requests.next().await.transpose()? {
143 None => Err(UploadError::HeaderMissing { stream_empty: true }),
144 Some(first) => match first.to_owned_message().body {
145 Some(UploadBody::Header(header)) => Ok(header),
146 _ => Err(UploadError::HeaderMissing {
147 stream_empty: false,
148 }),
149 },
150 };
151 let header = header.map_err(ServerError::from)?;
152 let token = header.ticket_token.as_deref().unwrap_or_default();
153 let mut session = if token.is_empty() {
154 pipe.open_upload(a, header.pack_id.as_deref(), header.total_bytes)
155 .await?
156 } else {
157 pipe.open_ticketed_upload(a, header.pack_id.as_deref(), header.total_bytes, token)
158 .await?
159 };
160 while let Some(item) = requests.next().await {
161 let chunk = match item.map(|m| m.to_owned_message().body) {
162 Ok(Some(UploadBody::Chunk(chunk))) => *chunk,
163 Ok(body) => {
165 let err = ServerError::from(UploadError::UnexpectedMessage {
166 header: body.is_some(),
167 });
168 session.abort_with(&err).await;
169 return Err(err.into());
170 }
171 Err(e) => {
172 session.abort_with(&recorded(&e)).await;
173 return Err(e);
174 }
175 };
176 let data = Bytes::from(chunk.data.unwrap_or_default());
177 let last = chunk.last.unwrap_or(false);
178 match session
179 .push(chunk.pack_id.as_deref(), chunk.offset, data, last)
180 .await
181 {
182 Ok(false) => {}
183 Ok(true) => break,
184 Err(e) => {
186 session.abort_with(&e).await;
187 return Err(e.into());
188 }
189 }
190 }
191 session.finish().await?;
192 Ok(())
193}
194
195async fn upload_part<B: MultipartBlobStore, N: NamespaceStore, H: HookSet>(
198 pipe: &Pipeline<B, N, H>,
199 a: &Authenticated,
200 mut requests: InboundStream<UploadPartRequest>,
201) -> Result<Vec<u8>, ConnectError> {
202 let first = match requests.next().await.transpose() {
203 Ok(first) => first,
204 Err(err) => {
205 pipe.record_part_error(a, &recorded(&err));
206 return Err(err);
207 }
208 };
209 let header = match first {
210 None => Err(UploadError::HeaderMissing { stream_empty: true }),
211 Some(first) => match first.to_owned_message().msg {
212 Some(PartMsg::Header(header)) => Ok(*header),
213 _ => Err(UploadError::HeaderMissing {
214 stream_empty: false,
215 }),
216 },
217 }
218 .map_err(|error| {
219 ServerError::invalid_argument(error.connect_message().replace("UploadPack", "UploadPart"))
220 });
221 let header = match header {
222 Ok(header) => header,
223 Err(err) => {
224 pipe.record_part_error(a, &err);
225 return Err(err.into());
226 }
227 };
228 let mut session = pipe
229 .open_part(
230 a,
231 header.ticket_token.as_deref().unwrap_or_default(),
232 header.index.unwrap_or_default(),
233 )
234 .await?;
235 while let Some(item) = requests.next().await {
236 let chunk = match item {
237 Ok(message) => match message.to_owned_message().msg {
238 Some(PartMsg::Chunk(chunk)) => chunk,
239 Some(PartMsg::Header(_)) => {
240 let err =
241 ServerError::invalid_argument("UploadPart: saw a second `header` message");
242 session.abort_with(&err).await;
243 return Err(err.into());
244 }
245 None => {
246 let err = ServerError::invalid_argument(
247 "UploadPart: message with neither `header` nor `chunk` set",
248 );
249 session.abort_with(&err).await;
250 return Err(err.into());
251 }
252 },
253 Err(err) => {
254 session.abort_with(&recorded(&err)).await;
255 return Err(err);
256 }
257 };
258 if let Err(err) = session.push(Bytes::from(chunk)).await {
259 session.abort_with(&err).await;
260 return Err(err.into());
261 }
262 }
263 session.finish().await.map_err(Into::into)
264}
265
266#[allow(refining_impl_trait)]
267impl<B, N, H> TransportService for ConnectTransport<B, N, H>
268where
269 B: MultipartBlobStore + 'static,
270 N: NamespaceStore + 'static,
271 H: HookSet + 'static,
272{
273 async fn list_repos(
274 &self,
275 ctx: RequestContext,
276 request: ServiceRequest<'_, super::proto::mkit::transport::v1::ListReposRequest>,
277 ) -> ServiceResult<super::proto::mkit::transport::v1::ListReposResponse> {
278 let a = authenticated(&ctx)?;
279 let m = request.to_owned_message();
280 let pipe = self.pipe.arc();
281 send_wrap(async move {
282 let token = m
283 .page_token
284 .as_deref()
285 .filter(|s| !s.is_empty())
286 .map(|s| {
287 if s.len() > 512 {
288 return Err(ServerError::invalid_argument(
289 "invalid repository page token",
290 ));
291 }
292 URL_SAFE_NO_PAD
293 .decode(s)
294 .map_err(|_| ServerError::invalid_argument("invalid repository page token"))
295 })
296 .transpose()?;
297 let page = pipe
298 .list_repos_page(
299 &a,
300 m.namespace.as_deref().unwrap_or_default(),
301 m.name_prefix.as_deref().unwrap_or_default(),
302 m.page_size,
303 token.as_deref(),
304 )
305 .await?;
306 let response = super::proto::mkit::transport::v1::ListReposResponse {
307 repos: page
308 .repos
309 .into_iter()
310 .map(|entry| super::proto::mkit::transport::v1::RepoEntry {
311 name: Some(entry.name),
312 visibility: Some(
313 if entry.visibility == crate::pipeline::RepoVisibility::Public {
314 RepoVisibility::REPO_VISIBILITY_PUBLIC
315 } else {
316 RepoVisibility::REPO_VISIBILITY_PRIVATE
317 }
318 .into(),
319 ),
320 ..Default::default()
321 })
322 .collect(),
323 next_page_token: page.next.map(|bytes| URL_SAFE_NO_PAD.encode(bytes)),
324 ..Default::default()
325 };
326 if response.encoded_len() > 64 * 1024 {
327 return Err(ServerError::unavailable("repository listing unavailable").into());
328 }
329 Response::ok(response)
330 })
331 .await
332 }
333
334 async fn list_refs(
335 &self,
336 ctx: RequestContext,
337 request: ServiceRequest<'_, ListRefsRequest>,
338 ) -> ServiceResult<ListRefsResponse> {
339 let a = authenticated(&ctx)?;
340 let m = request.to_owned_message();
341 let prefix = m.prefix.unwrap_or_default();
342 let pipe = self.pipe.arc();
343 send_wrap(async move {
344 let token = m
345 .page_token
346 .as_deref()
347 .filter(|t| !t.is_empty())
348 .map(|t| {
349 if t.len() > 730 {
350 return Err(ServerError::invalid_argument("invalid page token"));
351 }
352 URL_SAFE_NO_PAD
353 .decode(t)
354 .map_err(|_| ServerError::invalid_argument("invalid page token"))
355 })
356 .transpose()
357 .map_err(ConnectError::from)?;
358 let page = pipe
359 .list_refs_page(&a, &prefix, m.page_size, token.as_deref())
360 .await?;
361 let refs = page.refs.into_iter().map(|entry| RefEntry {
362 name: Some(entry.name),
363 object_id: Some(entry.id.to_vec()),
364 ..Default::default()
365 });
366 let response = ListRefsResponse {
367 refs: refs.collect(),
368 next_page_token: page.next.map(|bytes| URL_SAFE_NO_PAD.encode(bytes)),
369 ..Default::default()
370 };
371 if response.encoded_len() > 2 * 1024 * 1024 {
372 return Err(ServerError::unavailable("ref listing unavailable").into());
373 }
374 Response::ok(response)
375 })
376 .await
377 }
378
379 async fn read_ref(
380 &self,
381 ctx: RequestContext,
382 request: ServiceRequest<'_, ReadRefRequest>,
383 ) -> ServiceResult<ReadRefResponse> {
384 let a = authenticated(&ctx)?;
385 let name = request.to_owned_message().name.unwrap_or_default();
386 let pipe = self.pipe.arc();
387 send_wrap(async move {
388 let id = pipe.read_ref(&a, &name).await?;
389 Response::ok(ReadRefResponse {
390 exists: Some(id.is_some()),
391 object_id: Some(id.map(|id| id.to_vec()).unwrap_or_default()),
392 ..Default::default()
393 })
394 })
395 .await
396 }
397
398 async fn update_ref(
399 &self,
400 ctx: RequestContext,
401 request: ServiceRequest<'_, UpdateRefRequest>,
402 ) -> ServiceResult<UpdateRefResponse> {
403 let a = authenticated(&ctx)?;
404 let m = request.to_owned_message();
405 let upd = ref_update(
406 m.name,
407 m.expectation,
408 m.expected_id.as_deref(),
409 m.new_id.as_deref(),
410 m.delete.unwrap_or(false),
411 )?;
412 let pipe = self.pipe.arc();
413 send_wrap(async move {
414 match pipe.update_ref_with_meta(&a, upd).await? {
415 (UpdateRefResult::Committed, meta) => {
416 let mut response = Response::new(UpdateRefResponse::default());
417 for (name, value) in meta.headers() {
418 response = response.with_header(name, value);
419 }
420 Ok(response)
421 }
422 (UpdateRefResult::Conflict { .. }, _) => Err(ServerError::failed_precondition(
425 "ref CAS precondition failed — read_ref to disambiguate",
426 )
427 .into()),
428 }
429 })
430 .await
431 }
432
433 async fn advance_refs(
434 &self,
435 ctx: RequestContext,
436 request: ServiceRequest<'_, AdvanceRefsRequest>,
437 ) -> ServiceResult<AdvanceRefsResponse> {
438 let a = authenticated(&ctx)?;
439 let m = request.to_owned_message();
440 let delete = m.delete.unwrap_or(false);
441 if delete && !m.ticket_ids.is_empty() {
442 return Err(ServerError::invalid_argument("delete consumes no tickets").into());
443 }
444 if m.ticket_ids.len() > crate::store::outbox::MAX_TICKETS_PER_ADVANCE {
445 return Err(ServerError::invalid_argument("too many tickets in one advance").into());
446 }
447 let mut tickets = Vec::with_capacity(m.ticket_ids.len());
448 for raw in &m.ticket_ids {
449 let id: [u8; 32] = raw
450 .as_slice()
451 .try_into()
452 .map_err(|_| ServerError::invalid_argument("ticket id must be 32 bytes"))?;
453 if tickets.contains(&id) {
454 return Err(ServerError::invalid_argument("duplicate ticket id").into());
455 }
456 tickets.push(id);
457 }
458 let head = ref_update(
459 m.head_ref,
460 m.head_expectation,
461 m.head_expected_id.as_deref(),
462 m.head_new_id.as_deref(),
463 delete,
464 )?;
465 let packmap = ref_update(
466 m.packmap_ref,
467 m.packmap_expectation,
468 m.packmap_expected_id.as_deref(),
469 m.packmap_new_id.as_deref(),
470 delete,
471 )?;
472 let pipe = self.pipe.arc();
473 send_wrap(async move {
474 let (outcome, meta) = pipe
476 .advance_refs_with_tickets_with_meta(&a, head, packmap, tickets)
477 .await?;
478 let outcome = match outcome {
479 AdvanceOutcome::Committed => WireOutcome::ADVANCE_OUTCOME_COMMITTED,
480 AdvanceOutcome::HeadConflict => WireOutcome::ADVANCE_OUTCOME_HEAD_CONFLICT,
481 AdvanceOutcome::PackmapConflict => WireOutcome::ADVANCE_OUTCOME_PACKMAP_CONFLICT,
482 };
483 let mut response = Response::new(AdvanceRefsResponse {
484 outcome: Some(outcome.into()),
485 ..Default::default()
486 });
487 for (name, value) in meta.headers() {
488 response = response.with_header(name, value);
489 }
490 Ok(response)
491 })
492 .await
493 }
494
495 async fn pack_exists(
496 &self,
497 ctx: RequestContext,
498 request: ServiceRequest<'_, PackExistsRequest>,
499 ) -> ServiceResult<PackExistsResponse> {
500 let a = authenticated(&ctx)?;
501 let key = pack_key(request.to_owned_message().pack_id.as_deref())?;
502 let pipe = self.pipe.arc();
503 send_wrap(async move {
504 let exists = pipe.pack_exists(&a, key).await?;
505 Response::ok(PackExistsResponse {
506 exists: Some(exists),
507 ..Default::default()
508 })
509 })
510 .await
511 }
512
513 async fn upload_pack(
514 &self,
515 ctx: RequestContext,
516 requests: InboundStream<UploadPackRequest>,
517 ) -> ServiceResult<UploadPackResponse> {
518 let a = authenticated(&ctx)?;
519 let pipe = self.pipe.arc();
522 send_wrap(async move {
523 upload(&pipe, &a, requests).await?;
524 Response::ok(UploadPackResponse::default())
525 })
526 .await
527 }
528
529 async fn download_pack(
530 &self,
531 ctx: RequestContext,
532 request: ServiceRequest<'_, DownloadPackRequest>,
533 ) -> ServiceResult<ServiceStream<DownloadPackResponse>> {
534 let a = authenticated(&ctx)?;
535 let key = pack_key(request.to_owned_message().pack_id.as_deref())?;
536 let pipe = self.pipe.arc();
537 let download = send_wrap(async move { pipe.download(&a, key).await }).await?;
540 let header = download_message(DownloadBody::Header(Box::new(DownloadPackHeader {
541 total_bytes: Some(download.total_bytes),
542 ..Default::default()
543 })));
544 let chunks = download.chunks.map(move |chunk| {
545 chunk
546 .map(|c| chunk_message(&key.0, &c))
547 .map_err(ConnectError::from)
548 });
549 Response::stream_ok(stream::iter([Ok(header)]).chain(send_wrap_stream(chunks)))
550 }
551
552 async fn get_server_info(
553 &self,
554 _ctx: RequestContext,
555 _request: ServiceRequest<'_, GetServerInfoRequest>,
556 ) -> ServiceResult<GetServerInfoResponse> {
557 let info = self.pipe.get().server_info();
560 Ok(Response::new(info.into()).with_header("cache-control", "private, max-age=60"))
561 }
562
563 async fn get_receipt(
564 &self,
565 _ctx: RequestContext,
566 _request: ServiceRequest<'_, GetReceiptRequest>,
567 ) -> ServiceResult<GetReceiptResponse> {
568 Err(not_yet().into())
570 }
571
572 async fn begin_upload(
573 &self,
574 ctx: RequestContext,
575 request: ServiceRequest<'_, BeginUploadRequest>,
576 ) -> ServiceResult<BeginUploadResponse> {
577 let a = authenticated(&ctx)?;
578 let message = request.to_owned_message();
579 let pipe = self.pipe.arc();
580 send_wrap(async move {
581 let (result, meta) = pipe
582 .begin_upload_with_meta(
583 &a,
584 &message.r#ref.unwrap_or_default(),
585 message.pack_id.as_deref().unwrap_or_default(),
586 message.bytes.unwrap_or_default(),
587 )
588 .await?;
589 let result = match result {
590 BeginUploadResult::AlreadyPresent => BeginResult::AlreadyPresent(Box::default()),
591 BeginUploadResult::Ticket {
592 id,
593 part_size,
594 expires_at_ms,
595 token,
596 } => {
597 let expires_unix_ms = i64::try_from(expires_at_ms).map_err(|_| {
598 ServerError::internal(
599 "upload ticket expiry exceeds wire clock",
600 expires_at_ms,
601 )
602 })?;
603 BeginResult::Ticket(Box::new(UploadTicket {
604 id: Some(id.to_vec()),
605 part_size: Some(part_size),
606 expires_unix_ms: Some(expires_unix_ms),
607 token: Some(token),
608 ..Default::default()
609 }))
610 }
611 };
612 let mut response = Response::new(BeginUploadResponse {
613 result: Some(result),
614 ..Default::default()
615 });
616 for (name, value) in meta.headers() {
617 response = response.with_header(name, value);
618 }
619 Ok(response)
620 })
621 .await
622 }
623
624 async fn upload_part(
625 &self,
626 ctx: RequestContext,
627 requests: InboundStream<UploadPartRequest>,
628 ) -> ServiceResult<UploadPartResponse> {
629 let a = authenticated(&ctx)?;
630 let pipe = self.pipe.arc();
631 send_wrap(async move {
632 let receipt = upload_part(&pipe, &a, requests).await?;
633 Response::ok(UploadPartResponse {
634 receipt: Some(receipt),
635 ..Default::default()
636 })
637 })
638 .await
639 }
640
641 async fn complete_upload(
642 &self,
643 ctx: RequestContext,
644 request: ServiceRequest<'_, CompleteUploadRequest>,
645 ) -> ServiceResult<CompleteUploadResponse> {
646 let a = authenticated(&ctx)?;
647 let message = request.to_owned_message();
648 let pipe = self.pipe.arc();
649 send_wrap(async move {
650 pipe.complete_upload(
651 &a,
652 message.ticket_token.as_deref().unwrap_or_default(),
653 &message.receipts,
654 )
655 .await?;
656 Response::ok(CompleteUploadResponse::default())
657 })
658 .await
659 }
660
661 async fn get_grant_epoch(
662 &self,
663 _ctx: RequestContext,
664 request: ServiceRequest<'_, GetGrantEpochRequest>,
665 ) -> ServiceResult<GetGrantEpochResponse> {
666 let message = request.to_owned_message();
668 let pipe = self.pipe.arc();
669 send_wrap(async move {
670 let epoch = pipe
671 .get_grant_epoch(message.namespace.as_deref().unwrap_or_default())
672 .await?;
673 Response::ok(GetGrantEpochResponse {
674 epoch: Some(epoch),
675 ..Default::default()
676 })
677 })
678 .await
679 }
680
681 async fn set_grant_epoch(
682 &self,
683 _ctx: RequestContext,
684 request: ServiceRequest<'_, SetGrantEpochRequest>,
685 ) -> ServiceResult<SetGrantEpochResponse> {
686 let message = request.to_owned_message();
688 let pipe = self.pipe.arc();
689 send_wrap(async move {
690 let epoch = pipe
691 .set_grant_epoch(message.signed_statement.as_deref().unwrap_or_default())
692 .await?;
693 Response::ok(SetGrantEpochResponse {
694 epoch: Some(epoch),
695 ..Default::default()
696 })
697 })
698 .await
699 }
700
701 async fn get_authority_generation(
702 &self,
703 _ctx: RequestContext,
704 request: ServiceRequest<'_, GetAuthorityGenerationRequest>,
705 ) -> ServiceResult<GetAuthorityGenerationResponse> {
706 let message = request.to_owned_message();
708 let pipe = self.pipe.arc();
709 send_wrap(async move {
710 let generation = pipe
711 .get_authority_generation(message.namespace.as_deref().unwrap_or_default())
712 .await?;
713 Response::ok(GetAuthorityGenerationResponse {
714 generation: Some(generation),
715 ..Default::default()
716 })
717 })
718 .await
719 }
720
721 async fn set_authority_generation(
722 &self,
723 _ctx: RequestContext,
724 request: ServiceRequest<'_, SetAuthorityGenerationRequest>,
725 ) -> ServiceResult<SetAuthorityGenerationResponse> {
726 let message = request.to_owned_message();
728 let pipe = self.pipe.arc();
729 send_wrap(async move {
730 let generation = pipe
731 .set_authority_generation(message.signed_statement.as_deref().unwrap_or_default())
732 .await?;
733 Response::ok(SetAuthorityGenerationResponse {
734 generation: Some(generation),
735 ..Default::default()
736 })
737 })
738 .await
739 }
740
741 async fn set_repo_visibility(
742 &self,
743 ctx: RequestContext,
744 request: ServiceRequest<'_, SetRepoVisibilityRequest>,
745 ) -> ServiceResult<SetRepoVisibilityResponse> {
746 let a = authenticated(&ctx)?;
747 let req = match request.to_owned_message().mode {
748 Some(VisibilityMode::Visibility(visibility)) => match visibility.as_known() {
749 Some(RepoVisibility::REPO_VISIBILITY_PUBLIC) => {
750 VisibilityRequest::Envelope(mkit_attest::grant::Visibility::Public)
751 }
752 Some(RepoVisibility::REPO_VISIBILITY_PRIVATE) => {
753 VisibilityRequest::Envelope(mkit_attest::grant::Visibility::Private)
754 }
755 _ => return Err(ServerError::invalid_argument("invalid visibility").into()),
756 },
757 Some(VisibilityMode::SignedStatement(statement)) => {
758 VisibilityRequest::Statement(statement)
759 }
760 None => {
761 return Err(
762 ServerError::invalid_argument("SetRepoVisibility requires a mode").into(),
763 );
764 }
765 };
766 let pipe = self.pipe.arc();
767 send_wrap(async move {
768 pipe.set_repo_visibility(&a, req).await?;
769 Response::ok(SetRepoVisibilityResponse::default())
770 })
771 .await
772 }
773
774 async fn issue_object_url(
775 &self,
776 ctx: RequestContext,
777 request: ServiceRequest<'_, IssueObjectUrlRequest>,
778 ) -> ServiceResult<IssueObjectUrlResponse> {
779 let a = authenticated(&ctx)?;
780 let message = request.to_owned_message();
781 let invalid = || ServerError::invalid_argument("invalid object URL target");
782 let target = match message.target {
783 Some(Target::ObjectId(id)) => {
784 UrlTarget::Object(Hash::try_from(id.as_slice()).map_err(|_| invalid())?)
785 }
786 Some(Target::RefPath(path)) => UrlTarget::path(
787 path.r#ref.unwrap_or_default(),
788 path.path.unwrap_or_default(),
789 )
790 .map_err(|_| invalid())?,
791 None => {
792 return Err(
793 ServerError::invalid_argument("IssueObjectUrl requires a target").into(),
794 );
795 }
796 };
797 let pipe = self.pipe.arc();
798 send_wrap(async move {
799 let minted = pipe
800 .issue_object_url(&a, target, message.ttl_seconds.unwrap_or_default())
801 .await?;
802 Response::ok(IssueObjectUrlResponse {
803 token: Some(minted.expose().to_owned()),
804 expires_unix_ms: Some(minted.expires_at_ms),
805 ..Default::default()
806 })
807 })
808 .await
809 }
810}
811
812impl From<ServerInfo> for GetServerInfoResponse {
813 fn from(info: ServerInfo) -> Self {
814 Self {
815 protocol: Some(info.protocol.into()),
816 spec_version: Some(info.spec_version),
817 max_pack_bytes: Some(info.max_pack_bytes),
818 part_size: Some(info.part_size),
819 max_parts: Some(info.max_parts),
820 max_list_refs_page_size: Some(info.max_list_refs_page_size),
821 begin_upload_threshold_bytes: Some(info.begin_upload_threshold_bytes),
822 atomic_advance: Some(info.atomic_advance),
823 indexed_mode: Some(info.indexed_mode),
824 admission: Some(info.admission),
825 receipt_public_key: Some(info.receipt_public_key),
826 receipt_key_id: Some(info.receipt_key_id),
827 grant_schemes: info.grant_schemes,
828 namespace_policy: Some(info.namespace_policy.into()),
829 index_fanout: Some(info.index_fanout),
830 max_delta_chain_depth: Some(info.max_delta_chain_depth),
831 leases: Some(false), async_inspection: Some(false),
834 inspection_max_objects: info.inspection_max_objects,
835 __buffa_unknown_fields: buffa::UnknownFields::default(),
836 }
837 }
838}
839
840#[cfg(test)]
841mod proto_roundtrip {
842 use super::super::proto::mkit::transport::v1::__buffa::oneof::{
843 begin_upload_response::Result as BeginResult, upload_part_request::Msg as PartMsg,
844 };
845 use super::super::proto::mkit::transport::v1::*;
846 use super::ServerInfo;
847 use buffa::Message;
848
849 fn roundtrip<M: Message + PartialEq + core::fmt::Debug>(message: &M) {
850 let encoded = message.encode_to_vec();
851 assert_eq!(
852 &M::decode_from_slice(&encoded).expect("decode generated message"),
853 message
854 );
855 }
856
857 fn ticket() -> UploadTicket {
858 UploadTicket {
859 id: Some(vec![0x12; 32]),
860 part_size: Some(8 << 20),
861 expires_unix_ms: Some(1_700_000_000_000),
862 token: Some(vec![0xa1, 0xb2]),
863 ..Default::default()
864 }
865 }
866
867 #[test]
868 fn admission_challenge_roundtrips_with_zero_one_and_eight_entries() {
869 for count in [0, 1, 8] {
870 let message = AdmissionChallenge {
871 challenges: (0..count)
872 .map(|index| Challenge {
873 scheme: Some(format!("scheme{index}")),
874 value: Some(format!("opaque-{index}")),
875 ..Default::default()
876 })
877 .collect(),
878 description: (count != 0).then(|| "Admission required".into()),
879 ..Default::default()
880 };
881 roundtrip(&message);
882 let json = serde_json::to_vec(&message).expect("serialize challenge");
883 assert_eq!(
884 serde_json::from_slice::<AdmissionChallenge>(&json).expect("parse challenge"),
885 message
886 );
887 }
888 }
889
890 #[test]
891 fn discovery_messages_roundtrip() {
892 roundtrip(&GetServerInfoRequest::default());
894 roundtrip(&GetServerInfoResponse {
895 protocol: Some("mkit.transport.v1".into()),
896 spec_version: Some(2),
897 max_pack_bytes: Some(1 << 34),
898 part_size: Some(8 << 20),
899 max_parts: Some(1024),
900 max_list_refs_page_size: Some(512),
901 begin_upload_threshold_bytes: Some(8 << 20),
902 atomic_advance: Some(true),
903 indexed_mode: Some(true),
904 admission: Some(true),
905 receipt_public_key: Some(vec![0x34; 32]),
906 receipt_key_id: Some("receipt-key".into()),
907 grant_schemes: vec!["ed25519".into(), "secp256k1-eip191".into()],
908 namespace_policy: Some("allowlist".into()),
909 index_fanout: Some(4096),
910 inspection_max_objects: Some(10_000),
911 ..Default::default()
912 });
913 }
914
915 #[test]
916 fn discovery_explicitly_denies_unimplemented_lease_and_async_capabilities() {
917 for indexed_mode in [false, true] {
918 let response = GetServerInfoResponse::from(ServerInfo {
919 protocol: "mkit.transport.v1",
920 spec_version: 2,
921 max_pack_bytes: 1 << 30,
922 part_size: 8 << 20,
923 max_parts: 128,
924 max_list_refs_page_size: 512,
925 begin_upload_threshold_bytes: 0,
926 atomic_advance: false,
927 indexed_mode,
928 admission: false,
929 receipt_public_key: Vec::new(),
930 receipt_key_id: String::new(),
931 grant_schemes: Vec::new(),
932 namespace_policy: "allowlist",
933 index_fanout: 4096,
934 max_delta_chain_depth: if indexed_mode { 50 } else { 0 },
935 inspection_max_objects: None,
936 });
937 roundtrip(&response);
938 let json = serde_json::to_value(&response).unwrap();
939 assert_eq!(json["leases"], false);
940 assert_eq!(json["asyncInspection"], false);
941 assert_eq!(json["indexedMode"], indexed_mode);
942 assert!(response.receipt_public_key.unwrap().is_empty());
943 assert!(response.receipt_key_id.unwrap().is_empty());
944 }
945 }
946
947 #[test]
948 fn inspection_limit_roundtrips_and_is_absent_without_inspection() {
949 for limit in [None, Some(10_000)] {
950 let response = GetServerInfoResponse {
951 inspection_max_objects: limit,
952 ..Default::default()
953 };
954 roundtrip(&response);
955 let json = serde_json::to_value(&response).unwrap();
956 assert_eq!(
957 json.get("inspectionMaxObjects"),
958 limit.map(serde_json::Value::from).as_ref()
959 );
960 assert_eq!(
961 serde_json::from_value::<GetServerInfoResponse>(json).unwrap(),
962 response
963 );
964 }
965 }
966
967 #[test]
968 fn ticket_messages_and_both_results_roundtrip() {
969 roundtrip(&BeginUploadRequest {
970 r#ref: Some("refs/heads/main".into()),
971 pack_id: Some(vec![0x56; 32]),
972 bytes: Some(1 << 33),
973 ..Default::default()
974 });
975 roundtrip(&AlreadyPresent::default());
976 roundtrip(&ticket());
977 roundtrip(&BeginUploadResponse {
978 result: Some(BeginResult::AlreadyPresent(Box::default())),
979 ..Default::default()
980 });
981 roundtrip(&BeginUploadResponse {
982 result: Some(BeginResult::Ticket(Box::new(ticket()))),
983 ..Default::default()
984 });
985 }
986
987 #[test]
988 fn part_messages_and_both_stream_alternatives_roundtrip() {
989 let header = UploadPartHeader {
990 ticket_token: Some(vec![0x78, 0x9a]),
991 index: Some(3),
992 ..Default::default()
993 };
994 roundtrip(&header);
995 roundtrip(&UploadPartRequest {
996 msg: Some(PartMsg::Header(Box::new(header))),
997 ..Default::default()
998 });
999 roundtrip(&UploadPartRequest {
1000 msg: Some(PartMsg::Chunk(vec![0x01, 0x23, 0x45])),
1001 ..Default::default()
1002 });
1003 roundtrip(&UploadPartResponse {
1004 receipt: Some(vec![0xab, 0xcd]),
1005 ..Default::default()
1006 });
1007 roundtrip(&CompleteUploadRequest {
1008 ticket_token: Some(vec![0x78, 0x9a]),
1009 receipts: vec![vec![0xab, 0xcd], vec![0xef, 0x01]],
1010 ..Default::default()
1011 });
1012 roundtrip(&CompleteUploadResponse::default());
1013 }
1014
1015 #[test]
1016 fn m2_messages_roundtrip() {
1017 use super::super::proto::mkit::transport::v1::__buffa::oneof::issue_object_url_request::Target;
1018 use super::super::proto::mkit::transport::v1::__buffa::oneof::set_repo_visibility_request::Mode;
1019 use super::super::proto::mkit::transport::v1::{RefPath, RepoVisibility};
1020
1021 roundtrip(&GetGrantEpochRequest {
1022 namespace: Some("namespace".into()),
1023 ..Default::default()
1024 });
1025 roundtrip(&GetGrantEpochResponse {
1026 epoch: Some(42),
1027 ..Default::default()
1028 });
1029 roundtrip(&SetGrantEpochRequest {
1030 signed_statement: Some("statement.scheme.blob".into()),
1031 ..Default::default()
1032 });
1033 roundtrip(&SetGrantEpochResponse {
1034 epoch: Some(43),
1035 ..Default::default()
1036 });
1037 for visibility in [
1038 RepoVisibility::Unspecified,
1039 RepoVisibility::Public,
1040 RepoVisibility::Private,
1041 ] {
1042 roundtrip(&SetRepoVisibilityRequest {
1043 mode: Some(Mode::Visibility(visibility.into())),
1044 ..Default::default()
1045 });
1046 }
1047 roundtrip(&SetRepoVisibilityRequest {
1048 mode: Some(Mode::SignedStatement("statement.scheme.blob".into())),
1049 ..Default::default()
1050 });
1051 roundtrip(&SetRepoVisibilityResponse::default());
1052 roundtrip(&IssueObjectUrlRequest {
1053 target: Some(Target::ObjectId(vec![0x42; 32])),
1054 ttl_seconds: Some(15),
1055 ..Default::default()
1056 });
1057 let ref_path = RefPath {
1058 r#ref: Some("refs/heads/main".into()),
1059 path: Some("dir/file".into()),
1060 ..Default::default()
1061 };
1062 roundtrip(&ref_path);
1063 roundtrip(&IssueObjectUrlRequest {
1064 target: Some(Target::RefPath(Box::new(ref_path))),
1065 ttl_seconds: Some(20),
1066 ..Default::default()
1067 });
1068 roundtrip(&IssueObjectUrlResponse {
1069 token: Some("signed.token".into()),
1070 expires_unix_ms: Some(1_700_000_000_000),
1071 ..Default::default()
1072 });
1073 }
1074
1075 #[test]
1076 fn additive_fields_roundtrip() {
1077 roundtrip(&ListRefsRequest {
1078 prefix: Some("refs/heads/".into()),
1079 page_size: Some(1),
1080 page_token: Some("continuation".into()),
1081 ..Default::default()
1082 });
1083 roundtrip(&ListRefsResponse {
1084 refs: vec![RefEntry {
1085 name: Some("main".into()),
1086 object_id: Some(vec![0x23; 32]),
1087 ..Default::default()
1088 }],
1089 next_page_token: Some("next".into()),
1090 ..Default::default()
1091 });
1092 roundtrip(&UpdateRefRequest {
1093 delete: Some(true),
1094 ..Default::default()
1095 });
1096 roundtrip(&AdvanceRefsRequest {
1097 ticket_ids: vec![vec![0x45; 32], vec![0x67; 32]],
1098 delete: Some(true),
1099 ..Default::default()
1100 });
1101 roundtrip(&UploadPackHeader {
1102 ticket_token: Some(vec![0x89, 0xab]),
1103 ..Default::default()
1104 });
1105 }
1106}