feat: add backend resource fetch api

This commit is contained in:
2026-07-08 20:18:38 +09:00
parent 1a2097b10d
commit 57e96d3be8
16 changed files with 859 additions and 41 deletions
+105 -1
View File
@@ -4,6 +4,7 @@ use crate::profile_archive::{
ProfileArchiveError, ProfileSourceArchive, ProfileSourceArchiveRef,
VerifiedProfileSourceArchive,
};
use crate::resource::{BackendResourceHandle, validate_resource_handle_text};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@@ -24,6 +25,8 @@ pub struct ConfigBundle {
pub declarations: Vec<ConfigDeclaration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_archive: Option<ProfileSourceArchive>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_archive_handle: Option<BackendResourceHandle>,
}
impl ConfigBundle {
@@ -75,6 +78,27 @@ impl ConfigBundle {
lines.push(format!("profile_archive_entrypoint\0{selector}\0{path}"));
}
}
if let Some(handle) = &self.profile_source_archive_handle {
lines.push(format!(
"profile_archive_handle\0{}\0{}\0{}\0{}\0{}",
handle.workspace_id,
handle.resource_id,
handle.digest,
handle.revision,
handle.max_bytes
));
for (selector, path) in handle
.profile_source_graph
.as_ref()
.map(|graph| &graph.entrypoints)
.into_iter()
.flatten()
{
lines.push(format!(
"profile_archive_handle_entrypoint\0{selector}\0{path}"
));
}
}
lines.sort();
let mut hasher = Sha256::new();
@@ -105,7 +129,12 @@ impl ConfigBundle {
profile_source_archive: self
.profile_source_archive
.as_ref()
.map(|archive| archive.reference.source_graph.clone()),
.map(|archive| archive.reference.source_graph.clone())
.or_else(|| {
self.profile_source_archive_handle
.as_ref()
.and_then(|handle| handle.profile_source_graph.clone())
}),
}
}
@@ -253,6 +282,33 @@ pub(crate) fn validate_config_bundle(bundle: &ConfigBundle) -> Result<(), Runtim
RuntimeError::InvalidRequest(format!("invalid profile source archive: {err}"))
})?;
}
if let Some(handle) = &bundle.profile_source_archive_handle {
for (label, value) in [
("resource handle workspace id", handle.workspace_id.as_str()),
("resource handle resource id", handle.resource_id.as_str()),
("resource handle digest", handle.digest.as_str()),
("resource handle nonce", handle.nonce.as_str()),
("resource handle revision", handle.revision.as_str()),
("resource handle content type", handle.content_type.as_str()),
(
"resource handle audit correlation id",
handle.audit_correlation_id.as_str(),
),
] {
validate_resource_handle_text(label, value).map_err(RuntimeError::InvalidRequest)?;
}
if !handle.digest.starts_with("sha256:") {
return Err(RuntimeError::InvalidRequest(
"resource handle digest must use sha256:<hex>".to_string(),
));
}
if handle.profile_source_graph.is_none() {
return Err(RuntimeError::InvalidRequest(
"profile source archive resource handle must include source graph summary"
.to_string(),
));
}
}
Ok(())
}
@@ -529,6 +585,7 @@ mod tests {
reference: reference.to_string(),
}],
profile_source_archive: None,
profile_source_archive_handle: None,
}
.with_computed_digest()
}
@@ -560,6 +617,53 @@ mod tests {
validate_config_bundle(&bundle_with_declaration("vault:team.api-key")).unwrap();
}
#[test]
fn bundle_summary_redacts_runtime_internal_resource_handle() {
let mut bundle = bundle_with_declaration("secret:github-token");
let source_graph = crate::profile_archive::ProfileSourceGraphSummary {
entrypoints: std::collections::BTreeMap::from([(
"default".to_string(),
"profiles/default.dcdl".to_string(),
)]),
source_count: 1,
import_count: 0,
total_source_bytes: 16,
};
bundle.profile_source_archive_handle = Some(crate::resource::BackendResourceHandle {
kind: crate::resource::BackendResourceKind::ProfileSourceArchive,
workspace_id: "workspace-1".to_string(),
scope_id: Some("scope-1".to_string()),
runtime_id: Some("runtime-1".to_string()),
worker_id: Some("worker-1".to_string()),
resource_id: "profile-source-archive:test".to_string(),
digest: "sha256:0123456789abcdef".to_string(),
operation: crate::resource::BackendResourceOperation::FetchArchive,
expires_at_unix_seconds: 4_102_444_800,
nonce: "nonce-not-for-browser".to_string(),
revision: "rev-1".to_string(),
generation: Some(1),
max_bytes: 128,
content_type: crate::resource::PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE.to_string(),
redaction: crate::resource::ResourceRedactionPolicy::RuntimeInternalOnly,
audit_correlation_id: "audit-1".to_string(),
profile_source_graph: Some(source_graph),
});
let rendered = serde_json::to_string(&bundle.summary()).unwrap();
assert!(rendered.contains("profile_source_archive"));
for forbidden in [
"nonce-not-for-browser",
"profile-source-archive:test",
"sha256:0123456789abcdef",
"runtime-1",
"worker-1",
] {
assert!(
!rendered.contains(forbidden),
"leaked {forbidden}: {rendered}"
);
}
}
#[test]
fn rejects_unsafe_bundle_ids_and_refs() {
for id in ["bundle/1", "bundle?x", "bundle&x", "bundle#x", " bundle"] {