use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::{
Extension, Json, Router,
routing::{delete, get, post},
};
use scp_core::bridge::shadow::{CreateShadowParams, ShadowRegistry, create_shadow};
use scp_core::bridge::{BridgeMode, ShadowProvenanceStatus};
use scp_core::crypto::sender_keys::SenderKeyStore;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::error::ApiError;
#[derive(Debug, Clone, Serialize)]
pub struct StoredAttestation {
pub attestation_id: String,
pub status: String,
pub bridge_id: String,
pub platform_handle: String,
pub platform_user_id: String,
pub evidence: AttestationEvidence,
pub issued_at: u64,
pub expires_at: u64,
}
#[derive(Debug)]
pub struct BridgeState {
pub registries: RwLock<HashMap<String, ShadowRegistry>>,
pub sender_key_store: RwLock<SenderKeyStore>,
pub attestations: RwLock<HashMap<String, StoredAttestation>>,
pub deleted_shadows: RwLock<HashSet<String>>,
pub processed_event_ids: RwLock<HashSet<String>>,
pub messages: RwLock<Vec<EmittedMessage>>,
pub message_sequence: RwLock<u64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct EmittedMessage {
pub message_id: String,
pub shadow_id: String,
pub content: String,
pub content_type: String,
pub sequence: u64,
pub bridge_provenance: BridgeProvenanceResponse,
}
impl BridgeState {
#[must_use]
pub fn new() -> Self {
Self {
registries: RwLock::new(HashMap::new()),
sender_key_store: RwLock::new(SenderKeyStore::new()),
attestations: RwLock::new(HashMap::new()),
deleted_shadows: RwLock::new(HashSet::new()),
processed_event_ids: RwLock::new(HashSet::new()),
messages: RwLock::new(Vec::new()),
message_sequence: RwLock::new(0),
}
}
}
impl Default for BridgeState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Deserialize)]
pub struct CreateShadowRequest {
pub platform_handle: String,
pub platform_user_id: String,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
pub struct CreateShadowResponse {
pub shadow_id: String,
pub platform_handle: String,
pub platform_user_id: String,
pub attributed_role: String,
pub created_at: u64,
}
#[derive(Debug, Deserialize)]
pub struct AttestRequest {
pub platform_handle: String,
pub platform_user_id: String,
pub attestation_evidence: AttestationEvidence,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AttestationEvidence {
pub evidence_type: String,
pub verification_method: String,
pub verified_at: u64,
pub platform_confidence: String,
#[serde(default)]
pub additional_signals: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
pub struct AttestResponse {
pub attestation_id: String,
pub status: String,
pub platform_handle: String,
pub issued_at: u64,
pub expires_at: u64,
}
const ATTESTATION_TTL_SECS: u64 = 86_400;
#[derive(Debug, Deserialize)]
pub struct EmitMessageRequest {
pub shadow_id: String,
pub content: String,
pub content_type: String,
#[serde(default)]
pub platform_message_id: Option<String>,
#[serde(default)]
pub platform_timestamp: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeProvenanceResponse {
pub originating_platform: String,
pub bridge_mode: String,
pub shadow_status: String,
pub operator_did: String,
}
#[derive(Debug, Serialize)]
pub struct EmitMessageResponse {
pub message_id: String,
pub sequence: u64,
pub bridge_provenance: BridgeProvenanceResponse,
}
#[derive(Debug, Serialize)]
pub struct BridgeStatusResponse {
pub bridge_id: String,
pub status: String,
pub platform: String,
pub mode: String,
pub operator_did: String,
pub registered_at: u64,
pub shadow_count: usize,
pub shadows: Vec<ShadowSummary>,
}
#[derive(Debug, Serialize)]
pub struct ShadowSummary {
pub shadow_id: String,
pub platform_handle: String,
pub attributed_role: String,
pub provenance_status: String,
pub created_at: u64,
}
#[derive(Debug, Deserialize)]
pub struct WebhookRequest {
pub event_type: String,
pub event_id: String,
pub timestamp: u64,
pub payload: serde_json::Value,
}
#[derive(Debug, Serialize)]
pub struct WebhookResponse {
pub accepted: bool,
pub event_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
const VALID_EVENT_TYPES: &[&str] = &[
"message",
"presence",
"identity_update",
"user_departed",
"message_edit",
"message_delete",
];
fn derive_shadow_id(bridge_id: &str, platform_user_id: &str) -> String {
format!("shadow:{bridge_id}:{platform_user_id}")
}
#[allow(clippy::significant_drop_tightening)] async fn create_shadow_handler(
State(bridge_state): State<Arc<BridgeState>>,
Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
Json(body): Json<CreateShadowRequest>,
) -> impl IntoResponse {
if body.platform_handle.is_empty() {
return ApiError::bad_request("platform_handle must not be empty").into_response();
}
if body.platform_user_id.is_empty() {
return ApiError::bad_request("platform_user_id must not be empty").into_response();
}
let bridge_id = &auth_ctx.claims.scp_bridge_id;
let context_id = &auth_ctx.claims.scp_context_id;
let shadow_id = derive_shadow_id(bridge_id, &body.platform_user_id);
let mut registries = bridge_state.registries.write().await;
let registry = registries
.entry(context_id.clone())
.or_insert_with(|| ShadowRegistry::new(context_id.clone()));
if let Some(existing) = registry.shadows().iter().find(|s| s.shadow_id == shadow_id) {
return (
StatusCode::OK,
Json(CreateShadowResponse {
shadow_id: existing.shadow_id.clone(),
platform_handle: existing.platform_handle.clone(),
platform_user_id: body.platform_user_id,
attributed_role: existing.attributed_role.clone(),
created_at: existing.created_at,
}),
)
.into_response();
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let bridge_mode = auth_ctx.bridge.mode.clone();
let params = CreateShadowParams {
shadow_id: &shadow_id,
bridge_id,
bridge_mode,
platform_handle: &body.platform_handle,
context_member_dids: &[],
timestamp: now,
};
let mut sender_key_store = bridge_state.sender_key_store.write().await;
match create_shadow(registry, &mut sender_key_store, ¶ms) {
Ok((shadow, _event)) => (
StatusCode::CREATED,
Json(CreateShadowResponse {
shadow_id: shadow.shadow_id,
platform_handle: shadow.platform_handle,
platform_user_id: body.platform_user_id,
attributed_role: shadow.attributed_role,
created_at: shadow.created_at,
}),
)
.into_response(),
Err(e) => ApiError::internal_error(e.to_string()).into_response(),
}
}
fn is_valid_confidence(value: &str) -> bool {
matches!(value, "high" | "medium" | "low")
}
#[allow(clippy::significant_drop_tightening)] async fn attest_handler(
State(bridge_state): State<Arc<BridgeState>>,
Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
Json(body): Json<AttestRequest>,
) -> impl IntoResponse {
if body.platform_handle.is_empty() {
return ApiError::bad_request("platform_handle must not be empty").into_response();
}
if body.platform_user_id.is_empty() {
return ApiError::bad_request("platform_user_id must not be empty").into_response();
}
if body.attestation_evidence.evidence_type.is_empty() {
return ApiError::bad_request("attestation_evidence.evidence_type must not be empty")
.into_response();
}
if body.attestation_evidence.verification_method.is_empty() {
return ApiError::bad_request("attestation_evidence.verification_method must not be empty")
.into_response();
}
if !is_valid_confidence(&body.attestation_evidence.platform_confidence) {
return ApiError::bad_request(
"attestation_evidence.platform_confidence must be \"high\", \"medium\", or \"low\"",
)
.into_response();
}
let bridge_id = &auth_ctx.claims.scp_bridge_id;
let attestation_id = format!("attest:{bridge_id}:{}", body.platform_user_id);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let stored = StoredAttestation {
attestation_id: attestation_id.clone(),
status: "active".to_owned(),
bridge_id: bridge_id.clone(),
platform_handle: body.platform_handle.clone(),
platform_user_id: body.platform_user_id,
evidence: body.attestation_evidence,
issued_at: now,
expires_at: now + ATTESTATION_TTL_SECS,
};
let response = AttestResponse {
attestation_id: stored.attestation_id.clone(),
status: stored.status.clone(),
platform_handle: stored.platform_handle.clone(),
issued_at: stored.issued_at,
expires_at: stored.expires_at,
};
let mut attestations = bridge_state.attestations.write().await;
attestations.insert(attestation_id, stored);
(StatusCode::CREATED, Json(response)).into_response()
}
fn find_shadow(
registries: &HashMap<String, ShadowRegistry>,
shadow_id: &str,
) -> Option<(String, scp_core::bridge::ShadowIdentity)> {
for (ctx_id, registry) in registries {
if let Some(shadow) = registry.shadows().iter().find(|s| s.shadow_id == shadow_id) {
return Some((ctx_id.clone(), shadow.clone()));
}
}
None
}
async fn emit_message_handler(
State(bridge_state): State<Arc<BridgeState>>,
Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
Json(body): Json<EmitMessageRequest>,
) -> impl IntoResponse {
if body.shadow_id.is_empty() {
return ApiError::bad_request("shadow_id must not be empty").into_response();
}
if body.content.is_empty() {
return ApiError::bad_request("content must not be empty").into_response();
}
if body.content_type.is_empty() {
return ApiError::bad_request("content_type must not be empty").into_response();
}
let registries = bridge_state.registries.read().await;
let deleted = bridge_state.deleted_shadows.read().await;
if deleted.contains(&body.shadow_id) {
return ApiError::not_found("SHADOW_NOT_FOUND: shadow has been deleted").into_response();
}
let shadow_info = find_shadow(®istries, &body.shadow_id);
drop(registries);
drop(deleted);
let Some((_ctx_id, shadow)) = shadow_info else {
return (
StatusCode::NOT_FOUND,
Json(ApiError {
error: "shadow not found".to_owned(),
code: "SHADOW_NOT_FOUND".to_owned(),
}),
)
.into_response();
};
let shadow_status = match shadow.provenance_status {
ShadowProvenanceStatus::Shadow => "Shadow",
ShadowProvenanceStatus::Claimed => "Claimed",
};
let bridge_mode_str = match auth_ctx.bridge.mode {
BridgeMode::Relay => "Relay",
BridgeMode::Puppet => "Puppet",
BridgeMode::Api => "Api",
BridgeMode::Cooperative => "Cooperative",
};
let provenance_resp = BridgeProvenanceResponse {
originating_platform: auth_ctx.bridge.platform.clone(),
bridge_mode: bridge_mode_str.to_owned(),
shadow_status: shadow_status.to_owned(),
operator_did: auth_ctx.bridge.operator_did.0.clone(),
};
let mut seq = bridge_state.message_sequence.write().await;
*seq += 1;
let sequence = *seq;
drop(seq);
let message_id = format!("msg:{}:{sequence}", auth_ctx.claims.scp_bridge_id);
let emitted = EmittedMessage {
message_id: message_id.clone(),
shadow_id: body.shadow_id,
content: body.content,
content_type: body.content_type,
sequence,
bridge_provenance: provenance_resp.clone(),
};
bridge_state.messages.write().await.push(emitted);
(
StatusCode::ACCEPTED,
Json(EmitMessageResponse {
message_id,
sequence,
bridge_provenance: provenance_resp,
}),
)
.into_response()
}
#[allow(clippy::significant_drop_tightening)] async fn status_handler(
State(bridge_state): State<Arc<BridgeState>>,
Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
) -> impl IntoResponse {
let registries = bridge_state.registries.read().await;
let deleted = bridge_state.deleted_shadows.read().await;
let mut shadows = Vec::new();
for registry in registries.values() {
for shadow in registry.shadows() {
if !deleted.contains(&shadow.shadow_id) {
let status_str = match shadow.provenance_status {
ShadowProvenanceStatus::Shadow => "Shadow",
ShadowProvenanceStatus::Claimed => "Claimed",
};
shadows.push(ShadowSummary {
shadow_id: shadow.shadow_id.clone(),
platform_handle: shadow.platform_handle.clone(),
attributed_role: shadow.attributed_role.clone(),
provenance_status: status_str.to_owned(),
created_at: shadow.created_at,
});
}
}
}
let bridge_mode_str = match auth_ctx.bridge.mode {
BridgeMode::Relay => "Relay",
BridgeMode::Puppet => "Puppet",
BridgeMode::Api => "Api",
BridgeMode::Cooperative => "Cooperative",
};
let status_str = match auth_ctx.bridge.status {
scp_core::bridge::BridgeStatus::Active => "Active",
scp_core::bridge::BridgeStatus::Suspended => "Suspended",
scp_core::bridge::BridgeStatus::Revoked => "Revoked",
};
let shadow_count = shadows.len();
let resp = BridgeStatusResponse {
bridge_id: auth_ctx.bridge.bridge_id.clone(),
status: status_str.to_owned(),
platform: auth_ctx.bridge.platform.clone(),
mode: bridge_mode_str.to_owned(),
operator_did: auth_ctx.bridge.operator_did.0.clone(),
registered_at: auth_ctx.bridge.registered_at,
shadow_count,
shadows,
};
(StatusCode::OK, Json(resp)).into_response()
}
async fn delete_shadow_handler(
State(bridge_state): State<Arc<BridgeState>>,
Extension(_auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
Path(shadow_id): Path<String>,
) -> impl IntoResponse {
let deleted = bridge_state.deleted_shadows.read().await;
if deleted.contains(&shadow_id) {
return StatusCode::NO_CONTENT.into_response();
}
drop(deleted);
let registries = bridge_state.registries.read().await;
let shadow_info = find_shadow(®istries, &shadow_id);
drop(registries);
match shadow_info {
None => (
StatusCode::NOT_FOUND,
Json(ApiError {
error: "shadow not found".to_owned(),
code: "SHADOW_NOT_FOUND".to_owned(),
}),
)
.into_response(),
Some((_ctx_id, shadow)) => {
if shadow.provenance_status == ShadowProvenanceStatus::Claimed {
return (
StatusCode::CONFLICT,
Json(ApiError {
error: "shadow has been claimed and cannot be deleted".to_owned(),
code: "SHADOW_ALREADY_CLAIMED".to_owned(),
}),
)
.into_response();
}
bridge_state.deleted_shadows.write().await.insert(shadow_id);
StatusCode::NO_CONTENT.into_response()
}
}
}
fn extract_shadow_id(payload: &serde_json::Value) -> &str {
payload
.get("shadow_id")
.and_then(|v| v.as_str())
.unwrap_or("")
}
fn webhook_reject(event_id: String, reason: &str) -> axum::response::Response {
(
StatusCode::OK,
Json(WebhookResponse {
accepted: false,
event_id,
reason: Some(reason.to_owned()),
}),
)
.into_response()
}
async fn process_webhook_event(
bridge_state: &BridgeState,
event_type: &str,
event_id: &str,
payload: &serde_json::Value,
) -> Option<String> {
match event_type {
"message" => {
let shadow_id = extract_shadow_id(payload);
if shadow_id.is_empty() {
return Some("payload.shadow_id is required for message events".to_owned());
}
let registries = bridge_state.registries.read().await;
let exists = find_shadow(®istries, shadow_id).is_some();
drop(registries);
if !exists {
return Some("shadow not found".to_owned());
}
}
"identity_update" => {
let shadow_id = extract_shadow_id(payload);
if !shadow_id.is_empty() {
let registries = bridge_state.registries.read().await;
let exists = find_shadow(®istries, shadow_id).is_some();
drop(registries);
if !exists {
return Some("shadow not found for identity_update".to_owned());
}
}
}
"user_departed" => {
let shadow_id = extract_shadow_id(payload);
if !shadow_id.is_empty() {
bridge_state
.deleted_shadows
.write()
.await
.insert(shadow_id.to_owned());
}
}
_ => {}
}
let _ = event_id; None
}
async fn webhook_handler(
State(bridge_state): State<Arc<BridgeState>>,
Extension(_auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
Json(body): Json<WebhookRequest>,
) -> impl IntoResponse {
if !VALID_EVENT_TYPES.contains(&body.event_type.as_str()) {
return webhook_reject(
body.event_id,
&format!("unknown event_type: {}", body.event_type),
);
}
if body.event_id.is_empty() {
return ApiError::bad_request("event_id must not be empty").into_response();
}
{
let processed = bridge_state.processed_event_ids.read().await;
if processed.contains(&body.event_id) {
return (
StatusCode::OK,
Json(WebhookResponse {
accepted: true,
event_id: body.event_id,
reason: None,
}),
)
.into_response();
}
}
if let Some(reason) = process_webhook_event(
&bridge_state,
&body.event_type,
&body.event_id,
&body.payload,
)
.await
{
return webhook_reject(body.event_id, &reason);
}
bridge_state
.processed_event_ids
.write()
.await
.insert(body.event_id.clone());
(
StatusCode::OK,
Json(WebhookResponse {
accepted: true,
event_id: body.event_id,
reason: None,
}),
)
.into_response()
}
pub fn bridge_router(state: Arc<BridgeState>) -> Router {
Router::new()
.route("/v1/scp/bridge/shadow", post(create_shadow_handler))
.route(
"/v1/scp/bridge/shadow/{shadow_id}",
delete(delete_shadow_handler),
)
.route("/v1/scp/bridge/attest", post(attest_handler))
.route("/v1/scp/bridge/message", post(emit_message_handler))
.route("/v1/scp/bridge/status", get(status_handler))
.route("/v1/scp/bridge/webhook", post(webhook_handler))
.with_state(state)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::needless_pass_by_value,
clippy::significant_drop_tightening
)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use http_body_util::BodyExt;
use scp_core::bridge::{BridgeConnector, BridgeMode, BridgeStatus};
use tower::ServiceExt;
use crate::bridge_auth::{BridgeAuthContext, BridgeJwtClaims};
fn test_claims() -> BridgeJwtClaims {
BridgeJwtClaims {
iss: "did:dht:z6MkTestOperator".to_owned(),
aud: "https://node.example.com".to_owned(),
iat: 1_700_000_000,
exp: 1_700_003_600,
scp_bridge_id: "bridge-test-001".to_owned(),
scp_context_id: "ctx-test-001".to_owned(),
}
}
fn test_auth_ctx() -> BridgeAuthContext {
BridgeAuthContext {
claims: test_claims(),
bridge: BridgeConnector {
bridge_id: "bridge-test-001".to_owned(),
operator_did: scp_identity::DID("did:dht:z6MkTestOperator".to_owned()),
platform: "discord".to_owned(),
mode: BridgeMode::Relay,
status: BridgeStatus::Active,
registration_context: "ctx-test-001".to_owned(),
registered_at: 1_700_000_000,
},
}
}
fn test_app(state: Arc<BridgeState>) -> Router {
let auth_ctx = test_auth_ctx();
Router::new()
.route("/v1/scp/bridge/shadow", post(create_shadow_handler))
.route(
"/v1/scp/bridge/shadow/{shadow_id}",
delete(delete_shadow_handler),
)
.route("/v1/scp/bridge/attest", post(attest_handler))
.route("/v1/scp/bridge/message", post(emit_message_handler))
.route("/v1/scp/bridge/status", get(status_handler))
.route("/v1/scp/bridge/webhook", post(webhook_handler))
.layer(axum::Extension(auth_ctx))
.with_state(state)
}
fn create_request(body: serde_json::Value) -> Request<Body> {
Request::builder()
.method("POST")
.uri("/v1/scp/bridge/shadow")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).expect("test")))
.expect("test")
}
fn attest_request(body: serde_json::Value) -> Request<Body> {
Request::builder()
.method("POST")
.uri("/v1/scp/bridge/attest")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).expect("test")))
.expect("test")
}
async fn response_json(resp: axum::response::Response) -> serde_json::Value {
let bytes = resp.into_body().collect().await.expect("test").to_bytes();
serde_json::from_slice(&bytes).expect("test")
}
#[tokio::test]
async fn successful_creation_returns_201() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = create_request(serde_json::json!({
"platform_handle": "@alice#1234",
"platform_user_id": "user-alice-001"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let json = response_json(resp).await;
assert_eq!(json["platform_handle"], "@alice#1234");
assert_eq!(json["platform_user_id"], "user-alice-001");
assert_eq!(json["attributed_role"], "observer");
assert_eq!(json["shadow_id"], "shadow:bridge-test-001:user-alice-001");
assert!(json["created_at"].as_u64().is_some());
}
#[tokio::test]
async fn idempotent_creation_returns_200() {
let state = Arc::new(BridgeState::new());
let app = test_app(Arc::clone(&state));
let req = create_request(serde_json::json!({
"platform_handle": "@alice#1234",
"platform_user_id": "user-alice-001"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let app = test_app(state);
let req = create_request(serde_json::json!({
"platform_handle": "@alice#1234",
"platform_user_id": "user-alice-001"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["shadow_id"], "shadow:bridge-test-001:user-alice-001");
assert_eq!(json["attributed_role"], "observer");
}
#[tokio::test]
async fn missing_platform_handle_returns_400() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = create_request(serde_json::json!({
"platform_handle": "",
"platform_user_id": "user-001"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn missing_platform_user_id_returns_400() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = create_request(serde_json::json!({
"platform_handle": "@alice",
"platform_user_id": ""
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn missing_required_fields_returns_400() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = create_request(serde_json::json!({}));
let resp = app.oneshot(req).await.expect("test");
assert!(
resp.status() == StatusCode::BAD_REQUEST
|| resp.status() == StatusCode::UNPROCESSABLE_ENTITY
);
}
fn valid_attest_body() -> serde_json::Value {
serde_json::json!({
"platform_handle": "@dave#1234",
"platform_user_id": "usr_abc123",
"attestation_evidence": {
"evidence_type": "platform-verified",
"verification_method": "oauth2",
"verified_at": 1_700_000_300,
"platform_confidence": "high",
"additional_signals": {
"account_age_days": 730,
"email_verified": true
}
}
})
}
#[tokio::test]
async fn attest_successful_returns_201() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = attest_request(valid_attest_body());
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let json = response_json(resp).await;
assert_eq!(json["status"], "active");
assert_eq!(json["platform_handle"], "@dave#1234");
assert_eq!(json["attestation_id"], "attest:bridge-test-001:usr_abc123");
assert!(json["issued_at"].as_u64().is_some());
assert!(json["expires_at"].as_u64().is_some());
let issued = json["issued_at"].as_u64().unwrap();
let expires = json["expires_at"].as_u64().unwrap();
assert_eq!(expires - issued, 86_400);
}
#[tokio::test]
async fn attest_stores_attestation() {
let state = Arc::new(BridgeState::new());
let app = test_app(Arc::clone(&state));
let req = attest_request(valid_attest_body());
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let attestations = state.attestations.read().await;
let stored = attestations
.get("attest:bridge-test-001:usr_abc123")
.expect("attestation should be stored");
assert_eq!(stored.platform_handle, "@dave#1234");
assert_eq!(stored.evidence.evidence_type, "platform-verified");
assert_eq!(stored.evidence.platform_confidence, "high");
}
#[tokio::test]
async fn attest_empty_handle_returns_400() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = attest_request(serde_json::json!({
"platform_handle": "",
"platform_user_id": "usr_abc123",
"attestation_evidence": {
"evidence_type": "platform-verified",
"verification_method": "oauth2",
"verified_at": 1_700_000_300,
"platform_confidence": "high"
}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn attest_empty_user_id_returns_400() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = attest_request(serde_json::json!({
"platform_handle": "@dave#1234",
"platform_user_id": "",
"attestation_evidence": {
"evidence_type": "platform-verified",
"verification_method": "oauth2",
"verified_at": 1_700_000_300,
"platform_confidence": "high"
}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn attest_invalid_confidence_returns_400() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = attest_request(serde_json::json!({
"platform_handle": "@dave#1234",
"platform_user_id": "usr_abc123",
"attestation_evidence": {
"evidence_type": "platform-verified",
"verification_method": "oauth2",
"verified_at": 1_700_000_300,
"platform_confidence": "very-high"
}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn attest_missing_evidence_returns_422() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = attest_request(serde_json::json!({
"platform_handle": "@dave#1234",
"platform_user_id": "usr_abc123"
}));
let resp = app.oneshot(req).await.expect("test");
assert!(
resp.status() == StatusCode::BAD_REQUEST
|| resp.status() == StatusCode::UNPROCESSABLE_ENTITY
);
}
#[tokio::test]
async fn attest_without_additional_signals() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = attest_request(serde_json::json!({
"platform_handle": "@dave#1234",
"platform_user_id": "usr_abc123",
"attestation_evidence": {
"evidence_type": "oauth2",
"verification_method": "oauth2-flow",
"verified_at": 1_700_000_300,
"platform_confidence": "medium"
}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let json = response_json(resp).await;
assert_eq!(json["status"], "active");
}
fn message_request(body: serde_json::Value) -> Request<Body> {
Request::builder()
.method("POST")
.uri("/v1/scp/bridge/message")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).expect("test")))
.expect("test")
}
async fn create_test_shadow(state: &Arc<BridgeState>) -> String {
let app = test_app(Arc::clone(state));
let req = create_request(serde_json::json!({
"platform_handle": "@emitter#1234",
"platform_user_id": "user-emitter-001"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let json = response_json(resp).await;
json["shadow_id"].as_str().expect("shadow_id").to_owned()
}
#[tokio::test]
async fn emit_message_returns_202() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(Arc::clone(&state));
let req = message_request(serde_json::json!({
"shadow_id": shadow_id,
"content": "Hello from bridge!",
"content_type": "text/plain"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let json = response_json(resp).await;
assert!(json["message_id"].as_str().is_some());
assert_eq!(json["sequence"], 1);
assert_eq!(json["bridge_provenance"]["originating_platform"], "discord");
assert_eq!(json["bridge_provenance"]["bridge_mode"], "Relay");
assert_eq!(json["bridge_provenance"]["shadow_status"], "Shadow");
assert_eq!(
json["bridge_provenance"]["operator_did"],
"did:dht:z6MkTestOperator"
);
}
#[tokio::test]
async fn emit_message_shadow_not_found_returns_404() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = message_request(serde_json::json!({
"shadow_id": "shadow:nonexistent",
"content": "Hello",
"content_type": "text/plain"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn emit_message_empty_content_returns_400() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(state);
let req = message_request(serde_json::json!({
"shadow_id": shadow_id,
"content": "",
"content_type": "text/plain"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
fn status_request() -> Request<Body> {
Request::builder()
.method("GET")
.uri("/v1/scp/bridge/status")
.body(Body::empty())
.expect("test")
}
#[tokio::test]
async fn status_returns_bridge_info() {
let state = Arc::new(BridgeState::new());
let _shadow_id = create_test_shadow(&state).await;
let app = test_app(state);
let req = status_request();
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["bridge_id"], "bridge-test-001");
assert_eq!(json["status"], "Active");
assert_eq!(json["platform"], "discord");
assert_eq!(json["mode"], "Relay");
assert_eq!(json["operator_did"], "did:dht:z6MkTestOperator");
assert_eq!(json["shadow_count"], 1);
assert_eq!(json["shadows"].as_array().map(std::vec::Vec::len), Some(1));
}
#[tokio::test]
async fn status_empty_returns_zero_shadows() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = status_request();
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["shadow_count"], 0);
}
fn delete_shadow_request(shadow_id: &str) -> Request<Body> {
Request::builder()
.method("DELETE")
.uri(format!("/v1/scp/bridge/shadow/{shadow_id}"))
.body(Body::empty())
.expect("test")
}
#[tokio::test]
async fn delete_shadow_returns_204() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(state);
let req = delete_shadow_request(&shadow_id);
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}
#[tokio::test]
async fn delete_shadow_not_found_returns_404() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = delete_shadow_request("shadow:nonexistent");
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn delete_shadow_idempotent_returns_204() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(Arc::clone(&state));
let req = delete_shadow_request(&shadow_id);
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
let app = test_app(state);
let req = delete_shadow_request(&shadow_id);
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}
fn webhook_request(body: serde_json::Value) -> Request<Body> {
Request::builder()
.method("POST")
.uri("/v1/scp/bridge/webhook")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).expect("test")))
.expect("test")
}
#[tokio::test]
async fn webhook_message_event_accepted() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(state);
let req = webhook_request(serde_json::json!({
"event_type": "message",
"event_id": "evt-001",
"timestamp": 1_700_000_500,
"payload": {
"shadow_id": shadow_id,
"content": "Hello from webhook"
}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["accepted"], true);
assert_eq!(json["event_id"], "evt-001");
}
#[tokio::test]
async fn webhook_deduplication() {
let state = Arc::new(BridgeState::new());
let app = test_app(Arc::clone(&state));
let req = webhook_request(serde_json::json!({
"event_type": "presence",
"event_id": "evt-dedup-001",
"timestamp": 1_700_000_500,
"payload": {}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["accepted"], true);
let app = test_app(state);
let req = webhook_request(serde_json::json!({
"event_type": "presence",
"event_id": "evt-dedup-001",
"timestamp": 1_700_000_600,
"payload": {}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["accepted"], true);
}
#[tokio::test]
async fn webhook_unknown_event_type_rejected() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = webhook_request(serde_json::json!({
"event_type": "unknown_type",
"event_id": "evt-002",
"timestamp": 1_700_000_500,
"payload": {}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["accepted"], false);
assert!(json["reason"].as_str().is_some());
}
#[tokio::test]
async fn webhook_user_departed_triggers_shadow_deletion() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(Arc::clone(&state));
let req = webhook_request(serde_json::json!({
"event_type": "user_departed",
"event_id": "evt-depart-001",
"timestamp": 1_700_000_500,
"payload": {
"shadow_id": shadow_id
}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(json["accepted"], true);
let deleted = state.deleted_shadows.read().await;
assert!(deleted.contains(&shadow_id));
}
#[tokio::test]
async fn webhook_all_event_types_accepted() {
let state = Arc::new(BridgeState::new());
for (i, event_type) in [
"message",
"presence",
"identity_update",
"user_departed",
"message_edit",
"message_delete",
]
.iter()
.enumerate()
{
let shadow_id = if *event_type == "message" {
create_test_shadow(&state).await
} else {
String::new()
};
let payload = if *event_type == "message" {
serde_json::json!({ "shadow_id": shadow_id, "content": "test" })
} else {
serde_json::json!({})
};
let app = test_app(Arc::clone(&state));
let req = webhook_request(serde_json::json!({
"event_type": event_type,
"event_id": format!("evt-type-{i}"),
"timestamp": 1_700_000_500,
"payload": payload
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let json = response_json(resp).await;
assert_eq!(
json["accepted"], true,
"event type '{event_type}' should be accepted"
);
}
}
#[tokio::test]
async fn integration_full_lifecycle() {
let state = Arc::new(BridgeState::new());
let app = test_app(Arc::clone(&state));
let req = create_request(serde_json::json!({
"platform_handle": "@lifecycle-user#1234",
"platform_user_id": "lifecycle-user-001"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let create_json = response_json(resp).await;
let shadow_id = create_json["shadow_id"]
.as_str()
.expect("shadow_id")
.to_owned();
assert_eq!(create_json["attributed_role"], "observer");
let app = test_app(Arc::clone(&state));
let req = message_request(serde_json::json!({
"shadow_id": &shadow_id,
"content": "Hello from lifecycle test!",
"content_type": "text/plain",
"platform_message_id": "ext-msg-001",
"platform_timestamp": 1_700_001_000
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let msg_json = response_json(resp).await;
assert!(msg_json["message_id"].as_str().is_some());
assert_eq!(msg_json["sequence"], 1);
assert_eq!(
msg_json["bridge_provenance"]["originating_platform"],
"discord"
);
assert_eq!(msg_json["bridge_provenance"]["bridge_mode"], "Relay");
assert_eq!(
msg_json["bridge_provenance"]["operator_did"],
"did:dht:z6MkTestOperator"
);
assert_eq!(msg_json["bridge_provenance"]["shadow_status"], "Shadow");
let app = test_app(Arc::clone(&state));
let req = attest_request(serde_json::json!({
"platform_handle": "@lifecycle-user#1234",
"platform_user_id": "lifecycle-user-001",
"attestation_evidence": {
"evidence_type": "platform-verified",
"verification_method": "oauth2",
"verified_at": 1_700_001_200,
"platform_confidence": "high"
}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
let app = test_app(Arc::clone(&state));
let req = status_request();
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let status_json = response_json(resp).await;
assert_eq!(status_json["bridge_id"], "bridge-test-001");
assert_eq!(status_json["status"], "Active");
assert_eq!(status_json["shadow_count"], 1);
let shadows = status_json["shadows"].as_array().expect("shadows array");
assert_eq!(shadows.len(), 1);
assert_eq!(shadows[0]["shadow_id"], shadow_id);
let app = test_app(Arc::clone(&state));
let req = webhook_request(serde_json::json!({
"event_type": "presence",
"event_id": "evt-lifecycle-001",
"timestamp": 1_700_001_500,
"payload": {}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let wh_json = response_json(resp).await;
assert_eq!(wh_json["accepted"], true);
let app = test_app(Arc::clone(&state));
let req = delete_shadow_request(&shadow_id);
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
let app = test_app(state);
let req = status_request();
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::OK);
let status_after = response_json(resp).await;
assert_eq!(status_after["shadow_count"], 0);
}
#[tokio::test]
async fn all_endpoints_return_json_content_type() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(Arc::clone(&state));
let req = create_request(serde_json::json!({
"platform_handle": "@ct-user",
"platform_user_id": "ct-user-001"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("application/json"),
"POST /shadow must return application/json"
);
let app = test_app(Arc::clone(&state));
let req = message_request(serde_json::json!({
"shadow_id": &shadow_id,
"content": "content-type test",
"content_type": "text/plain"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("application/json"),
"POST /message must return application/json"
);
let app = test_app(Arc::clone(&state));
let req = status_request();
let resp = app.oneshot(req).await.expect("test");
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("application/json"),
"GET /status must return application/json"
);
let app = test_app(Arc::clone(&state));
let req = attest_request(valid_attest_body());
let resp = app.oneshot(req).await.expect("test");
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("application/json"),
"POST /attest must return application/json"
);
let app = test_app(Arc::clone(&state));
let req = webhook_request(serde_json::json!({
"event_type": "presence",
"event_id": "ct-evt-001",
"timestamp": 1_700_000_500,
"payload": {}
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("application/json"),
"POST /webhook must return application/json"
);
}
#[tokio::test]
async fn error_responses_use_scp_format() {
let state = Arc::new(BridgeState::new());
let app = test_app(state);
let req = message_request(serde_json::json!({
"shadow_id": "shadow:nonexistent",
"content": "test",
"content_type": "text/plain"
}));
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let json = response_json(resp).await;
assert!(
json["code"].as_str().is_some(),
"error response must have code field"
);
assert!(
json["error"].as_str().is_some(),
"error response must have error field"
);
}
#[tokio::test]
async fn delete_shadow_through_router() {
let state = Arc::new(BridgeState::new());
let shadow_id = create_test_shadow(&state).await;
let app = test_app(Arc::clone(&state));
let req = delete_shadow_request(&shadow_id);
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
let app = test_app(state);
let req = delete_shadow_request("shadow:nonexistent:claimed");
let resp = app.oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let json = response_json(resp).await;
assert_eq!(json["code"], "SHADOW_NOT_FOUND");
}
#[tokio::test]
async fn bridge_router_mounts_all_endpoints() {
let state = Arc::new(BridgeState::new());
let auth_ctx = test_auth_ctx();
let router = bridge_router(state).layer(axum::Extension(auth_ctx));
let req = create_request(serde_json::json!({
"platform_handle": "@router-test",
"platform_user_id": "router-user-001"
}));
let resp = router.clone().oneshot(req).await.expect("test");
assert_eq!(resp.status(), StatusCode::CREATED);
}
}