src/federation/aws.rs
//! **AWS CodeArtifact** harbour.
//!
//! CodeArtifact exposes each repository through a package-manager-compatible HTTPS
//! endpoint:
//! `https://{domain}-{owner}.d.codeartifact.{region}.amazonaws.com/{format}/{repo}/…`
//! (the value `aws codeartifact get-repository-endpoint` returns). Those endpoints
//! authenticate with a short-lived **bearer authorization token** — the exact
//! token `aws codeartifact get-authorization-token` mints and that npm/pip/cargo
//! all consume. We speak the endpoint directly with that bearer token via the
//! shared sync client.
//!
//! We deliberately do NOT vendor `aws-sdk-codeartifact` + `aws-config`: their
//! default TLS provider is `aws-lc-rs`, which pulls a **C/cmake/nasm** build
//! toolchain and so violates the nordisk pure-Rust charter (and would risk the
//! green build). The one thing the SDK is genuinely needed for — SigV4-signing the
//! `GetAuthorizationToken` STS call — is done out-of-band by the operator's `aws`
//! CLI (or their own SDK) and the resulting token is provisioned via config/env,
//! exactly as every AWS package-manager integration does. No AWS crypto is
//! hand-rolled here; the harbour is a faithful CodeArtifact HTTP client.
use anyhow::Result;
use super::{ArtifactCoord, ArtifactMeta, RegistryFormat, UpstreamAuth, UpstreamRegistry, verify_sha256};
#[cfg(feature = "federation")]
use super::{get_bytes, head_meta};
/// The CodeArtifact URL path segment for a repository format.
fn format_segment(fmt: RegistryFormat) -> &'static str {
match fmt {
RegistryFormat::Npm => "npm",
RegistryFormat::Maven => "maven",
RegistryFormat::Pip => "pypi",
RegistryFormat::Nuget => "nuget",
RegistryFormat::Rust => "cargo",
RegistryFormat::Gem => "ruby",
// AWS CodeArtifact has no OCI repository format (OCI images are served by the
// OciDistributionRegistry harbour, not CodeArtifact) → the generic segment.
RegistryFormat::Oci => "generic",
RegistryFormat::Raw => "generic",
}
}
/// A read-only harbour proxying one AWS CodeArtifact repository.
pub struct AwsCodeArtifactRegistry {
name: String,
/// The repository endpoint base, e.g.
/// `https://my-domain-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my-repo`
/// (no trailing slash).
endpoint: String,
auth: UpstreamAuth,
format: RegistryFormat,
}
impl AwsCodeArtifactRegistry {
/// Build from a full repository endpoint (what `get-repository-endpoint`
/// returns) plus the bearer authorization token.
pub fn new(
name: impl Into<String>,
endpoint: impl Into<String>,
auth_token: impl Into<String>,
format: RegistryFormat,
) -> Self {
Self {
name: name.into(),
endpoint: endpoint.into().trim_end_matches('/').to_string(),
auth: UpstreamAuth::Bearer {
token: auth_token.into(),
},
format,
}
}
/// Build the endpoint from its parts (domain, 12-digit account owner, region,
/// repository) — the canonical CodeArtifact host shape.
pub fn from_parts(
name: impl Into<String>,
domain: &str,
domain_owner: &str,
region: &str,
repository: &str,
format: RegistryFormat,
auth_token: impl Into<String>,
) -> Self {
let endpoint = format!(
"https://{domain}-{domain_owner}.d.codeartifact.{region}.amazonaws.com/{}/{repository}",
format_segment(format)
);
Self::new(name, endpoint, auth_token, format)
}
/// Build from parts against an arbitrary host (tests / VPC endpoint). `host`
/// is the scheme+host base, e.g. `http://127.0.0.1:1234`.
pub fn with_host(
name: impl Into<String>,
host: &str,
repository: &str,
format: RegistryFormat,
auth_token: impl Into<String>,
) -> Self {
let endpoint = format!(
"{}/{}/{repository}",
host.trim_end_matches('/'),
format_segment(format)
);
Self::new(name, endpoint, auth_token, format)
}
/// Full content URL for one artifact, per the format's CodeArtifact asset path.
pub fn content_url(&self, id: &ArtifactCoord) -> String {
let name = &id.name;
let ver = &id.version;
let tail = match self.format {
RegistryFormat::Npm => format!("{name}/-/{name}-{ver}.tgz"),
RegistryFormat::Maven => {
let g = id.namespace.as_deref().unwrap_or("").replace('.', "/");
format!("{g}/{name}/{ver}/{name}-{ver}.jar")
}
RegistryFormat::Pip => format!("simple/{name}/{name}-{ver}.tar.gz"),
RegistryFormat::Nuget => {
let n = name.to_lowercase();
format!("v3/content/{n}/{ver}/{n}.{ver}.nupkg")
}
RegistryFormat::Rust => format!("api/v1/crates/{name}/{ver}/download"),
RegistryFormat::Gem => format!("gems/{name}-{ver}.gem"),
// OCI images are served by the OciDistributionRegistry harbour, not
// CodeArtifact → the opaque fallback path.
RegistryFormat::Oci => format!("{name}/{ver}"),
RegistryFormat::Raw => format!("{name}/{ver}"),
};
format!("{}/{}", self.endpoint, tail)
}
}
#[cfg(feature = "federation")]
impl UpstreamRegistry for AwsCodeArtifactRegistry {
fn name(&self) -> &str {
&self.name
}
fn base_url(&self) -> &str {
&self.endpoint
}
fn auth(&self) -> &UpstreamAuth {
&self.auth
}
fn format(&self) -> RegistryFormat {
self.format
}
fn fetch(&self, id: &ArtifactCoord) -> Result<Option<Vec<u8>>> {
let (bytes, sha256) = match get_bytes(&self.endpoint, &self.content_url(id), &self.auth)? {
Some(b) => b,
None => return Ok(None),
};
if let Some(expected) = sha256 {
verify_sha256(&bytes, &expected)?;
}
Ok(Some(bytes))
}
fn metadata(&self, id: &ArtifactCoord) -> Result<Option<ArtifactMeta>> {
head_meta(&self.endpoint, &self.content_url(id), &self.auth)
}
}
#[cfg(test)]
mod tests {
use super::super::testutil::MockServer;
use super::*;
fn id(name: &str, version: &str) -> ArtifactCoord {
ArtifactCoord::new(name, version)
}
#[test]
fn endpoint_and_asset_url_for_npm() {
let up = AwsCodeArtifactRegistry::from_parts(
"ca",
"my-domain",
"111122223333",
"us-west-2",
"my-repo",
RegistryFormat::Npm,
"AUTHTOK",
);
assert_eq!(
up.content_url(&id("left-pad", "1.3.0")),
"https://my-domain-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my-repo/left-pad/-/left-pad-1.3.0.tgz"
);
}
#[test]
fn cargo_asset_url_uses_codeartifact_download_path() {
let up = AwsCodeArtifactRegistry::from_parts(
"ca",
"d",
"111122223333",
"eu-north-1",
"crates",
RegistryFormat::Rust,
"T",
);
assert!(
up.content_url(&id("serde", "1.0.0"))
.ends_with("/cargo/crates/api/v1/crates/serde/1.0.0/download")
);
}
#[test]
fn bearer_token_attached_and_fetch_against_mock() {
let server = MockServer::start(|method, path| {
if method == "GET" && path.ends_with("/left-pad/-/left-pad-1.3.0.tgz") {
(200, b"tgz-bytes".to_vec())
} else {
(404, b"no".to_vec())
}
});
let up = AwsCodeArtifactRegistry::with_host(
"ca",
&server.base_url,
"my-repo",
RegistryFormat::Npm,
"AUTHTOK",
);
let got = up.fetch(&id("left-pad", "1.3.0")).unwrap();
assert_eq!(got.as_deref(), Some(b"tgz-bytes".as_ref()));
assert_eq!(
server.requests()[0].header("authorization"),
Some("Bearer AUTHTOK")
);
crate::functional_status(
"skidbladnir/federation-aws",
"codeartifact_bearer_fetch",
got.as_deref() == Some(b"tgz-bytes".as_ref())
&& server.requests()[0].header("authorization") == Some("Bearer AUTHTOK"),
"CodeArtifact fetch with bearer authorization token",
);
}
#[test]
fn missing_is_clean_none() {
let server = MockServer::start(|_m, _p| (404, b"no".to_vec()));
let up = AwsCodeArtifactRegistry::with_host(
"ca",
&server.base_url,
"my-repo",
RegistryFormat::Npm,
"T",
);
assert!(up.fetch(&id("ghost", "0.0.0")).unwrap().is_none());
}
}