Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3776371
Extract explicit EC storage selection and celld persistence
ChristianPavilonis Nov 11, 2026
9af2540
Carry the lower baseline check corrections into storage
ChristianPavilonis Nov 11, 2026
cdad6df
Carry the documentation site correction into storage
ChristianPavilonis Nov 11, 2026
83f58c5
Carry the startup probe correction through the compatible baseline
ChristianPavilonis Nov 11, 2026
f14eefa
Carry the browser CI host compatibility fix through the baseline
ChristianPavilonis Nov 11, 2026
c279229
Carry the tagged EdgeZero snapshot through the stack
ChristianPavilonis Nov 11, 2026
2610d4a
Carry the monthly EdgeZero tag through the stack
ChristianPavilonis Nov 11, 2026
bad5db5
Retain explicit EC storage and typed celld records on the cutover
ChristianPavilonis Nov 12, 2026
3a0d8bf
Retain the owning partial-Geo parity fixture ancestry
ChristianPavilonis Nov 12, 2026
4287413
Retain the concurrent cutover documentation closeout
ChristianPavilonis Nov 12, 2026
c9a8b66
Retain prepared native development documentation
ChristianPavilonis Nov 12, 2026
ad00c40
Select runtime-appropriate storage in local acceptance fixtures
ChristianPavilonis Nov 12, 2026
5ab4ba8
Retain current main validation and mediator fallback
ChristianPavilonis Nov 12, 2026
0df6080
Retain the Cargo target-aware cache harness
ChristianPavilonis Nov 12, 2026
83b5bd0
Exercise the runtime-specific smoke helper in CLI store tests
ChristianPavilonis Nov 12, 2026
53bcc15
Retain target-aware GPT bundle lookup for the cache fixture
ChristianPavilonis Nov 12, 2026
c04da1d
Carry hosted qualification fixes through PR 1537
ChristianPavilonis Nov 12, 2026
df61007
Carry hosted qualification fixes through PR 1537
ChristianPavilonis Nov 12, 2026
cca7318
Carry hosted qualification fixes through PR 1537
ChristianPavilonis Nov 12, 2026
b570c46
Carry hosted qualification fixes through PR 1537
ChristianPavilonis Nov 12, 2026
4a83827
Carry hosted qualification fixes through PR 1537
ChristianPavilonis Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ edgezero-adapter-fastly = { git = "https://github.com/IABTechLab/edgezero", tag
edgezero-adapter-spin = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.12-rc-axum.202611.2", default-features = false }
edgezero-cli = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.12-rc-axum.202611.2" }
edgezero-core = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.12-rc-axum.202611.2", default-features = false }
edgezero-store-celld = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.12-rc-axum.202611.2", default-features = false }
env_logger = "0.11"
error-stack = "0.6"
esi = "0.7.2"
Expand Down
4 changes: 4 additions & 0 deletions crates/trusted-server-adapter-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ aws-appconfig-agent = ["edgezero-adapter-axum/aws-appconfig-agent"]
aws-secrets-manager = ["edgezero-adapter-axum/aws-secrets-manager"]
aws-dynamodb-ec = ["dep:aws-config", "dep:aws-sdk-dynamodb"]
geo-cloudfront = ["edgezero-adapter-axum/geo-cloudfront"]
celld-ec = ["dep:edgezero-store-celld"]

[dependencies]
async-trait = { workspace = true }
Expand All @@ -41,6 +42,9 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "ti
trusted-server-core = { workspace = true }
url = { workspace = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
edgezero-store-celld = { workspace = true, features = ["records"], optional = true }

[dev-dependencies]
axum = { workspace = true }
tempfile = { workspace = true }
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ fn build_state_with_services(
settings: Settings,
services: Option<RuntimeServices>,
) -> Result<Arc<AppState>, Report<TrustedServerError>> {
settings
.ec
.resolve_storage(trusted_server_core::ec::storage::EcRuntime::Native)?;
let plan = Arc::new(compile_auction_plan(&settings)?);
let orchestrator = build_orchestrator_with_plan(Arc::clone(&plan), &settings)?;
let registry = IntegrationRegistry::with_plan(&settings, plan)?;
Expand Down
120 changes: 120 additions & 0 deletions crates/trusted-server-adapter-axum/src/celld_ec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! Native EC persistence through celld's typed conditional-record client.

use async_trait::async_trait;
use edgezero_core::EdgeError;
use edgezero_core::time::MonotonicClock;
use edgezero_store_celld::CelldPreparation;
use edgezero_store_celld::records::{
CelldRecordStore, RecordWrite, RecordWriteMode, RecordWriteOutcome,
};
use edgezero_store_celld::settings::{CelldAccessToken, CelldKvSettings, CelldPreparationLimits};
use error_stack::Report;
use trusted_server_core::ec::kv_backend::{
EcKvLookup, EcKvStore, EcKvWrite, EcKvWriteMode, EcKvWriteOutcome,
};
use trusted_server_core::error::TrustedServerError;

/// A live provider. Clones share transport, never cached identity rows.
#[derive(Clone, Debug)]
pub struct CelldEcKv {
records: CelldRecordStore,
}

fn preparation_error() -> EdgeError {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking: Every preparation failure (bad or rejected token, unsupported protocol or record version, unreachable service) collapses into the same native EC celld preparation failed, and store_error does the same for request-time errors. Hiding input values is intended, but an operator can't tell a credential problem from an outage. Could you attach a value-free error category from the celld error kind (for example credential, protocol or transport)?

EdgeError::internal(std::io::Error::other("native EC celld preparation failed"))
}

fn store_error() -> Report<TrustedServerError> {
Report::new(TrustedServerError::KvStore {
store_name: "native-ec-celld".to_owned(),
message: "EC celld operation failed".to_owned(),
})
}

impl CelldEcKv {
/// Prepare an explicitly selected provider using one captured bootstrap token.
///
/// # Errors
/// Invalid settings, credentials, protocol or service failures prevent readiness.
pub async fn prepare(
settings: &CelldKvSettings,
token: Option<&CelldAccessToken>,
) -> Result<Self, EdgeError> {
let mut preparation =
CelldPreparation::new(MonotonicClock::default(), CelldPreparationLimits::default())
.map_err(|_| preparation_error())?;
let records = preparation
.prepare_records(settings, token)
.await
.map_err(|_| preparation_error())?;
Ok(Self { records })
}
}

#[async_trait(?Send)]
impl EcKvStore for CelldEcKv {
fn store_name(&self) -> &str {
"native-ec-celld"
}
async fn lookup(&self, key: &str) -> Result<Option<EcKvLookup>, Report<TrustedServerError>> {
self.records
.lookup(key)
.await
.map(|row| {
row.map(|row| EcKvLookup {
body: row.body,
metadata: row.metadata,
generation: row.generation,
})
})
.map_err(|_| store_error())
}
async fn key_exists(&self, key: &str) -> Result<bool, Report<TrustedServerError>> {
self.records
.key_exists(key)
.await
.map_err(|_| store_error())
}
async fn insert(
&self,
key: &str,
write: EcKvWrite<'_>,
) -> Result<EcKvWriteOutcome, Report<TrustedServerError>> {
let mode = match write.mode {
EcKvWriteMode::Add => RecordWriteMode::Add,
EcKvWriteMode::Overwrite => RecordWriteMode::Overwrite,
EcKvWriteMode::IfGenerationMatch(generation) => {
RecordWriteMode::IfGenerationMatch(generation)
}
};
self.records
.insert(
key,
RecordWrite {
body: write.body.as_bytes(),
metadata: Some(write.metadata.as_bytes()),
ttl: write.ttl,
mode,
},
)
.await
.map(|outcome| match outcome {
RecordWriteOutcome::Written => EcKvWriteOutcome::Written,
RecordWriteOutcome::PreconditionFailed => EcKvWriteOutcome::PreconditionFailed,
})
.map_err(|_| store_error())
}
async fn list_keys_with_prefix(
&self,
prefix: &str,
limit: u32,
) -> Result<Vec<String>, Report<TrustedServerError>> {
self.records
.list_keys_with_prefix(prefix, limit)
.await
.map_err(|_| store_error())
}
async fn delete(&self, key: &str) -> Result<(), Report<TrustedServerError>> {
self.records.delete(key).await.map_err(|_| store_error())
}
}
94 changes: 35 additions & 59 deletions crates/trusted-server-adapter-axum/src/ec_kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use async_trait::async_trait;
use edgezero_core::EdgeError;
use error_stack::Report;
use trusted_server_core::ec::kv_backend::{EcKvLookup, EcKvStore, EcKvWrite, EcKvWriteOutcome};
use trusted_server_core::ec::storage::DynamoDbEcStorage;
use trusted_server_core::error::TrustedServerError;

/// Cloneable live provider. Clones share the SDK connection pool, not a cache.
Expand Down Expand Up @@ -40,29 +41,32 @@ fn store_error() -> Report<TrustedServerError> {
})
}

/// Prepare the explicitly selected EC backend before accepting requests.
///
/// `TS_EC_DYNAMODB_ENABLED=true` selects this backend. Absent or `false` leaves
/// it off, regardless of AWS configuration. Selection requires REGION, TABLE,
/// and NAMESPACE under the same prefix, and the `aws-dynamodb-ec` build feature.
/// Credentials use the normal AWS SDK chain. For isolated local tests only,
/// `TS_EC_DYNAMODB_LOCAL_ENDPOINT` accepts an HTTP root URL at `127.0.0.1` with
/// an explicit port and bypasses the credential chain with dummy credentials.
/// Provider errors never include SDK responses, identifiers, endpoint URLs,
/// credentials, or record contents.
/// Reject obsolete native launch selection without preparing from environment.
///
/// # Errors
/// Returns a sanitized error on invalid selection or failed preparation.
pub async fn prepare_from_env() -> Result<Option<Arc<DynamoDbEcKv>>, EdgeError> {
/// Enabled or malformed legacy selection must be migrated to published config.
pub fn reject_legacy_configuration() -> Result<(), EdgeError> {
match std::env::var("TS_EC_DYNAMODB_ENABLED") {
Err(std::env::VarError::NotPresent) => return Ok(None),
Ok(value) if value == "false" => return Ok(None),
Ok(value) if value == "true" => {}
_ => return Err(preparation_error()),
Err(std::env::VarError::NotPresent) => Ok(()),
Ok(value) if value == "false" => Ok(()),
Ok(value) if value == "true" => Err(EdgeError::internal(std::io::Error::other(
"TS_EC_DYNAMODB_ENABLED is obsolete; publish ec.storage and remove legacy EC environment selection",
))),
_ => Err(EdgeError::internal(std::io::Error::other(
"TS_EC_DYNAMODB_ENABLED is malformed; migrate to published ec.storage configuration",
))),
}
}

/// Prepare the explicitly selected typed `DynamoDB` provider before readiness.
///
/// # Errors
/// Returns a sanitized error on invalid input, missing feature or failed preparation.
pub async fn prepare(config: &DynamoDbEcStorage) -> Result<Arc<DynamoDbEcKv>, EdgeError> {
config.validate().map_err(|_| preparation_error())?;
#[cfg(feature = "aws-dynamodb-ec")]
{
enabled::prepare_from_env().await.map(Some)
enabled::prepare(config).await
}
#[cfg(not(feature = "aws-dynamodb-ec"))]
Err(EdgeError::internal(std::io::Error::other(
Expand Down Expand Up @@ -126,26 +130,16 @@ mod enabled {
type Item = HashMap<String, AttributeValue>;
type StoreResult<T> = Result<T, Report<TrustedServerError>>;

pub(super) async fn prepare_from_env() -> Result<Arc<DynamoDbEcKv>, EdgeError> {
let required = |suffix: &str| {
std::env::var(format!("TS_EC_DYNAMODB_{suffix}")).map_err(|_| preparation_error())
};
let region = required("REGION")?;
let table = required("TABLE")?;
let namespace = required("NAMESPACE")?;
if region.is_empty() || region.chars().any(char::is_whitespace) {
return Err(preparation_error());
}
validate_binding(&table, &namespace)?;
pub(super) async fn prepare(
config: &super::DynamoDbEcStorage,
) -> Result<Arc<DynamoDbEcKv>, EdgeError> {
tokio::time::timeout(PREPARATION_TIMEOUT, async {
let builder = match std::env::var("TS_EC_DYNAMODB_LOCAL_ENDPOINT") {
Ok(endpoint) => {
validate_local_endpoint(&endpoint)?;
// Explicit emulator mode never reads local AWS files or
// sends real workload credentials to the test endpoint.
let builder = match &config.local_endpoint {
Some(endpoint) => {
// Explicit emulator mode never reads ambient credentials.
aws_sdk_dynamodb::Config::builder()
.behavior_version(BehaviorVersion::v2026_01_12())
.region(Region::new(region))
.region(Region::new(config.region.clone()))
.credentials_provider(aws_sdk_dynamodb::config::Credentials::new(
"local",
"local",
Expand All @@ -155,50 +149,32 @@ mod enabled {
))
.endpoint_url(endpoint)
}
Err(std::env::VarError::NotPresent) => {
None => {
let shared = aws_config::defaults(BehaviorVersion::v2026_01_12())
.region(Region::new(region))
.region(Region::new(config.region.clone()))
.load()
.await;
let mut builder = aws_sdk_dynamodb::Config::from(&shared).to_builder();
// Clear both global and service-specific endpoint URLs
// copied from the shared config. Resolve normal AWS endpoints.
// Production ignores ambient endpoint overrides.
builder.set_endpoint_url(None);
builder
}
Err(_) => return Err(preparation_error()),
};
let config = builder
let config_sdk = builder
.retry_config(RetryConfig::standard().with_max_attempts(1))
.timeout_config(timeout_config())
.build();
DynamoDbEcKv::prepare_with_client(
aws_sdk_dynamodb::Client::from_conf(config),
table,
namespace,
aws_sdk_dynamodb::Client::from_conf(config_sdk),
config.table.clone(),
config.namespace.clone(),
)
.await
})
.await
.map_err(|_| preparation_error())?
}

pub(super) fn validate_local_endpoint(endpoint: &str) -> Result<(), EdgeError> {
let url = url::Url::parse(endpoint).map_err(|_| preparation_error())?;
if url.scheme() != "http"
|| url.host_str() != Some("127.0.0.1")
|| url.port().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.path() != "/"
|| url.query().is_some()
|| url.fragment().is_some()
{
return Err(preparation_error());
}
Ok(())
}

fn validate_binding(table: &str, namespace: &str) -> Result<(), EdgeError> {
if !(3..=255).contains(&table.len())
|| !table
Expand Down
Loading
Loading