use buffa::EnumValue;
use polyc_proto::proto::polychrome::state::v1 as pb;
use polyc_state::{
ceremonies::{
CeremonyClassification, CeremonyCommand, CeremonyExpiration, CeremonyExpirationShard,
CeremonyId, CeremonyKind, CeremonyOperation, CeremonyRecord, CeremonyStatus,
ceremony_scope,
},
command::{CommandEnvelope, CommandMetadata, ResourceBounds},
digest::ContentDigest,
error::StateError,
id::{Audience, CommandId, NamespaceId, Purpose},
revision::Revision,
versioned::{EntryExpectation, MAX_MUTATIONS_PER_TRANSACTION, MAX_TRANSACTION_PAYLOAD_BYTES},
};
use crate::wire::{fixed_bytes, malformed, required};
fn expected_to_wire(value: EntryExpectation) -> pb::CeremonyExpectedEntry {
use pb::__buffa::oneof::ceremony_expected_entry::Expected;
let expected = match value {
EntryExpectation::Absent => Expected::from(pb::CeremonyExpectedAbsent {
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
EntryExpectation::Revision(revision) => Expected::from(pb::CeremonyExpectedRevision {
revision: revision.get(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
};
pb::CeremonyExpectedEntry {
expected: Some(expected),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
fn expected_from_wire(value: pb::CeremonyExpectedEntry) -> Result<EntryExpectation, StateError> {
use pb::__buffa::oneof::ceremony_expected_entry::Expected;
match value.expected {
Some(Expected::Absent(_)) => Ok(EntryExpectation::Absent),
Some(Expected::Revision(value)) => {
Ok(EntryExpectation::Revision(Revision::new(value.revision)))
}
None => Err(malformed(
"expected",
"a ceremony operation declares its exact row premise",
)),
}
}
const fn kind_to_wire(value: CeremonyKind) -> pb::CeremonyKind {
match value {
CeremonyKind::PasskeyLogin => pb::CeremonyKind::PasskeyLogin,
CeremonyKind::WalletLogin => pb::CeremonyKind::WalletLogin,
CeremonyKind::EmailLink => pb::CeremonyKind::EmailLink,
CeremonyKind::PersonaCredential => pb::CeremonyKind::PersonaCredential,
CeremonyKind::WalletSignupPending => pb::CeremonyKind::WalletSignupPending,
CeremonyKind::WalletSignupVerified => pb::CeremonyKind::WalletSignupVerified,
CeremonyKind::WalletLink => pb::CeremonyKind::WalletLink,
CeremonyKind::WalletRevoke => pb::CeremonyKind::WalletRevoke,
CeremonyKind::WalletLimitUpdate => pb::CeremonyKind::WalletLimitUpdate,
}
}
fn kind_from_wire(value: EnumValue<pb::CeremonyKind>) -> Result<CeremonyKind, StateError> {
match value {
EnumValue::Known(pb::CeremonyKind::PasskeyLogin) => Ok(CeremonyKind::PasskeyLogin),
EnumValue::Known(pb::CeremonyKind::WalletLogin) => Ok(CeremonyKind::WalletLogin),
EnumValue::Known(pb::CeremonyKind::EmailLink) => Ok(CeremonyKind::EmailLink),
EnumValue::Known(pb::CeremonyKind::PersonaCredential) => {
Ok(CeremonyKind::PersonaCredential)
}
EnumValue::Known(pb::CeremonyKind::WalletSignupPending) => {
Ok(CeremonyKind::WalletSignupPending)
}
EnumValue::Known(pb::CeremonyKind::WalletSignupVerified) => {
Ok(CeremonyKind::WalletSignupVerified)
}
EnumValue::Known(pb::CeremonyKind::WalletLink) => Ok(CeremonyKind::WalletLink),
EnumValue::Known(pb::CeremonyKind::WalletRevoke) => Ok(CeremonyKind::WalletRevoke),
EnumValue::Known(pb::CeremonyKind::WalletLimitUpdate) => {
Ok(CeremonyKind::WalletLimitUpdate)
}
EnumValue::Known(pb::CeremonyKind::Unspecified) | EnumValue::Unknown(_) => {
Err(malformed("kind", "a ceremony record names one known kind"))
}
}
}
const fn classification_to_wire(value: CeremonyClassification) -> pb::CeremonyClassification {
match value {
CeremonyClassification::Internal => pb::CeremonyClassification::Internal,
CeremonyClassification::Confidential => pb::CeremonyClassification::Confidential,
}
}
fn classification_from_wire(
value: EnumValue<pb::CeremonyClassification>,
) -> Result<CeremonyClassification, StateError> {
match value {
EnumValue::Known(pb::CeremonyClassification::Internal) => {
Ok(CeremonyClassification::Internal)
}
EnumValue::Known(pb::CeremonyClassification::Confidential) => {
Ok(CeremonyClassification::Confidential)
}
EnumValue::Known(pb::CeremonyClassification::Unspecified) | EnumValue::Unknown(_) => Err(
malformed("classification", "a ceremony record names one known class"),
),
}
}
fn status_to_wire(value: &CeremonyStatus) -> pb::CeremonyStatus {
use pb::__buffa::oneof::ceremony_status::Status;
let status = match value {
CeremonyStatus::Active => Status::from(pb::CeremonyActive {
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyStatus::Claimed {
attempt_id,
fence,
claim_until_ms,
} => Status::from(pb::CeremonyClaimed {
attempt_id: attempt_id.clone(),
fence: *fence,
claim_until_ms: *claim_until_ms,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyStatus::Redeemed { at_ms } => Status::from(pb::CeremonyRedeemed {
at_ms: *at_ms,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyStatus::Expired { at_ms } => Status::from(pb::CeremonyExpired {
at_ms: *at_ms,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
};
pb::CeremonyStatus {
status: Some(status),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
fn status_from_wire(value: pb::CeremonyStatus) -> Result<CeremonyStatus, StateError> {
use pb::__buffa::oneof::ceremony_status::Status;
match value.status {
Some(Status::Active(_)) => Ok(CeremonyStatus::Active),
Some(Status::Claimed(value)) => Ok(CeremonyStatus::Claimed {
attempt_id: value.attempt_id,
fence: value.fence,
claim_until_ms: value.claim_until_ms,
}),
Some(Status::Redeemed(value)) => Ok(CeremonyStatus::Redeemed { at_ms: value.at_ms }),
Some(Status::Expired(value)) => Ok(CeremonyStatus::Expired { at_ms: value.at_ms }),
None => Err(malformed(
"status",
"a ceremony record names one lifecycle state",
)),
}
}
pub(crate) fn record_to_wire(value: &CeremonyRecord) -> pb::StateCeremonyRecord {
pb::StateCeremonyRecord {
ceremony_id: value.id().as_str().to_owned(),
kind: EnumValue::Known(kind_to_wire(value.kind())),
schema_version: value.schema_version(),
classification: EnumValue::Known(classification_to_wire(value.classification())),
payload: value.payload().to_vec(),
payload_digest: value.payload_digest().as_bytes().to_vec(),
created_at_ms: value.created_at_ms(),
expires_at_ms: value.expires_at_ms(),
claim_fence: value.claim_fence(),
status: buffa::MessageField::some(status_to_wire(value.status())),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
pub(crate) fn record_from_wire(
value: pb::StateCeremonyRecord,
) -> Result<CeremonyRecord, StateError> {
Ok(CeremonyRecord::from_parts(
CeremonyId::new(value.ceremony_id),
kind_from_wire(value.kind)?,
value.schema_version,
classification_from_wire(value.classification)?,
value.payload,
ContentDigest::from_bytes(fixed_bytes::<{ ContentDigest::LEN }>(
"payload_digest",
&value.payload_digest,
)?),
value.created_at_ms,
value.expires_at_ms,
value.claim_fence,
status_from_wire(required(
"status",
"a ceremony record carries its lifecycle state",
value.status,
)?)?,
))
}
pub(crate) fn expiration_to_wire(value: &CeremonyExpirationShard) -> pb::CeremonyExpirationShard {
pb::CeremonyExpirationShard {
entries: value
.entries()
.iter()
.map(|entry| pb::CeremonyExpirationEntry {
expires_at_ms: entry.expires_at_ms(),
ceremony_id: entry.id().as_str().to_owned(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
})
.collect(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
pub(crate) fn expiration_from_wire(value: pb::CeremonyExpirationShard) -> CeremonyExpirationShard {
CeremonyExpirationShard::new(
value
.entries
.into_iter()
.map(|entry| {
CeremonyExpiration::new(entry.expires_at_ms, CeremonyId::new(entry.ceremony_id))
})
.collect(),
)
}
pub(crate) fn operation_to_wire(value: &CeremonyOperation) -> pb::CeremonyOperation {
use pb::__buffa::oneof::ceremony_operation::Operation;
let operation = match value {
CeremonyOperation::Mint {
record,
record_expected,
expiration,
expiration_expected,
} => Operation::from(pb::MintCeremony {
record: buffa::MessageField::some(record_to_wire(record)),
record_expected: buffa::MessageField::some(expected_to_wire(*record_expected)),
expiration: buffa::MessageField::some(expiration_to_wire(expiration)),
expiration_expected: buffa::MessageField::some(expected_to_wire(*expiration_expected)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyOperation::Redeem {
at_ms,
record,
record_expected,
expiration,
expiration_expected,
} => Operation::from(pb::RedeemCeremony {
at_ms: *at_ms,
record: buffa::MessageField::some(record_to_wire(record)),
record_expected: buffa::MessageField::some(expected_to_wire(*record_expected)),
expiration: buffa::MessageField::some(expiration_to_wire(expiration)),
expiration_expected: buffa::MessageField::some(expected_to_wire(*expiration_expected)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyOperation::Claim {
now_ms,
attempt_id,
fence,
claim_until_ms,
record,
record_expected,
} => Operation::from(pb::ClaimCeremony {
now_ms: *now_ms,
attempt_id: attempt_id.clone(),
fence: *fence,
claim_until_ms: *claim_until_ms,
record: buffa::MessageField::some(record_to_wire(record)),
record_expected: buffa::MessageField::some(expected_to_wire(*record_expected)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyOperation::Release {
at_ms,
attempt_id,
fence,
record,
record_expected,
} => Operation::from(pb::ReleaseCeremony {
at_ms: *at_ms,
attempt_id: attempt_id.clone(),
fence: *fence,
record: buffa::MessageField::some(record_to_wire(record)),
record_expected: buffa::MessageField::some(expected_to_wire(*record_expected)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyOperation::Complete {
at_ms,
attempt_id,
fence,
record,
record_expected,
expiration,
expiration_expected,
} => Operation::from(pb::CompleteCeremony {
at_ms: *at_ms,
attempt_id: attempt_id.clone(),
fence: *fence,
record: buffa::MessageField::some(record_to_wire(record)),
record_expected: buffa::MessageField::some(expected_to_wire(*record_expected)),
expiration: buffa::MessageField::some(expiration_to_wire(expiration)),
expiration_expected: buffa::MessageField::some(expected_to_wire(*expiration_expected)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
CeremonyOperation::Reap {
at_ms,
record,
record_expected,
expiration,
expiration_expected,
} => Operation::from(pb::ReapCeremony {
at_ms: *at_ms,
record: buffa::MessageField::some(record_to_wire(record)),
record_expected: buffa::MessageField::some(expected_to_wire(*record_expected)),
expiration: buffa::MessageField::some(expiration_to_wire(expiration)),
expiration_expected: buffa::MessageField::some(expected_to_wire(*expiration_expected)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
};
pb::CeremonyOperation {
operation: Some(operation),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
fn operation_from_wire(value: pb::CeremonyOperation) -> Result<CeremonyOperation, StateError> {
use pb::__buffa::oneof::ceremony_operation::Operation;
let record = |value| {
required("record", "a ceremony operation carries its record", value)
.and_then(record_from_wire)
};
let expected = |field, value| {
expected_from_wire(required(
field,
"a ceremony operation carries its premise",
value,
)?)
};
let expiration = |value| {
Ok(expiration_from_wire(required(
"expiration",
"a ceremony operation carries its expiry shard",
value,
)?))
};
match value.operation {
Some(Operation::Mint(value)) => Ok(CeremonyOperation::Mint {
record: record(value.record)?,
record_expected: expected("record_expected", value.record_expected)?,
expiration: expiration(value.expiration)?,
expiration_expected: expected("expiration_expected", value.expiration_expected)?,
}),
Some(Operation::Redeem(value)) => Ok(CeremonyOperation::Redeem {
at_ms: value.at_ms,
record: record(value.record)?,
record_expected: expected("record_expected", value.record_expected)?,
expiration: expiration(value.expiration)?,
expiration_expected: expected("expiration_expected", value.expiration_expected)?,
}),
Some(Operation::Claim(value)) => Ok(CeremonyOperation::Claim {
now_ms: value.now_ms,
attempt_id: value.attempt_id,
fence: value.fence,
claim_until_ms: value.claim_until_ms,
record: record(value.record)?,
record_expected: expected("record_expected", value.record_expected)?,
}),
Some(Operation::Release(value)) => Ok(CeremonyOperation::Release {
at_ms: value.at_ms,
attempt_id: value.attempt_id,
fence: value.fence,
record: record(value.record)?,
record_expected: expected("record_expected", value.record_expected)?,
}),
Some(Operation::Complete(value)) => Ok(CeremonyOperation::Complete {
at_ms: value.at_ms,
attempt_id: value.attempt_id,
fence: value.fence,
record: record(value.record)?,
record_expected: expected("record_expected", value.record_expected)?,
expiration: expiration(value.expiration)?,
expiration_expected: expected("expiration_expected", value.expiration_expected)?,
}),
Some(Operation::Reap(value)) => Ok(CeremonyOperation::Reap {
at_ms: value.at_ms,
record: record(value.record)?,
record_expected: expected("record_expected", value.record_expected)?,
expiration: expiration(value.expiration)?,
expiration_expected: expected("expiration_expected", value.expiration_expected)?,
}),
None => Err(malformed(
"operation",
"a ceremony command names one operation",
)),
}
}
pub(crate) fn metadata_to_wire(command: &CeremonyCommand) -> pb::CeremonyCommandMetadata {
let value = command.metadata();
pb::CeremonyCommandMetadata {
command_id: value.command_id().as_str().to_owned(),
namespace: value.scope().namespace().as_str().to_owned(),
purpose: value.envelope().purpose().as_str().to_owned(),
command_audience: value.envelope().audience().as_str().to_owned(),
digest: value.digest().as_bytes().to_vec(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
pub(crate) fn command_from_wire(
metadata: pb::CeremonyCommandMetadata,
operation: pb::CeremonyOperation,
) -> Result<CeremonyCommand, StateError> {
let namespace = NamespaceId::new(metadata.namespace);
let digest = ContentDigest::from_bytes(fixed_bytes::<{ ContentDigest::LEN }>(
"digest",
&metadata.digest,
)?);
Ok(CeremonyCommand::new(
CommandMetadata::new(
CommandId::new(metadata.command_id),
polyc_state::versioned::family(),
digest,
ceremony_scope(&namespace),
CommandEnvelope::new(
Purpose::new(metadata.purpose),
Audience::new(metadata.command_audience),
ResourceBounds::new(MAX_TRANSACTION_PAYLOAD_BYTES, MAX_MUTATIONS_PER_TRANSACTION),
),
),
operation_from_wire(operation)?,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_operation_and_unknown_enums_fail_closed() {
assert!(operation_from_wire(pb::CeremonyOperation::default()).is_err());
let mut record = record_to_wire(&CeremonyRecord::active(
CeremonyId::new("id"),
CeremonyKind::EmailLink,
1,
CeremonyClassification::Confidential,
vec![1],
ContentDigest::from_bytes([2; ContentDigest::LEN]),
1,
2,
));
record.kind = EnumValue::Unknown(999);
assert!(record_from_wire(record).is_err());
}
#[test]
fn the_retired_persona_credential_kind_still_round_trips_both_ways() {
assert_eq!(
kind_to_wire(CeremonyKind::PersonaCredential),
pb::CeremonyKind::PersonaCredential
);
assert_eq!(
kind_from_wire(EnumValue::Known(pb::CeremonyKind::PersonaCredential))
.expect("a durably recorded retired kind decodes"),
CeremonyKind::PersonaCredential
);
}
}