use std::sync::Arc;
use alkcall::protocol::wire::{CallError, ResponseEnvelope};
use alkcall::registry::registration::{make_handler, Handler};
use alkcall::registry::spec::{AccessControl, OperationSpec, OperationType, Visibility};
use serde_json::json;
use super::tokens::{OpsContext, PinToken};
use super::wire::{
declared_error, digest_details_schema, digest_ingress, digest_wire_schema, digests_wire_schema,
envelope_error, json_string_array, json_string_field, namespace_schema, present_wire_schema,
schema_validation_error, store_internal_error, token_wire_schema, BLOB_MISSING, GC_REFUSED,
INVALID_DIGEST, INVALID_RENEWAL_FORM,
};
use crate::Error;
pub fn stat_pair(ctx: Arc<OpsContext>) -> (OperationSpec, Handler) {
let spec = OperationSpec::new(
"blobs/stat",
OperationType::Query,
Visibility::External,
json!({
"type": "object",
"properties": {
"namespace": namespace_schema(),
"digest": digest_wire_schema()
},
"required": ["namespace", "digest"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": { "len": { "type": "integer", "minimum": 0 } },
"required": ["len"],
"additionalProperties": false
}),
vec![
declared_error(
BLOB_MISSING,
"the digest is absent from the pool",
digest_details_schema(),
),
declared_error(
INVALID_DIGEST,
"the digest is not tag-prefixed key hex (lowercase, 42 or 66 chars)",
digest_details_schema(),
),
],
AccessControl {
resource_type: Some("blob-namespace".to_owned()),
resource_action: Some("read".to_owned()),
..AccessControl::default()
},
Some("$.namespace".to_owned()),
)
.with_description(
"the entry's byte length for a digest, no content load (the probe before offering)",
);
let handler = make_handler(move |input, context| {
let ctx = Arc::clone(&ctx);
async move {
let request_id = context.request_id.clone();
let Some(digest_hex) = json_string_field(&input, "digest") else {
return schema_validation_error(&request_id);
};
let key = match digest_ingress(digest_hex) {
Ok(key) => key,
Err(error) => return envelope_error(&request_id, error),
};
match ctx.store().stat(&key.key_bytes()).await {
Ok(Some(meta)) => ResponseEnvelope::ok(request_id, json!({ "len": meta.len })),
Ok(None) => envelope_error(
&request_id,
CallError::new(BLOB_MISSING, "the digest is absent from the pool", false)
.with_details(json!({ "digest": digest_hex })),
),
Err(error) => store_internal_error(&request_id, "stat", error),
}
}
});
(spec, handler)
}
pub fn have_pair(ctx: Arc<OpsContext>) -> (OperationSpec, Handler) {
let spec = OperationSpec::new(
"blobs/have",
OperationType::Query,
Visibility::External,
json!({
"type": "object",
"properties": {
"namespace": namespace_schema(),
"digests": digests_wire_schema(),
"token": token_wire_schema()
},
"required": ["namespace", "digests"],
"additionalProperties": false,
"if": { "required": ["token"] },
"then": { "properties": { "digests": { "minItems": 1, "maxItems": 1 } } }
}),
present_wire_schema(),
vec![
declared_error(
INVALID_RENEWAL_FORM,
"a renewal probe carries exactly one digest alongside its token",
json!({
"type": "object",
"properties": { "digests_len": { "type": "integer", "minimum": 0 } },
"required": ["digests_len"],
"additionalProperties": false
}),
),
declared_error(
INVALID_DIGEST,
"the digest is not tag-prefixed key hex (lowercase, 42 or 66 chars)",
digest_details_schema(),
),
],
AccessControl {
resource_type: Some("blob-namespace".to_owned()),
resource_action: Some("read".to_owned()),
..AccessControl::default()
},
Some("$.namespace".to_owned()),
)
.with_description("the existence probe over digests (index-corresponded present[]); the pin-token renewal form is its single-digest shape");
let handler = make_handler(move |input, context| {
let ctx = Arc::clone(&ctx);
async move {
let request_id = context.request_id.clone();
let Some(namespace) = json_string_field(&input, "namespace") else {
return schema_validation_error(&request_id);
};
let Some(digests_wire) = json_string_array(&input, "digests") else {
return schema_validation_error(&request_id);
};
match json_string_field(&input, "token") {
Some(token_hex) => {
have_renewal(ctx, &request_id, namespace, &digests_wire, token_hex).await
}
None => have_probe(ctx, &request_id, &digests_wire).await,
}
}
});
(spec, handler)
}
async fn have_renewal(
ctx: Arc<OpsContext>,
request_id: &str,
namespace: &str,
digests_wire: &[&str],
token_hex: &str,
) -> ResponseEnvelope {
if digests_wire.len() != 1 {
return envelope_error(
request_id,
CallError::new(
INVALID_RENEWAL_FORM,
"a renewal probe carries exactly one digest alongside its token",
false,
)
.with_details(json!({ "digests_len": digests_wire.len() })),
);
}
let key = match digest_ingress(digests_wire[0]) {
Ok(key) => key,
Err(error) => return envelope_error(request_id, error),
};
let present = match PinToken::from_hex(token_hex) {
Some(token) => ctx.renew(namespace, &token, &key).await.present(),
None => false,
};
ResponseEnvelope::ok(request_id, json!({ "present": [present] }))
}
async fn have_probe(
ctx: Arc<OpsContext>,
request_id: &str,
digests_wire: &[&str],
) -> ResponseEnvelope {
let mut keys = Vec::with_capacity(digests_wire.len());
for digest_hex in digests_wire {
match digest_ingress(digest_hex) {
Ok(key) => keys.push(key),
Err(error) => return envelope_error(request_id, error),
}
}
let mut present = Vec::with_capacity(keys.len());
for key in &keys {
present.push(ctx.store().has(&key.key_bytes()).await.unwrap_or(false));
}
ResponseEnvelope::ok(request_id, json!({ "present": present }))
}
pub fn delete_pair(ctx: Arc<OpsContext>) -> (OperationSpec, Handler) {
let spec = OperationSpec::new(
"blobs/delete",
OperationType::Mutation,
Visibility::Internal,
json!({
"type": "object",
"properties": {
"namespace": namespace_schema(),
"digests": digests_wire_schema()
},
"required": ["namespace", "digests"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": { "deleted": digests_wire_schema() },
"required": ["deleted"],
"additionalProperties": false
}),
vec![
declared_error(
BLOB_MISSING,
"the digest is absent: nothing deleted",
digest_details_schema(),
),
declared_error(
GC_REFUSED,
"the digest is protected (pinned, liveness-live, or protect-callback-named): \
nothing deleted",
digest_details_schema(),
),
declared_error(
INVALID_DIGEST,
"the digest is not tag-prefixed key hex (lowercase, 42 or 66 chars)",
digest_details_schema(),
),
],
AccessControl::default(),
None,
)
.with_description("the operator's correction-flow deletion (internal authority context; failures are fail-fast)");
let handler = make_handler(move |input, context| {
let ctx = Arc::clone(&ctx);
async move {
let request_id = context.request_id.clone();
let Some(digests_wire) = json_string_array(&input, "digests") else {
return schema_validation_error(&request_id);
};
let mut keys = Vec::with_capacity(digests_wire.len());
for digest_hex in &digests_wire {
match digest_ingress(digest_hex) {
Ok(key) => keys.push(key),
Err(error) => return envelope_error(&request_id, error),
}
}
let mut deleted = Vec::with_capacity(digests_wire.len());
for (key, digest_hex) in keys.iter().zip(&digests_wire) {
match ctx.store().delete(&key.key_bytes()).await {
Ok(()) => deleted.push(digest_hex.to_owned()),
Err(Error::Missing) => {
return envelope_error(
&request_id,
CallError::new(
BLOB_MISSING,
"the digest is absent: nothing deleted",
false,
)
.with_details(json!({ "digest": digest_hex })),
);
}
Err(Error::GcRefuse) => {
return envelope_error(
&request_id,
CallError::new(
GC_REFUSED,
"the digest is protected (pinned, liveness-live, or \
protect-callback-named): nothing deleted",
false,
)
.with_details(json!({ "digest": digest_hex })),
);
}
Err(error) => return store_internal_error(&request_id, "delete", error),
}
}
ResponseEnvelope::ok(request_id, json!({ "deleted": deleted }))
}
});
(spec, handler)
}