use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::{
ContextSessionStateV1, DecisionId, PackageId, PlanId, PolicyId, ProfileId, ProjectId,
ProtocolReference, ReceiptId, SemanticVersion, SessionIdentityV1, Sha256Digest, SourceId,
TaskId, TenantId, UtcTimestamp, ValidationError, WorkspaceId, deserialize_schema_version,
validate_milliunit, validate_schema_version,
};
pub const CONTEXT_CHECKPOINT_SCHEMA_ID: &str = "leanctx.context-checkpoint/v1";
pub const CONTEXT_CHECKPOINT_V2_SCHEMA_ID: &str = "leanctx.context-checkpoint-live/v2";
pub const CONTEXT_CHECKPOINT_DIGEST_DOMAIN: &[u8] = b"leanctx/context-checkpoint/v1\0";
pub const CONTEXT_CHECKPOINT_IDENTITY_DIGEST_DOMAIN: &[u8] =
b"leanctx/context-checkpoint-identity/v1\0";
pub const CONTEXT_CHECKPOINT_LINEAGE_DIGEST_DOMAIN: &[u8] =
b"leanctx/context-checkpoint-lineage/v1\0";
pub const CONTEXT_CHECKPOINT_LIVE_STATE_DIGEST_DOMAIN: &[u8] =
b"leanctx/context-checkpoint-live-state/v1\0";
pub const CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN: &[u8] = b"leanctx/context-checkpoint-signature/v1\0";
pub const CONTEXT_CHECKPOINT_V2_DIGEST_DOMAIN: &[u8] = b"leanctx/context-checkpoint/v2\0";
pub const CONTEXT_CHECKPOINT_V2_LINEAGE_DIGEST_DOMAIN: &[u8] =
b"leanctx/context-checkpoint-lineage/v2\0";
pub const CONTEXT_CHECKPOINT_V2_SIGNATURE_DOMAIN: &[u8] =
b"leanctx/context-checkpoint-signature/v2\0";
pub const CONTEXT_CHECKPOINT_CARRIER_SCHEMA_ID: &str = "leanctx.context-checkpoint/v2";
pub const CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_SCHEMA_ID: &str = "leanctx.workspace.state/v1";
pub const CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_KEYS: [&str; 7] = [
"entries",
"package_lock_digest",
"package_pins",
"policy",
"schema_version",
"sources",
"workspace_id",
];
pub const CONTEXT_CHECKPOINT_CARRIER_STATE_DIGEST_DOMAIN: &str = "leanctx.workspace.state.v1";
pub const CONTEXT_CHECKPOINT_CARRIER_ENVELOPE_DIGEST_DOMAIN: &str =
"leanctx.checkpoint.envelope.v2";
pub const MAX_CONTEXT_CHECKPOINT_TEXT_BYTES: usize = 1_024;
pub const MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES: usize = 1_048_576;
pub const MAX_CONTEXT_CHECKPOINT_SLUG_BYTES: usize = 128;
pub const MAX_CONTEXT_CHECKPOINT_ID_BYTES: usize = 256;
pub const MAX_CONTEXT_CHECKPOINT_REFERENCE_BYTES: usize = 512;
pub const MAX_CONTEXT_CHECKPOINT_DECISIONS: usize = 64;
pub const MAX_CONTEXT_CHECKPOINT_DECISION_REFS: usize = 16;
pub const MAX_CONTEXT_CHECKPOINT_FINDINGS: usize = 64;
pub const MAX_CONTEXT_CHECKPOINT_NEXT_STEPS: usize = 64;
pub const MAX_CONTEXT_CHECKPOINT_FILES: usize = 256;
pub const MAX_CONTEXT_CHECKPOINT_POLICY_PINS: usize = 64;
pub const MAX_CONTEXT_CHECKPOINT_PACKAGE_PINS: usize = 128;
pub const MAX_CONTEXT_CHECKPOINT_LINEAGE_REFS: usize = 64;
pub const MAX_CONTEXT_CHECKPOINT_STEPS: u32 = 4_096;
const REJECTED_ASSIGNMENTS: [&str; 19] = [
"-----begin",
"pid=",
"process_id=",
"lock_id=",
"provider_handle=",
"cache_handle=",
"temp_dir=",
"tmpdir=",
"flush_timestamp=",
"authorization: bearer ",
"bearer ",
"password=",
"password:",
"passwd=",
"api_key=",
"access_key=",
"aws_secret=",
"client_secret=",
"token=",
];
const REJECTED_TOKEN_PREFIXES: [&str; 12] = [
"base64:",
"ghp_",
"gho_",
"ghr_",
"ghs_",
"ghu_",
"github_pat_",
"hex:",
"sk-",
"xoxa-",
"xoxb-",
"xoxp-",
];
macro_rules! checkpoint_string {
($(#[$meta:meta])* $name:ident, $validate:path) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
let value = value.into();
$validate(&value, stringify!($name))?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::str::FromStr for $name {
type Err = ValidationError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for $name {
type Error = ValidationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<String> for $name {
type Error = ValidationError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<$name> for String {
fn from(value: $name) -> Self {
value.0
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(DeError::custom)
}
}
};
}
macro_rules! validated_deserialize {
($target:ident { $($field:ident : $field_type:ty),+ $(,)? }) => {
impl<'de> Deserialize<'de> for $target {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
$($field: $field_type),+
}
let Wire { $($field),+ } = Wire::deserialize(deserializer)?;
let value = Self { $($field),+ };
value.validate().map_err(DeError::custom)?;
Ok(value)
}
}
};
}
checkpoint_string!(
ContextCheckpointIdV1,
validate_canonical_uuid
);
checkpoint_string!(
ContextCheckpointBranchIdV1,
validate_checkpoint_slug
);
checkpoint_string!(
ContextCheckpointDeviceIdV1,
validate_checkpoint_slug
);
checkpoint_string!(
ContextCheckpointTextV1,
validate_checkpoint_text
);
mod validation;
use validation::*;
mod v1;
pub use v1::*;
mod v2;
pub use v2::*;
#[cfg(test)]
mod tests;
pub(super) use validation::{
canonical_bytes_of, digest_with_domain, is_unicode_format_character,
reject_machine_local_content, require_capacity, require_sorted_unique, require_unique,
validate_checkpoint_identifier, validate_checkpoint_reference,
};