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.
This commit is contained in:
CarterPerez-dev 2026-04-29 00:32:53 -04:00
parent fd77a4c323
commit d91477cfa1
4 changed files with 136 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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