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:
parent
fd77a4c323
commit
d91477cfa1
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue