From d91477cfa11b506b203edc35897d6c2658a7635d Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Wed, 29 Apr 2026 00:32:53 -0400 Subject: [PATCH] feat(domain): NewSecret and CredentialVersion structs CredentialVersion adds a revoked? predicate so callers don't need to nil-check revoked_at directly. Both structs are immutable value types. --- .../unit/domain/credential_version_spec.cr | 51 +++++++++++++++++++ .../spec/unit/domain/new_secret_spec.cr | 29 +++++++++++ .../src/cre/domain/credential_version.cr | 37 ++++++++++++++ .../src/cre/domain/new_secret.cr | 19 +++++++ 4 files changed, 136 insertions(+) create mode 100644 PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/credential_version_spec.cr create mode 100644 PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/new_secret_spec.cr create mode 100644 PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/credential_version.cr create mode 100644 PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/new_secret.cr diff --git a/PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/credential_version_spec.cr b/PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/credential_version_spec.cr new file mode 100644 index 00000000..a032c645 --- /dev/null +++ b/PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/credential_version_spec.cr @@ -0,0 +1,51 @@ +# =================== +# ©AngelaMos | 2026 +# credential_version_spec.cr +# =================== + +require "../../spec_helper" +require "../../../src/cre/domain/credential_version" + +describe CRE::Domain::CredentialVersion do + it "constructs with all fields" do + v = CRE::Domain::CredentialVersion.new( + id: UUID.random, + credential_id: UUID.random, + ciphertext: "x".to_slice, + dek_wrapped: "y".to_slice, + kek_version: 1, + algorithm_id: 1_i16, + metadata: {} of String => String, + ) + v.kek_version.should eq 1 + v.algorithm_id.should eq 1_i16 + v.revoked_at.should be_nil + end + + it "reports revocation" do + revoked_at = Time.utc - 1.hour + v = CRE::Domain::CredentialVersion.new( + id: UUID.random, + credential_id: UUID.random, + ciphertext: Bytes.new(0), + dek_wrapped: Bytes.new(0), + kek_version: 1, + algorithm_id: 1_i16, + revoked_at: revoked_at, + ) + v.revoked?.should be_true + v.revoked_at.should eq revoked_at + end + + it "is not revoked by default" do + v = CRE::Domain::CredentialVersion.new( + id: UUID.random, + credential_id: UUID.random, + ciphertext: Bytes.new(0), + dek_wrapped: Bytes.new(0), + kek_version: 1, + algorithm_id: 1_i16, + ) + v.revoked?.should be_false + end +end diff --git a/PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/new_secret_spec.cr b/PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/new_secret_spec.cr new file mode 100644 index 00000000..a20635ad --- /dev/null +++ b/PROJECTS/intermediate/credential-rotation-enforcer/spec/unit/domain/new_secret_spec.cr @@ -0,0 +1,29 @@ +# =================== +# ©AngelaMos | 2026 +# new_secret_spec.cr +# =================== + +require "../../spec_helper" +require "../../../src/cre/domain/new_secret" + +describe CRE::Domain::NewSecret do + it "wraps ciphertext + metadata + timestamp" do + s = CRE::Domain::NewSecret.new( + ciphertext: "abc".to_slice, + metadata: {"version_id" => "v123"}, + ) + s.ciphertext.should eq "abc".to_slice + s.metadata["version_id"].should eq "v123" + s.generated_at.should be_close(Time.utc, 5.seconds) + end + + it "defaults metadata to empty hash" do + s = CRE::Domain::NewSecret.new(ciphertext: Bytes.new(8)) + s.metadata.should be_empty + end + + it "ciphertext is exposed as Bytes" do + s = CRE::Domain::NewSecret.new(ciphertext: Bytes[1, 2, 3]) + s.ciphertext.size.should eq 3 + end +end diff --git a/PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/credential_version.cr b/PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/credential_version.cr new file mode 100644 index 00000000..3c256c2b --- /dev/null +++ b/PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/credential_version.cr @@ -0,0 +1,37 @@ +# =================== +# ©AngelaMos | 2026 +# credential_version.cr +# =================== + +require "uuid" + +module CRE::Domain + struct CredentialVersion + getter id : UUID + getter credential_id : UUID + getter ciphertext : Bytes + getter dek_wrapped : Bytes + getter kek_version : Int32 + getter algorithm_id : Int16 + getter metadata : Hash(String, String) + getter generated_at : Time + getter revoked_at : Time? + + def initialize( + @id : UUID, + @credential_id : UUID, + @ciphertext : Bytes, + @dek_wrapped : Bytes, + @kek_version : Int32, + @algorithm_id : Int16, + @metadata : Hash(String, String) = {} of String => String, + @generated_at : Time = Time.utc, + @revoked_at : Time? = nil, + ) + end + + def revoked? : Bool + !@revoked_at.nil? + end + end +end diff --git a/PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/new_secret.cr b/PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/new_secret.cr new file mode 100644 index 00000000..dd06c2b4 --- /dev/null +++ b/PROJECTS/intermediate/credential-rotation-enforcer/src/cre/domain/new_secret.cr @@ -0,0 +1,19 @@ +# =================== +# ©AngelaMos | 2026 +# new_secret.cr +# =================== + +module CRE::Domain + struct NewSecret + getter ciphertext : Bytes + getter metadata : Hash(String, String) + getter generated_at : Time + + def initialize( + @ciphertext : Bytes, + @metadata : Hash(String, String) = {} of String => String, + @generated_at : Time = Time.utc, + ) + end + end +end