use std::sync::Arc;
use arrow::array::{ArrayRef, BinaryBuilder, StringBuilder, UInt64Builder};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use arrow::error::ArrowError;
use arrow::record_batch::RecordBatch;
use polyc_eventlog::Event;
const SIGNATURE_VERIFIED: &str = "verified";
const SIGNATURE_INVALID: &str = "invalid";
#[derive(Debug, Clone)]
pub(crate) struct GrantReplayRow {
pub partition: String,
pub position: u64,
pub turn_id: Option<String>,
pub tool: String,
pub grant_ref: String,
pub covered_capabilities: String,
pub coverage_hash: String,
pub signature_status: String,
pub signer_public_key: Option<Vec<u8>>,
}
#[must_use]
pub(crate) fn schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("partition", DataType::Utf8, false),
Field::new("position", DataType::UInt64, false),
Field::new("turn_id", DataType::Utf8, true),
Field::new("tool", DataType::Utf8, false),
Field::new("grant_ref", DataType::Utf8, false),
Field::new("covered_capabilities", DataType::Utf8, false),
Field::new("coverage_hash", DataType::Utf8, false),
Field::new("signature_status", DataType::Utf8, false),
Field::new("signer_public_key", DataType::Binary, true),
]))
}
pub(crate) fn decode_grant_replays_batch(
rows: &[GrantReplayRow],
) -> Result<RecordBatch, ArrowError> {
let mut partition_b = StringBuilder::with_capacity(rows.len(), rows.len() * 8);
let mut position_b = UInt64Builder::with_capacity(rows.len());
let mut turn_id_b = StringBuilder::with_capacity(rows.len(), rows.len() * 36);
let mut tool_b = StringBuilder::with_capacity(rows.len(), rows.len() * 16);
let mut grant_ref_b = StringBuilder::with_capacity(rows.len(), rows.len() * 16);
let mut covered_capabilities_b = StringBuilder::with_capacity(rows.len(), rows.len() * 32);
let mut coverage_hash_b = StringBuilder::with_capacity(rows.len(), rows.len() * 16);
let mut signature_status_b = StringBuilder::with_capacity(rows.len(), rows.len() * 8);
let mut signer_public_key_b = BinaryBuilder::with_capacity(rows.len(), rows.len() * 32);
for row in rows {
partition_b.append_value(&row.partition);
position_b.append_value(row.position);
match &row.turn_id {
Some(id) => turn_id_b.append_value(id),
None => turn_id_b.append_null(),
}
tool_b.append_value(&row.tool);
grant_ref_b.append_value(&row.grant_ref);
covered_capabilities_b.append_value(&row.covered_capabilities);
coverage_hash_b.append_value(&row.coverage_hash);
signature_status_b.append_value(&row.signature_status);
match &row.signer_public_key {
Some(v) => signer_public_key_b.append_value(v),
None => signer_public_key_b.append_null(),
}
}
let columns: Vec<ArrayRef> = vec![
Arc::new(partition_b.finish()),
Arc::new(position_b.finish()),
Arc::new(turn_id_b.finish()),
Arc::new(tool_b.finish()),
Arc::new(grant_ref_b.finish()),
Arc::new(covered_capabilities_b.finish()),
Arc::new(coverage_hash_b.finish()),
Arc::new(signature_status_b.finish()),
Arc::new(signer_public_key_b.finish()),
];
RecordBatch::try_new(schema(), columns)
}
const fn signature_status_str(status: polyc_facts::GrantReplaySignatureStatus) -> &'static str {
match status {
polyc_facts::GrantReplaySignatureStatus::Verified => SIGNATURE_VERIFIED,
polyc_facts::GrantReplaySignatureStatus::Invalid => SIGNATURE_INVALID,
}
}
#[must_use]
pub(crate) fn decode_grant_replays_events(
partition: &str,
events: &[(u64, Event)],
trusted_signers: &[Vec<u8>],
) -> Vec<GrantReplayRow> {
events
.iter()
.filter_map(|(position, event)| {
let (_base, turn_id) = polyc_proto::kinds::parse(&event.kind);
let turn_id = turn_id.map(|id| id.to_string());
let fact = polyc_facts::fold_grant_replay_event(event, trusted_signers)?;
let covered_capabilities =
serde_json::to_string(&fact.covered_capabilities).unwrap_or_default();
Some(GrantReplayRow {
partition: partition.to_string(),
position: *position,
turn_id,
tool: fact.tool,
grant_ref: fact.grant_ref,
covered_capabilities,
coverage_hash: fact.coverage_hash,
signature_status: signature_status_str(fact.signature_status).to_string(),
signer_public_key: fact.signer_public_key,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use arrow::array::Array as _;
use polyc_crypto::approval::{ApprovalSigner, test_util::grant_replay_payload};
use polyc_proto::kinds;
use uuid::Uuid;
use super::*;
#[test]
fn schema_shape() {
let schema = schema();
let names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
assert_eq!(
names,
vec![
"partition",
"position",
"turn_id",
"tool",
"grant_ref",
"covered_capabilities",
"coverage_hash",
"signature_status",
"signer_public_key",
]
);
let expect = [
("partition", DataType::Utf8, false),
("position", DataType::UInt64, false),
("turn_id", DataType::Utf8, true),
("tool", DataType::Utf8, false),
("grant_ref", DataType::Utf8, false),
("covered_capabilities", DataType::Utf8, false),
("coverage_hash", DataType::Utf8, false),
("signature_status", DataType::Utf8, false),
("signer_public_key", DataType::Binary, true),
];
for (field, (name, ty, nullable)) in schema.fields().iter().zip(expect) {
assert_eq!(field.name(), name);
assert_eq!(field.data_type(), &ty);
assert_eq!(field.is_nullable(), nullable);
}
}
#[test]
fn decode_round_trips_a_real_signed_grant_replay() {
let turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_5555);
let signer = ApprovalSigner::from_seed(1);
let (payload, _sig, _pk) = grant_replay_payload(
"conv-rt",
&turn.to_string(),
"read_file",
"grant-ref-rt",
&["fs.read".to_owned(), "fs.list".to_owned()],
"sha256:coverage-rt",
&signer,
);
let events = vec![(
1,
Event::new(kinds::tagged(kinds::GRANT_REPLAY, &turn), payload),
)];
let trusted_signers = vec![signer.public_key_bytes()];
let decoded = decode_grant_replays_events("conv-rt", &events, &trusted_signers);
assert_eq!(decoded.len(), 1);
let row = &decoded[0];
assert_eq!(row.turn_id, Some(turn.to_string()));
assert_eq!(row.tool, "read_file");
assert_eq!(row.grant_ref, "grant-ref-rt");
assert_eq!(row.covered_capabilities, r#"["fs.read","fs.list"]"#);
assert_eq!(row.coverage_hash, "sha256:coverage-rt");
assert_eq!(row.signature_status, "verified");
assert_eq!(row.signer_public_key, Some(signer.public_key_bytes()));
let batch = decode_grant_replays_batch(&decoded).expect("batch build");
assert_eq!(batch.num_rows(), 1);
assert_eq!(batch.schema(), schema());
let tool = batch
.column(3)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(tool.value(0), "read_file");
}
#[test]
fn signature_status_trusted_signer_verifies_and_row_is_present() {
let signer = ApprovalSigner::from_seed(2);
let (payload, _sig, _pk) = grant_replay_payload(
"conv-a",
"turn-a",
"read_file",
"grant-ref-a",
&["fs.read".to_owned()],
"sha256:coverage-a",
&signer,
);
let events = vec![(1, Event::new(kinds::GRANT_REPLAY.to_owned(), payload))];
let trusted_signers = vec![signer.public_key_bytes()];
let decoded = decode_grant_replays_events("conv-a", &events, &trusted_signers);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].signature_status, "verified");
}
#[test]
fn signature_status_untrusted_signer_is_invalid_but_row_stays_present() {
let trusted = ApprovalSigner::from_seed(3);
let untrusted = ApprovalSigner::from_seed(4);
let (payload, _sig, _pk) = grant_replay_payload(
"conv-b",
"turn-b",
"read_file",
"grant-ref-b",
&["fs.read".to_owned()],
"sha256:coverage-b",
&untrusted,
);
let events = vec![(1, Event::new(kinds::GRANT_REPLAY.to_owned(), payload))];
let trusted_signers = vec![trusted.public_key_bytes()];
let decoded = decode_grant_replays_events("conv-b", &events, &trusted_signers);
assert_eq!(
decoded.len(),
1,
"an untrusted-signer record must still surface as a row, unlike payments"
);
assert_eq!(decoded[0].signature_status, "invalid");
assert_eq!(decoded[0].tool, "read_file", "claimed fields still shown");
}
#[test]
fn structurally_malformed_record_drops_the_row() {
let events = vec![(
1,
Event::new(kinds::GRANT_REPLAY.to_owned(), vec![0xFF, 0xFE, 0xFD]),
)];
let decoded = decode_grant_replays_events("conv-c", &events, &[]);
assert_eq!(decoded.len(), 0);
}
#[test]
fn unrelated_kind_is_not_decoded_as_a_grant_replay() {
let events = vec![(1, Event::new(kinds::USAGE.to_owned(), Vec::new()))];
let decoded = decode_grant_replays_events("conv-d", &events, &[]);
assert_eq!(decoded.len(), 0);
}
}