1use guts_storage::ObjectId;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct SerializablePublicKey(pub String);
13
14impl SerializablePublicKey {
15 pub fn from_hex(hex: impl Into<String>) -> Self {
17 Self(hex.into())
18 }
19
20 pub fn from_pubkey(pk: &commonware_cryptography::ed25519::PublicKey) -> Self {
22 Self(hex::encode(pk.as_ref()))
23 }
24
25 pub fn to_pubkey(
27 &self,
28 ) -> Result<commonware_cryptography::ed25519::PublicKey, hex::FromHexError> {
29 let bytes = hex::decode(&self.0)?;
30 if bytes.len() != 32 {
31 return Err(hex::FromHexError::InvalidStringLength);
32 }
33 let mut arr = [0u8; 32];
34 arr.copy_from_slice(&bytes);
35 let vk = ed25519_consensus::VerificationKey::try_from(arr)
37 .map_err(|_| hex::FromHexError::InvalidStringLength)?;
38 Ok(commonware_cryptography::ed25519::PublicKey::from(vk))
39 }
40
41 pub fn as_hex(&self) -> &str {
43 &self.0
44 }
45
46 pub fn to_hex(&self) -> String {
48 self.0.clone()
49 }
50}
51
52impl std::fmt::Display for SerializablePublicKey {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}", self.0)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct SerializableSignature(pub String);
61
62impl SerializableSignature {
63 pub fn from_hex(hex: impl Into<String>) -> Self {
65 Self(hex.into())
66 }
67
68 pub fn from_signature(sig: &commonware_cryptography::ed25519::Signature) -> Self {
70 Self(hex::encode(sig.as_ref()))
71 }
72
73 pub fn to_signature(
75 &self,
76 ) -> Result<commonware_cryptography::ed25519::Signature, hex::FromHexError> {
77 let bytes = hex::decode(&self.0)?;
78 if bytes.len() != 64 {
79 return Err(hex::FromHexError::InvalidStringLength);
80 }
81 let mut arr = [0u8; 64];
82 arr.copy_from_slice(&bytes);
83 let sig = ed25519_consensus::Signature::from(arr);
85 Ok(commonware_cryptography::ed25519::Signature::from(sig))
86 }
87
88 pub fn as_hex(&self) -> &str {
90 &self.0
91 }
92}
93
94impl std::fmt::Display for SerializableSignature {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 write!(f, "{}", self.0)
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102pub struct PullRequestUpdate {
103 pub title: Option<String>,
105 pub description: Option<String>,
107 pub state: Option<String>,
109 pub labels_add: Vec<String>,
111 pub labels_remove: Vec<String>,
113 pub merged_by: Option<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct IssueUpdate {
120 pub title: Option<String>,
122 pub description: Option<String>,
124 pub state: Option<String>,
126 pub labels_add: Vec<String>,
128 pub labels_remove: Vec<String>,
130 pub closed_by: Option<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
136pub struct OrgUpdate {
137 pub display_name: Option<String>,
139 pub description: Option<String>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
145pub enum CommentTargetSpec {
146 PullRequest { repo_key: String, number: u32 },
148 Issue { repo_key: String, number: u32 },
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct BranchProtectionSpec {
155 pub require_pr: bool,
157 pub required_reviews: u32,
159 pub require_signed_commits: bool,
161 pub allow_force_push: bool,
163 pub allow_deletions: bool,
165 pub required_status_checks: Vec<String>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171pub struct TeamSpec {
172 pub name: String,
174 pub description: Option<String>,
176 pub permission: String,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182pub struct TransactionId([u8; 32]);
183
184impl TransactionId {
185 pub fn from_bytes(bytes: [u8; 32]) -> Self {
187 Self(bytes)
188 }
189
190 pub fn as_bytes(&self) -> &[u8; 32] {
192 &self.0
193 }
194
195 pub fn to_hex(&self) -> String {
197 hex::encode(self.0)
198 }
199
200 pub fn from_hex(hex_str: &str) -> Result<Self, hex::FromHexError> {
202 let mut bytes = [0u8; 32];
203 hex::decode_to_slice(hex_str, &mut bytes)?;
204 Ok(Self(bytes))
205 }
206}
207
208impl std::fmt::Display for TransactionId {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 write!(f, "{}", self.to_hex())
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
219pub enum Transaction {
220 GitPush {
223 repo_key: String,
225 ref_name: String,
227 old_oid: ObjectId,
229 new_oid: ObjectId,
231 objects: Vec<ObjectId>,
233 signer: SerializablePublicKey,
235 signature: SerializableSignature,
237 },
238
239 CreateRepository {
242 owner: String,
244 name: String,
246 description: String,
248 default_branch: String,
250 visibility: String,
252 creator: SerializablePublicKey,
254 signature: SerializableSignature,
256 },
257
258 DeleteRepository {
260 repo_key: String,
262 deleter: SerializablePublicKey,
264 signature: SerializableSignature,
266 },
267
268 CreatePullRequest {
271 repo_key: String,
273 title: String,
275 description: String,
277 author: String,
279 source_branch: String,
281 target_branch: String,
283 source_commit: ObjectId,
285 target_commit: ObjectId,
287 signer: SerializablePublicKey,
289 signature: SerializableSignature,
291 },
292
293 UpdatePullRequest {
295 repo_key: String,
297 pr_number: u32,
299 update: PullRequestUpdate,
301 signer: SerializablePublicKey,
303 signature: SerializableSignature,
305 },
306
307 MergePullRequest {
309 repo_key: String,
311 pr_number: u32,
313 merge_commit: ObjectId,
315 merged_by: String,
317 signer: SerializablePublicKey,
319 signature: SerializableSignature,
321 },
322
323 CreateIssue {
326 repo_key: String,
328 title: String,
330 description: String,
332 author: String,
334 signer: SerializablePublicKey,
336 signature: SerializableSignature,
338 },
339
340 UpdateIssue {
342 repo_key: String,
344 issue_number: u32,
346 update: IssueUpdate,
348 signer: SerializablePublicKey,
350 signature: SerializableSignature,
352 },
353
354 CreateComment {
357 target: CommentTargetSpec,
359 author: String,
361 body: String,
363 signer: SerializablePublicKey,
365 signature: SerializableSignature,
367 },
368
369 CreateReview {
371 repo_key: String,
373 pr_number: u32,
375 author: String,
377 state: String,
379 body: Option<String>,
381 commit_id: String,
383 signer: SerializablePublicKey,
385 signature: SerializableSignature,
387 },
388
389 CreateOrganization {
392 name: String,
394 display_name: String,
396 creator: SerializablePublicKey,
398 signature: SerializableSignature,
400 },
401
402 UpdateOrganization {
404 org_name: String,
406 update: OrgUpdate,
408 signer: SerializablePublicKey,
410 signature: SerializableSignature,
412 },
413
414 AddOrgMember {
416 org_name: String,
418 member: SerializablePublicKey,
420 role: String,
422 signer: SerializablePublicKey,
424 signature: SerializableSignature,
426 },
427
428 RemoveOrgMember {
430 org_name: String,
432 member: SerializablePublicKey,
434 signer: SerializablePublicKey,
436 signature: SerializableSignature,
438 },
439
440 CreateTeam {
443 org_name: String,
445 team: TeamSpec,
447 signer: SerializablePublicKey,
449 signature: SerializableSignature,
451 },
452
453 DeleteTeam {
455 org_name: String,
457 team_name: String,
459 signer: SerializablePublicKey,
461 signature: SerializableSignature,
463 },
464
465 AddTeamMember {
467 org_name: String,
469 team_name: String,
471 member: SerializablePublicKey,
473 signer: SerializablePublicKey,
475 signature: SerializableSignature,
477 },
478
479 RemoveTeamMember {
481 org_name: String,
483 team_name: String,
485 member: SerializablePublicKey,
487 signer: SerializablePublicKey,
489 signature: SerializableSignature,
491 },
492
493 AddTeamRepo {
495 org_name: String,
497 team_name: String,
499 repo_key: String,
501 signer: SerializablePublicKey,
503 signature: SerializableSignature,
505 },
506
507 SetCollaborator {
510 repo_key: String,
512 collaborator: String,
514 permission: String,
516 signer: SerializablePublicKey,
518 signature: SerializableSignature,
520 },
521
522 RemoveCollaborator {
524 repo_key: String,
526 collaborator: String,
528 signer: SerializablePublicKey,
530 signature: SerializableSignature,
532 },
533
534 SetBranchProtection {
537 repo_key: String,
539 branch: String,
541 protection: BranchProtectionSpec,
543 signer: SerializablePublicKey,
545 signature: SerializableSignature,
547 },
548
549 RemoveBranchProtection {
551 repo_key: String,
553 branch: String,
555 signer: SerializablePublicKey,
557 signature: SerializableSignature,
559 },
560}
561
562impl Transaction {
563 pub fn id(&self) -> TransactionId {
565 let bytes = serde_json::to_vec(self).expect("transaction serialization should not fail");
566 let mut hasher = Sha256::new();
567 hasher.update(&bytes);
568 let result = hasher.finalize();
569 let mut id = [0u8; 32];
570 id.copy_from_slice(&result);
571 TransactionId(id)
572 }
573
574 pub fn signer(&self) -> &SerializablePublicKey {
576 match self {
577 Transaction::GitPush { signer, .. } => signer,
578 Transaction::CreateRepository { creator, .. } => creator,
579 Transaction::DeleteRepository { deleter, .. } => deleter,
580 Transaction::CreatePullRequest { signer, .. } => signer,
581 Transaction::UpdatePullRequest { signer, .. } => signer,
582 Transaction::MergePullRequest { signer, .. } => signer,
583 Transaction::CreateIssue { signer, .. } => signer,
584 Transaction::UpdateIssue { signer, .. } => signer,
585 Transaction::CreateComment { signer, .. } => signer,
586 Transaction::CreateReview { signer, .. } => signer,
587 Transaction::CreateOrganization { creator, .. } => creator,
588 Transaction::UpdateOrganization { signer, .. } => signer,
589 Transaction::AddOrgMember { signer, .. } => signer,
590 Transaction::RemoveOrgMember { signer, .. } => signer,
591 Transaction::CreateTeam { signer, .. } => signer,
592 Transaction::DeleteTeam { signer, .. } => signer,
593 Transaction::AddTeamMember { signer, .. } => signer,
594 Transaction::RemoveTeamMember { signer, .. } => signer,
595 Transaction::AddTeamRepo { signer, .. } => signer,
596 Transaction::SetCollaborator { signer, .. } => signer,
597 Transaction::RemoveCollaborator { signer, .. } => signer,
598 Transaction::SetBranchProtection { signer, .. } => signer,
599 Transaction::RemoveBranchProtection { signer, .. } => signer,
600 }
601 }
602
603 pub fn signature(&self) -> &SerializableSignature {
605 match self {
606 Transaction::GitPush { signature, .. } => signature,
607 Transaction::CreateRepository { signature, .. } => signature,
608 Transaction::DeleteRepository { signature, .. } => signature,
609 Transaction::CreatePullRequest { signature, .. } => signature,
610 Transaction::UpdatePullRequest { signature, .. } => signature,
611 Transaction::MergePullRequest { signature, .. } => signature,
612 Transaction::CreateIssue { signature, .. } => signature,
613 Transaction::UpdateIssue { signature, .. } => signature,
614 Transaction::CreateComment { signature, .. } => signature,
615 Transaction::CreateReview { signature, .. } => signature,
616 Transaction::CreateOrganization { signature, .. } => signature,
617 Transaction::UpdateOrganization { signature, .. } => signature,
618 Transaction::AddOrgMember { signature, .. } => signature,
619 Transaction::RemoveOrgMember { signature, .. } => signature,
620 Transaction::CreateTeam { signature, .. } => signature,
621 Transaction::DeleteTeam { signature, .. } => signature,
622 Transaction::AddTeamMember { signature, .. } => signature,
623 Transaction::RemoveTeamMember { signature, .. } => signature,
624 Transaction::AddTeamRepo { signature, .. } => signature,
625 Transaction::SetCollaborator { signature, .. } => signature,
626 Transaction::RemoveCollaborator { signature, .. } => signature,
627 Transaction::SetBranchProtection { signature, .. } => signature,
628 Transaction::RemoveBranchProtection { signature, .. } => signature,
629 }
630 }
631
632 pub fn repo_key(&self) -> Option<&str> {
634 match self {
635 Transaction::GitPush { repo_key, .. } => Some(repo_key),
636 Transaction::CreateRepository { .. } => None, Transaction::DeleteRepository { repo_key, .. } => Some(repo_key),
638 Transaction::CreatePullRequest { repo_key, .. } => Some(repo_key),
639 Transaction::UpdatePullRequest { repo_key, .. } => Some(repo_key),
640 Transaction::MergePullRequest { repo_key, .. } => Some(repo_key),
641 Transaction::CreateIssue { repo_key, .. } => Some(repo_key),
642 Transaction::UpdateIssue { repo_key, .. } => Some(repo_key),
643 Transaction::CreateComment { target, .. } => match target {
644 CommentTargetSpec::PullRequest { repo_key, .. } => Some(repo_key),
645 CommentTargetSpec::Issue { repo_key, .. } => Some(repo_key),
646 },
647 Transaction::CreateReview { repo_key, .. } => Some(repo_key),
648 Transaction::SetCollaborator { repo_key, .. } => Some(repo_key),
649 Transaction::RemoveCollaborator { repo_key, .. } => Some(repo_key),
650 Transaction::SetBranchProtection { repo_key, .. } => Some(repo_key),
651 Transaction::RemoveBranchProtection { repo_key, .. } => Some(repo_key),
652 Transaction::AddTeamRepo { repo_key, .. } => Some(repo_key),
653 _ => None, }
655 }
656
657 pub fn kind(&self) -> &'static str {
659 match self {
660 Transaction::GitPush { .. } => "git_push",
661 Transaction::CreateRepository { .. } => "create_repository",
662 Transaction::DeleteRepository { .. } => "delete_repository",
663 Transaction::CreatePullRequest { .. } => "create_pull_request",
664 Transaction::UpdatePullRequest { .. } => "update_pull_request",
665 Transaction::MergePullRequest { .. } => "merge_pull_request",
666 Transaction::CreateIssue { .. } => "create_issue",
667 Transaction::UpdateIssue { .. } => "update_issue",
668 Transaction::CreateComment { .. } => "create_comment",
669 Transaction::CreateReview { .. } => "create_review",
670 Transaction::CreateOrganization { .. } => "create_organization",
671 Transaction::UpdateOrganization { .. } => "update_organization",
672 Transaction::AddOrgMember { .. } => "add_org_member",
673 Transaction::RemoveOrgMember { .. } => "remove_org_member",
674 Transaction::CreateTeam { .. } => "create_team",
675 Transaction::DeleteTeam { .. } => "delete_team",
676 Transaction::AddTeamMember { .. } => "add_team_member",
677 Transaction::RemoveTeamMember { .. } => "remove_team_member",
678 Transaction::AddTeamRepo { .. } => "add_team_repo",
679 Transaction::SetCollaborator { .. } => "set_collaborator",
680 Transaction::RemoveCollaborator { .. } => "remove_collaborator",
681 Transaction::SetBranchProtection { .. } => "set_branch_protection",
682 Transaction::RemoveBranchProtection { .. } => "remove_branch_protection",
683 }
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use commonware_cryptography::{ed25519, PrivateKeyExt, Signer};
691
692 fn test_keypair() -> (SerializablePublicKey, SerializableSignature) {
693 let key = ed25519::PrivateKey::from_seed(42);
694 let sig = key.sign(Some(b"_GUTS"), b"test");
695 (
696 SerializablePublicKey::from_pubkey(&key.public_key()),
697 SerializableSignature::from_signature(&sig),
698 )
699 }
700
701 #[test]
702 fn test_transaction_id_roundtrip() {
703 let bytes = [0xab; 32];
704 let id = TransactionId::from_bytes(bytes);
705 assert_eq!(id.as_bytes(), &bytes);
706
707 let hex = id.to_hex();
708 let parsed = TransactionId::from_hex(&hex).unwrap();
709 assert_eq!(id, parsed);
710 }
711
712 #[test]
713 fn test_transaction_id_display() {
714 let id = TransactionId::from_bytes([0; 32]);
715 assert_eq!(format!("{}", id), "0".repeat(64));
716 }
717
718 #[test]
719 fn test_transaction_kind() {
720 let (signer, signature) = test_keypair();
721
722 let tx = Transaction::CreateRepository {
723 owner: "alice".into(),
724 name: "test".into(),
725 description: "A test repo".into(),
726 default_branch: "main".into(),
727 visibility: "public".into(),
728 creator: signer,
729 signature,
730 };
731
732 assert_eq!(tx.kind(), "create_repository");
733 }
734
735 #[test]
736 fn test_transaction_id_unique() {
737 let (signer, signature) = test_keypair();
738
739 let tx1 = Transaction::CreateRepository {
740 owner: "alice".into(),
741 name: "test1".into(),
742 description: "A test repo".into(),
743 default_branch: "main".into(),
744 visibility: "public".into(),
745 creator: signer.clone(),
746 signature: signature.clone(),
747 };
748
749 let tx2 = Transaction::CreateRepository {
750 owner: "alice".into(),
751 name: "test2".into(), description: "A test repo".into(),
753 default_branch: "main".into(),
754 visibility: "public".into(),
755 creator: signer,
756 signature,
757 };
758
759 assert_ne!(tx1.id(), tx2.id());
760 }
761
762 #[test]
763 fn test_serializable_pubkey_roundtrip() {
764 let key = ed25519::PrivateKey::from_seed(42);
765 let pk = key.public_key();
766 let serializable = SerializablePublicKey::from_pubkey(&pk);
767 let recovered = serializable.to_pubkey().unwrap();
768 assert_eq!(pk, recovered);
769 }
770
771 #[test]
772 fn test_serializable_signature_roundtrip() {
773 let key = ed25519::PrivateKey::from_seed(42);
774 let sig = key.sign(Some(b"_GUTS"), b"test");
775 let serializable = SerializableSignature::from_signature(&sig);
776 let recovered = serializable.to_signature().unwrap();
777 assert_eq!(sig, recovered);
778 }
779
780 #[test]
781 fn test_transaction_serialization() {
782 let (signer, signature) = test_keypair();
783
784 let tx = Transaction::CreateRepository {
785 owner: "alice".into(),
786 name: "test".into(),
787 description: "A test repo".into(),
788 default_branch: "main".into(),
789 visibility: "public".into(),
790 creator: signer,
791 signature,
792 };
793
794 let json = serde_json::to_string(&tx).unwrap();
795 let parsed: Transaction = serde_json::from_str(&json).unwrap();
796 assert_eq!(tx.id(), parsed.id());
797 }
798}