From 9ab06cfea31430b17c5b162ab8bf606ceb88daf2 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:03:00 -0400 Subject: [PATCH 01/15] DEN-2859: salvage package-reader authn with revocable lineage --- src/auth.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/src/auth.rs b/src/auth.rs index 181229d..e48a45e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -11,6 +11,8 @@ use crate::shared_auth::{ClientError, Introspection}; use crate::state::AppState; const REQUIRED_ACCOUNT_SCOPE: &str = "zpkg:account"; +const REQUIRED_PACKAGE_READ_SCOPE: &str = "zpkg:packages:read"; +const ZPKG_CLI_AUTHORIZED_PARTY: &str = "zpkg-cli"; const CUSTOMER_AUTH_REALM: &str = "customer"; /// Verified browser/account identity. The Shared Auth token is intentionally @@ -25,6 +27,16 @@ pub struct AccountIdentity { pub parent_jti: String, } +/// Verified CLI identity for private package-resource authorization. +/// Authentication alone grants no package access; handlers must still perform +/// product-local visibility and membership authorization for the target resource. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PackageReaderIdentity { + pub session: SessionIdentity, + pub session_id: String, + pub parent_jti: String, +} + /// Tokens are stored as sha256 hex; the plaintext is shown exactly once by /// the `create-token` subcommand. pub fn hash_token(plaintext: &str) -> String { @@ -61,6 +73,24 @@ pub async fn require_account(state: &AppState, headers: &HeaderMap) -> ApiResult account_from_introspection(&introspection, &state.shared_auth_application_id) } +pub async fn require_package_reader( + state: &AppState, + headers: &HeaderMap, +) -> ApiResult { + let token = bearer_token(headers).ok_or_else(ApiErr::unauthorized)?; + let client = state.shared_auth.as_ref().ok_or_else(|| { + ApiErr::service_unavailable( + "auth_unavailable", + "shared authentication is not configured", + ) + })?; + let introspection = client + .introspect_for_audience(&token, &state.shared_auth_audience) + .await + .map_err(map_shared_auth_error)?; + package_reader_from_introspection(&introspection) +} + fn account_from_introspection( introspection: &Introspection, expected_authorized_party: &str, @@ -137,6 +167,75 @@ fn account_from_introspection( }) } +fn package_reader_from_introspection( + introspection: &Introspection, +) -> ApiResult { + if !introspection.active { + return Err(ApiErr::unauthorized()); + } + let subject = required_claim(introspection.sub.as_deref())? + .parse::() + .map_err(|_| ApiErr::unauthorized())?; + required_claim(introspection.iss.as_deref())?; + + let delegated_required = || { + ApiErr::forbidden( + "delegated_user_token_required", + "private package reads require a session-backed delegated user token", + ) + }; + let session_id = optional_rest_string(introspection, "sid")?.ok_or_else(delegated_required)?; + let authorized_party = + optional_rest_string(introspection, "azp")?.ok_or_else(delegated_required)?; + let parent_jti = + optional_rest_string(introspection, "parent_jti")?.ok_or_else(delegated_required)?; + + if authorized_party != ZPKG_CLI_AUTHORIZED_PARTY { + return Err(ApiErr::forbidden( + "wrong_authorized_party", + "the delegated token was not issued to the zed-pkg CLI", + )); + } + + let scope = optional_rest_string(introspection, "scope")?.ok_or_else(|| { + ApiErr::forbidden( + "insufficient_scope", + "the delegated token is missing the private-package read scope", + ) + })?; + if !scope + .split_ascii_whitespace() + .any(|candidate| candidate == REQUIRED_PACKAGE_READ_SCOPE) + { + return Err(ApiErr::forbidden( + "insufficient_scope", + "the delegated token is missing the private-package read scope", + )); + } + + let realm = optional_rest_string(introspection, "auth_realm")? + .or(optional_rest_string(introspection, "realm")?) + .unwrap_or_else(|| CUSTOMER_AUTH_REALM.to_owned()); + if realm != CUSTOMER_AUTH_REALM { + return Err(ApiErr::forbidden( + "wrong_auth_realm", + "the zed-pkg CLI accepts customer identities only", + )); + } + + Ok(PackageReaderIdentity { + session: SessionIdentity { + subject, + realm, + email: introspection.email.clone(), + display_name: optional_rest_string(introspection, "display_name")?, + avatar_url: optional_rest_string(introspection, "avatar_url")?, + }, + session_id, + parent_jti, + }) +} + pub(crate) fn map_shared_auth_error(error: ClientError) -> ApiErr { match error { ClientError::Unauthorized | ClientError::Status(401) | ClientError::InvalidInput(_) => { @@ -319,6 +418,77 @@ mod tests { ); } + #[test] + fn cli_package_reader_requires_exact_party_scope_lineage_and_customer_realm() { + let mut rest = delegated_rest("zpkg-cli", "zpkg:packages:read"); + rest.insert("auth_realm".into(), Value::String("customer".into())); + let introspection = Introspection { + active: true, + sub: Some(SUBJECT.into()), + iss: Some("https://auth.example.test".into()), + email: None, + rest, + }; + let identity = package_reader_from_introspection(&introspection).unwrap(); + assert_eq!(identity.session.subject, SUBJECT.parse::().unwrap()); + assert_eq!(identity.session_id, "session-1"); + assert_eq!(identity.parent_jti, "parent-token-1"); + + let wrong_party = Introspection { + active: true, + sub: Some(SUBJECT.into()), + iss: Some("https://auth.example.test".into()), + email: None, + rest: delegated_rest("zpkg-web", "zpkg:packages:read"), + }; + assert_eq!( + package_reader_from_introspection(&wrong_party) + .unwrap_err() + .code, + "wrong_authorized_party" + ); + + let wrong_scope = Introspection { + active: true, + sub: Some(SUBJECT.into()), + iss: Some("https://auth.example.test".into()), + email: None, + rest: delegated_rest("zpkg-cli", "zpkg:account"), + }; + assert_eq!( + package_reader_from_introspection(&wrong_scope) + .unwrap_err() + .code, + "insufficient_scope" + ); + + let base = Introspection { + active: true, + sub: Some(SUBJECT.into()), + iss: Some("https://auth.example.test".into()), + email: None, + rest: serde_json::Map::new(), + }; + assert_eq!( + package_reader_from_introspection(&base).unwrap_err().code, + "delegated_user_token_required" + ); + + let mut admin_rest = delegated_rest("zpkg-cli", "zpkg:packages:read"); + admin_rest.insert("auth_realm".into(), Value::String("admin".into())); + let admin = Introspection { + active: true, + sub: Some(SUBJECT.into()), + iss: Some("https://auth.example.test".into()), + email: None, + rest: admin_rest, + }; + assert_eq!( + package_reader_from_introspection(&admin).unwrap_err().code, + "wrong_auth_realm" + ); + } + #[test] fn delegated_token_without_account_scope_is_forbidden() { let introspection = Introspection { From 238e4926071200fc380e4084f5cb1cab1bfcdd4c Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:18:05 -0400 Subject: [PATCH 02/15] DEN-2859: split package-reader FP matrix and narrow visibility --- src/auth.rs | 112 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index e48a45e..651aded 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -31,7 +31,7 @@ pub struct AccountIdentity { /// Authentication alone grants no package access; handlers must still perform /// product-local visibility and membership authorization for the target resource. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct PackageReaderIdentity { +pub(crate) struct PackageReaderIdentity { pub session: SessionIdentity, pub session_id: String, pub parent_jti: String, @@ -73,7 +73,7 @@ pub async fn require_account(state: &AppState, headers: &HeaderMap) -> ApiResult account_from_introspection(&introspection, &state.shared_auth_application_id) } -pub async fn require_package_reader( +pub(crate) async fn require_package_reader( state: &AppState, headers: &HeaderMap, ) -> ApiResult { @@ -418,50 +418,73 @@ mod tests { ); } - #[test] - fn cli_package_reader_requires_exact_party_scope_lineage_and_customer_realm() { - let mut rest = delegated_rest("zpkg-cli", "zpkg:packages:read"); - rest.insert("auth_realm".into(), Value::String("customer".into())); - let introspection = Introspection { - active: true, - sub: Some(SUBJECT.into()), - iss: Some("https://auth.example.test".into()), - email: None, - rest, - }; - let identity = package_reader_from_introspection(&introspection).unwrap(); - assert_eq!(identity.session.subject, SUBJECT.parse::().unwrap()); - assert_eq!(identity.session_id, "session-1"); - assert_eq!(identity.parent_jti, "parent-token-1"); + fn package_reader_claims( + authorized_party: &str, + scope: &str, + realm: Option<&str>, + ) -> serde_json::Map { + delegated_rest(authorized_party, scope) + .into_iter() + .chain(realm.map(|realm| ("auth_realm".into(), Value::String(realm.into())))) + .collect() + } - let wrong_party = Introspection { + fn package_reader_introspection( + authorized_party: &str, + scope: &str, + realm: Option<&str>, + ) -> Introspection { + Introspection { active: true, sub: Some(SUBJECT.into()), iss: Some("https://auth.example.test".into()), email: None, - rest: delegated_rest("zpkg-web", "zpkg:packages:read"), - }; + rest: package_reader_claims(authorized_party, scope, realm), + } + } + + #[test] + fn cli_package_reader_preserves_revocable_lineage() { + let introspection = + package_reader_introspection("zpkg-cli", "zpkg:packages:read", Some("customer")); + let identity = package_reader_from_introspection(&introspection).ok(); + assert_eq!( + identity.as_ref().map(|identity| identity.session.subject.to_string()), + Some(SUBJECT.to_owned()) + ); + assert_eq!( + identity.as_ref().map(|identity| identity.session_id.as_str()), + Some("session-1") + ); + assert_eq!( + identity.as_ref().map(|identity| identity.parent_jti.as_str()), + Some("parent-token-1") + ); + } + + #[test] + fn cli_package_reader_rejects_wrong_party_and_scope() { + let wrong_party = + package_reader_introspection("zpkg-web", "zpkg:packages:read", Some("customer")); assert_eq!( package_reader_from_introspection(&wrong_party) - .unwrap_err() - .code, - "wrong_authorized_party" + .err() + .map(|error| error.code), + Some("wrong_authorized_party") ); - let wrong_scope = Introspection { - active: true, - sub: Some(SUBJECT.into()), - iss: Some("https://auth.example.test".into()), - email: None, - rest: delegated_rest("zpkg-cli", "zpkg:account"), - }; + let wrong_scope = + package_reader_introspection("zpkg-cli", "zpkg:account", Some("customer")); assert_eq!( package_reader_from_introspection(&wrong_scope) - .unwrap_err() - .code, - "insufficient_scope" + .err() + .map(|error| error.code), + Some("insufficient_scope") ); + } + #[test] + fn cli_package_reader_rejects_base_and_admin_tokens() { let base = Introspection { active: true, sub: Some(SUBJECT.into()), @@ -470,22 +493,19 @@ mod tests { rest: serde_json::Map::new(), }; assert_eq!( - package_reader_from_introspection(&base).unwrap_err().code, - "delegated_user_token_required" + package_reader_from_introspection(&base) + .err() + .map(|error| error.code), + Some("delegated_user_token_required") ); - let mut admin_rest = delegated_rest("zpkg-cli", "zpkg:packages:read"); - admin_rest.insert("auth_realm".into(), Value::String("admin".into())); - let admin = Introspection { - active: true, - sub: Some(SUBJECT.into()), - iss: Some("https://auth.example.test".into()), - email: None, - rest: admin_rest, - }; + let admin = + package_reader_introspection("zpkg-cli", "zpkg:packages:read", Some("admin")); assert_eq!( - package_reader_from_introspection(&admin).unwrap_err().code, - "wrong_auth_realm" + package_reader_from_introspection(&admin) + .err() + .map(|error| error.code), + Some("wrong_auth_realm") ); } From 9d8b05848e6729f48ff9cb2085eed30389e4ef2c Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:19:11 -0400 Subject: [PATCH 03/15] DEN-2859: pin artifact authorization ORM read contract --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e421dec..b509e1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ zed-interfaces = { git = "https://github.com/zed-pkg/zed-interfaces.git", rev = # Canonical account, registry projection, and migration data plane. The API is # the only long-running service that opts into write symbols; the discrete # `migrate` command is the only runtime path that invokes DDL. -zed-orm-core = { git = "https://github.com/zed-pkg/zed-lib-core.git", rev = "c3d486a1519381276fbec02aa25247f542924443", features = ["read-write", "migrate"] } +zed-orm-core = { git = "https://github.com/zed-pkg/zed-lib-core.git", rev = "43b87769032c993f424e89825ad22abe980f4652", features = ["read-write", "migrate"] } # The private shared-auth-clients repository cannot be cloned by this # repository's scoped GITHUB_TOKEN. `src/shared_auth.rs` implements the small, # reviewed exchange/introspection HTTP contract using the existing reqwest From 861ec8e29c17e5cf5444747e7d35dff461e6e2bc Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:19:14 -0400 Subject: [PATCH 04/15] DEN-2859: pin artifact authorization ORM read contract --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index da7c904..b42a348 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5462,7 +5462,7 @@ dependencies = [ [[package]] name = "zed-orm-core" version = "0.1.0" -source = "git+https://github.com/zed-pkg/zed-lib-core.git?rev=c3d486a1519381276fbec02aa25247f542924443#c3d486a1519381276fbec02aa25247f542924443" +source = "git+https://github.com/zed-pkg/zed-lib-core.git?rev=43b87769032c993f424e89825ad22abe980f4652#43b87769032c993f424e89825ad22abe980f4652" dependencies = [ "chrono", "sea-orm", From 6909a893b61a8c37d4de784a544c7b5344d75847 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:19:17 -0400 Subject: [PATCH 05/15] DEN-2859: pin artifact authorization ORM read contract --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2e7d49..9d23d99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ env: EXPECTED_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} ZED_INTERFACES_SHA: 996fc56e20c4bb41366377be14edfae886b84e82 - ZED_LIB_CORE_SHA: c3d486a1519381276fbec02aa25247f542924443 + ZED_LIB_CORE_SHA: 43b87769032c993f424e89825ad22abe980f4652 jobs: test: From dbd88616c9787b96f55344cb6796e8b6105f4c19 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:19:20 -0400 Subject: [PATCH 06/15] DEN-2859: pin artifact authorization ORM read contract --- .github/workflows/publish-container.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 236beef..29dac74 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -57,7 +57,7 @@ env: # Keep these synchronized with the Rust CI pins; source-graph drift previously # made the debug test build pass while the release container could not compile. ZED_INTERFACES_SHA: 996fc56e20c4bb41366377be14edfae886b84e82 - ZED_LIB_CORE_SHA: c3d486a1519381276fbec02aa25247f542924443 + ZED_LIB_CORE_SHA: 43b87769032c993f424e89825ad22abe980f4652 jobs: image: From 5e11c502bbfeef440ead8fc88ebb35198c533ad0 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:19:23 -0400 Subject: [PATCH 07/15] DEN-2859: enforce private authorization on digest reads --- src/routes/artifacts.rs | 113 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/src/routes/artifacts.rs b/src/routes/artifacts.rs index d7e41bf..7de9ba6 100644 --- a/src/routes/artifacts.rs +++ b/src/routes/artifacts.rs @@ -4,11 +4,12 @@ use std::sync::Arc; use axum::body::Body; use axum::extract::{Path, State}; -use axum::http::{StatusCode, header}; +use axum::http::{HeaderMap, StatusCode, header}; use axum::response::{IntoResponse, Response}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use tokio_util::io::ReaderStream; +use crate::auth::require_package_reader; use crate::entities::version; use crate::error::{ApiErr, ApiResult}; use crate::files; @@ -19,10 +20,76 @@ use super::{artifact_format, find_org, find_package}; const IMMUTABLE: &str = "public, max-age=31536000, immutable"; +fn artifact_requires_private_authorization( + packages: &[zed_orm_core::models::PackageSummary], +) -> bool { + !packages.is_empty() + && packages + .iter() + .all(|package| package.visibility != "public") +} + +async fn authorize_artifact_read( + state: &AppState, + headers: &HeaderMap, + sha256: &str, +) -> ApiResult<()> { + let read = state.registry_read.as_ref().ok_or_else(|| { + ApiErr::service_unavailable( + "registry_data_plane_unavailable", + "canonical registry read context is not configured", + ) + })?; + let packages = zed_orm_core::read::packages_for_artifact_sha256(read, sha256) + .await + .map_err(crate::account::map_orm_error)?; + + // Legacy-only artifacts predate canonical visibility and remain in the + // existing anonymous compatibility plane. A shared digest with any public + // canonical reference is public bytes by definition; private references + // cannot make those same bytes secret again. + if !artifact_requires_private_authorization(&packages) { + return Ok(()); + } + + let reader = require_package_reader(state, headers).await?; + let Some(user) = + zed_orm_core::read::user_by_subject(read, &reader.session.realm, reader.session.subject) + .await + .map_err(crate::account::map_orm_error)? + else { + return Err(ApiErr::not_found("artifact")); + }; + + for package in &packages { + let org_role = zed_orm_core::read::org_role_for_user(read, package.org_id, user.id) + .await + .map_err(crate::account::map_orm_error)?; + if org_role.is_some() { + return Ok(()); + } + if let Some(project_id) = package.project_id { + let project_role = + zed_orm_core::read::project_role_for_user(read, project_id, user.id) + .await + .map_err(crate::account::map_orm_error)?; + if project_role.is_some() { + return Ok(()); + } + } + } + + // Do not confirm a private artifact's existence to an authenticated user + // who lacks membership in every referencing package. + Err(ApiErr::not_found("artifact")) +} + pub async fn get_artifact( State(state): State>, + headers: HeaderMap, Path(sha256): Path, ) -> ApiResult { + authorize_artifact_read(&state, &headers, &sha256).await?; let row = version::Entity::find() .filter(version::Column::Sha256.eq(&sha256)) .one(&state.db) @@ -65,6 +132,50 @@ pub async fn get_artifact( } } +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use uuid::Uuid; + use zed_orm_core::models::PackageSummary; + + fn package(visibility: &str) -> PackageSummary { + PackageSummary { + id: Uuid::nil(), + org_id: Uuid::nil(), + org_slug: "example".into(), + project_id: None, + project_slug: None, + name: "pkg".into(), + description: None, + visibility: visibility.into(), + repo_url: "https://github.com/example/pkg".into(), + config: json!({}), + latest_version: None, + download_count: 0, + version_count: 1, + } + } + + #[test] + fn shared_digest_is_anonymous_when_any_reference_is_public() { + assert!(!artifact_requires_private_authorization(&[])); + assert!(!artifact_requires_private_authorization(&[package("public")])); + assert!(!artifact_requires_private_authorization(&[ + package("private"), + package("public"), + ])); + } + + #[test] + fn all_private_digest_requires_resource_authorization() { + assert!(artifact_requires_private_authorization(&[ + package("private"), + package("private"), + ])); + } +} + pub async fn get_file( State(state): State>, Path((org_slug, name, ver, path)): Path<(String, String, String, String)>, From cbe90c819f8fab8655580e12a83a8fd1fe6d7bdb Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:23:40 -0400 Subject: [PATCH 08/15] DEN-2859: rustfmt delegated reader tests --- src/auth.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 651aded..2e5af6b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -449,15 +449,21 @@ mod tests { package_reader_introspection("zpkg-cli", "zpkg:packages:read", Some("customer")); let identity = package_reader_from_introspection(&introspection).ok(); assert_eq!( - identity.as_ref().map(|identity| identity.session.subject.to_string()), + identity + .as_ref() + .map(|identity| identity.session.subject.to_string()), Some(SUBJECT.to_owned()) ); assert_eq!( - identity.as_ref().map(|identity| identity.session_id.as_str()), + identity + .as_ref() + .map(|identity| identity.session_id.as_str()), Some("session-1") ); assert_eq!( - identity.as_ref().map(|identity| identity.parent_jti.as_str()), + identity + .as_ref() + .map(|identity| identity.parent_jti.as_str()), Some("parent-token-1") ); } @@ -499,8 +505,7 @@ mod tests { Some("delegated_user_token_required") ); - let admin = - package_reader_introspection("zpkg-cli", "zpkg:packages:read", Some("admin")); + let admin = package_reader_introspection("zpkg-cli", "zpkg:packages:read", Some("admin")); assert_eq!( package_reader_from_introspection(&admin) .err() From 5c2d0d98cea75c832f0cd864234064f93e9812bc Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:23:44 -0400 Subject: [PATCH 09/15] DEN-2859: rustfmt private artifact authorization --- src/routes/artifacts.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/routes/artifacts.rs b/src/routes/artifacts.rs index 7de9ba6..3d643ab 100644 --- a/src/routes/artifacts.rs +++ b/src/routes/artifacts.rs @@ -69,10 +69,9 @@ async fn authorize_artifact_read( return Ok(()); } if let Some(project_id) = package.project_id { - let project_role = - zed_orm_core::read::project_role_for_user(read, project_id, user.id) - .await - .map_err(crate::account::map_orm_error)?; + let project_role = zed_orm_core::read::project_role_for_user(read, project_id, user.id) + .await + .map_err(crate::account::map_orm_error)?; if project_role.is_some() { return Ok(()); } @@ -160,7 +159,9 @@ mod tests { #[test] fn shared_digest_is_anonymous_when_any_reference_is_public() { assert!(!artifact_requires_private_authorization(&[])); - assert!(!artifact_requires_private_authorization(&[package("public")])); + assert!(!artifact_requires_private_authorization(&[package( + "public" + )])); assert!(!artifact_requires_private_authorization(&[ package("private"), package("public"), From 8c7aea5c1554905212a83fc2e9cde96be0e50aea Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 01:23:50 -0400 Subject: [PATCH 10/15] DEN-1721: repin TJSV past expired advisory --- .github/workflows/zed-contract-parity.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/zed-contract-parity.yml b/.github/workflows/zed-contract-parity.yml index cc690cb..92b6d20 100644 --- a/.github/workflows/zed-contract-parity.yml +++ b/.github/workflows/zed-contract-parity.yml @@ -173,7 +173,7 @@ jobs: test -s "$instance_dir/current.json" - name: Admit independent TypeSpec and Draft 2020-12 authorities with TJSV - uses: ORESoftware/typespec-json-schema-validator@8bf7ebda4e35c0d11b5b5e5f865867bee4fc3085 + uses: ORESoftware/typespec-json-schema-validator@bd503465dab8c5148fee722b443ed04ff126c9bf with: typespec: contracts/toolchain/main.tsp schema: contracts/toolchain/authored.schema.json From 36930cdd57c6d6f580288dd375c3e9c50d3da437 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 12:12:36 -0500 Subject: [PATCH 11/15] DEN-2859: complete private artifact summary fixture --- src/routes/artifacts.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/artifacts.rs b/src/routes/artifacts.rs index 3d643ab..ca1aa71 100644 --- a/src/routes/artifacts.rs +++ b/src/routes/artifacts.rs @@ -153,6 +153,7 @@ mod tests { latest_version: None, download_count: 0, version_count: 1, + updated_at: chrono::Utc::now().fixed_offset(), } } From b8515e127d6556a18e51c820d39579d5d1fd0fcf Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 12:17:06 -0500 Subject: [PATCH 12/15] DEN-2859: place artifact tests after route items --- src/routes/artifacts.rs | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/routes/artifacts.rs b/src/routes/artifacts.rs index ca1aa71..63faa4e 100644 --- a/src/routes/artifacts.rs +++ b/src/routes/artifacts.rs @@ -131,6 +131,35 @@ pub async fn get_artifact( } } +pub async fn get_file( + State(state): State>, + Path((org_slug, name, ver, path)): Path<(String, String, String, String)>, +) -> ApiResult { + let org_row = find_org(&state, &org_slug).await?; + let pkg = find_package(&state, &org_row, &name).await?; + let row = version::Entity::find() + .filter(version::Column::PackageId.eq(pkg.id)) + .filter(version::Column::Version.eq(&ver)) + .one(&state.db) + .await? + .ok_or_else(|| ApiErr::not_found("version"))?; + let archive = state.store.get_bytes(&row.artifact_key).await?; + // Decompression is CPU-bound and can run long on a large artifact. Left + // inline it blocks a tokio worker, and a future that never yields cannot be + // interrupted by the router's TimeoutLayer — so the request keeps burning + // CPU past the timeout. Hand it to the blocking pool. + let format = artifact_format(&row.format); + let want = path.clone(); + let file = tokio::task::spawn_blocking(move || files::extract_file(&archive, format, &want)) + .await + .map_err(|err| ApiErr::from(anyhow::anyhow!("extract task failed: {err}")))? + .map_err(ApiErr::from)? + .ok_or_else(|| ApiErr::not_found("file"))?; + // Active-content types are neutralized and the response is sandboxed so + // author-published files cannot run as active content from this origin (H2). + Ok((StatusCode::OK, files::served_file_headers(&path), file).into_response()) +} + #[cfg(test)] mod tests { use super::*; @@ -177,32 +206,3 @@ mod tests { ])); } } - -pub async fn get_file( - State(state): State>, - Path((org_slug, name, ver, path)): Path<(String, String, String, String)>, -) -> ApiResult { - let org_row = find_org(&state, &org_slug).await?; - let pkg = find_package(&state, &org_row, &name).await?; - let row = version::Entity::find() - .filter(version::Column::PackageId.eq(pkg.id)) - .filter(version::Column::Version.eq(&ver)) - .one(&state.db) - .await? - .ok_or_else(|| ApiErr::not_found("version"))?; - let archive = state.store.get_bytes(&row.artifact_key).await?; - // Decompression is CPU-bound and can run long on a large artifact. Left - // inline it blocks a tokio worker, and a future that never yields cannot be - // interrupted by the router's TimeoutLayer — so the request keeps burning - // CPU past the timeout. Hand it to the blocking pool. - let format = artifact_format(&row.format); - let want = path.clone(); - let file = tokio::task::spawn_blocking(move || files::extract_file(&archive, format, &want)) - .await - .map_err(|err| ApiErr::from(anyhow::anyhow!("extract task failed: {err}")))? - .map_err(ApiErr::from)? - .ok_or_else(|| ApiErr::not_found("file"))?; - // Active-content types are neutralized and the response is sandboxed so - // author-published files cannot run as active content from this origin (H2). - Ok((StatusCode::OK, files::served_file_headers(&path), file).into_response()) -} From 5ebb17b045d3f3a55c2af5b41d13186575a76ac1 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 12:19:05 -0500 Subject: [PATCH 13/15] release: advance API candidate to 0.1.2 --- .zpkg.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zpkg.toml b/.zpkg.toml index 0b4e4f5..b0452df 100644 --- a/.zpkg.toml +++ b/.zpkg.toml @@ -1,7 +1,7 @@ [package] org = "zed-pkg" name = "zed-api-server" -version = "0.1.1" +version = "0.1.2" description = "Zed registry and account API server with PostgreSQL metadata and S3/R2 artifact storage" license = "MIT" keywords = ["zed-pkg", "package-manager", "registry", "postgres", "r2"] From b655ad402c18cbc21aaa821212113af068ae5647 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 12:19:07 -0500 Subject: [PATCH 14/15] release: align Cargo package at 0.1.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b509e1f..a52e3ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed-api-server" -version = "0.1.1" +version = "0.1.2" edition = "2024" description = "zed-pkg registry REST API: Postgres metadata, S3/R2 artifacts" license = "MIT" From 3d5c073e85611f665888256cfb1bb880506188b6 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 27 Sep 2026 12:19:09 -0500 Subject: [PATCH 15/15] release: align locked API package version --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b42a348..018efff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5388,7 +5388,7 @@ dependencies = [ [[package]] name = "zed-api-server" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "aws-config",