use alkcall::channels::client::ChannelOpenError;
use alkcall::registry::spec::{
AccessControl, ChannelOpenSpec, ErrorDefinition, OperationSpec, OperationType, Visibility,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
pub const OP_SOCKS5_OPEN: &str = "channels/socks5/sub";
pub const SOCKS5_ALPN: &str = "alk/socks5";
pub const SOCKS5_OPEN_SCOPE: &str = "socks5:open";
pub const SOCKS5_BIND_SCOPE: &str = "socks5:bind";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Socks5Params {}
impl Socks5Params {
pub fn as_json() -> serde_json::Value {
json!({})
}
}
pub fn socks5_open_spec() -> OperationSpec {
let open_failed_schema = json!({
"type": "object",
"properties": {
"reason": {
"type": "string",
"enum": [
"dial_failed",
"unknown_resource",
"resource_shortage",
"handler_error",
"timeout"
]
},
"message": { "type": "string" }
},
"required": ["reason", "message"]
});
OperationSpec::new(
OP_SOCKS5_OPEN,
OperationType::Sub,
Visibility::External,
json!({
"type": "object",
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {
"channel_id": { "type": "integer", "minimum": 1 }
},
"required": ["channel_id"]
}),
vec![ErrorDefinition {
code: "channel:open_failed".to_string(),
description: "The channel open failed: the registry's ACL gate refused \
(FORBIDDEN — no socks5:open scope), the params payload did \
not match the schema (INVALID_INPUT), the per-connection \
channel cap was hit (channel:too_many_channels), or the \
establishment phase failed (details.reason ∈ the ADR-049 \
§3 vocabulary). No channel_id is returned — no phantom \
channel (ADR-049)."
.to_string(),
schema: open_failed_schema,
http_status: None,
}],
AccessControl {
required_scopes: vec![SOCKS5_OPEN_SCOPE.to_string()],
required_scopes_any: None,
resource_type: None,
resource_action: None,
},
None,
)
.with_channel_open(ChannelOpenSpec::new(SOCKS5_ALPN))
.with_description(
"Open a SOCKS5 proxy channel (RFC 1928 rides the channel BiStream; \
trivial params — the target arrives in-stream post-negotiation)",
)
}
pub fn establishment_reason(err: &ChannelOpenError) -> Option<&str> {
err.establishment_reason()
}
#[cfg(test)]
mod tests {
use super::*;
use alkcall::protocol::wire::CallError;
#[test]
fn open_spec_matches_adr_001_and_wire_md() {
let spec = socks5_open_spec();
assert_eq!(spec.name, "channels/socks5/sub");
assert_eq!(spec.op_type, OperationType::Sub);
assert_eq!(spec.visibility, Visibility::External);
assert_eq!(spec.input_schema["type"], "object");
assert_eq!(spec.input_schema["additionalProperties"], json!(false));
assert!(spec.input_schema.get("required").is_none());
assert_eq!(
spec.output_schema["properties"]["channel_id"],
json!({"type": "integer", "minimum": 1})
);
assert_eq!(spec.output_schema["required"], json!(["channel_id"]));
assert_eq!(
spec.access_control.required_scopes,
vec![SOCKS5_OPEN_SCOPE.to_string()]
);
assert!(spec.access_control.required_scopes_any.is_none());
let channel_open = spec.channel_open.as_ref().expect("channel-open marker");
assert_eq!(channel_open.alpn, SOCKS5_ALPN);
assert!(spec.description.is_some());
}
#[test]
fn constants_match_adr_001_and_adr_006() {
assert_eq!(OP_SOCKS5_OPEN, "channels/socks5/sub");
assert_eq!(SOCKS5_ALPN, "alk/socks5");
assert_eq!(SOCKS5_OPEN_SCOPE, "socks5:open");
assert_eq!(SOCKS5_BIND_SCOPE, "socks5:bind");
}
#[test]
fn trivial_params_serde_round_trip() {
let v = serde_json::to_value(Socks5Params::default()).unwrap();
assert_eq!(v, json!({}));
assert_eq!(v, Socks5Params::as_json());
let back: Socks5Params = serde_json::from_value(json!({})).unwrap();
assert_eq!(back, Socks5Params::default());
}
#[test]
fn trivial_params_reject_unknown_fields() {
let v = json!({"resource": "sneaky"});
assert!(serde_json::from_value::<Socks5Params>(v).is_err());
let v = json!({"substrate": "tcp"});
assert!(serde_json::from_value::<Socks5Params>(v).is_err());
}
#[test]
fn establishment_reason_reads_the_typed_surface() {
let open_failed = ChannelOpenError::CallFailed {
error: CallError::new("channel:open_failed", "establishment failed", false)
.with_details(json!({"reason": "dial_failed"})),
};
assert_eq!(establishment_reason(&open_failed), Some("dial_failed"));
let registry_gate = ChannelOpenError::CallFailed {
error: CallError::internal("forbidden"),
};
assert_eq!(establishment_reason(®istry_gate), None);
let local_only = ChannelOpenError::MissingChannelId;
assert_eq!(establishment_reason(&local_only), None);
}
#[test]
fn open_spec_declares_the_open_failed_error_definition() {
let spec = socks5_open_spec();
let def = spec
.error_schemas
.iter()
.find(|e| e.code == "channel:open_failed")
.expect("channel:open_failed ErrorDefinition declared");
assert!(
!def.description.is_empty(),
"the failure contract is described"
);
let props = &def.schema["properties"];
assert!(props["message"]["type"] == "string");
assert_eq!(def.schema["required"], json!(["reason", "message"]));
let enum_values = props["reason"]["enum"].as_array().expect("reason enum");
let reasons: Vec<&str> = enum_values.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(
reasons,
vec![
"dial_failed",
"unknown_resource",
"resource_shortage",
"handler_error",
"timeout"
]
);
for reason in &reasons {
assert_eq!(
crate::error::SocksEstablishmentReason::parse(reason).map(|r| r.as_str()),
Some(*reason),
"declared reason {reason} is a typed ADR-049 §3 code"
);
}
}
}