use serde::Deserialize;
use wasm_bindgen::prelude::*;
use crate::serde_bridge::*;
const MAX_WASM_FORUM_EMERGENCY_ACTIONS: usize = 4_096;
const MAX_WASM_FORUM_CHALLENGES: usize = 4_096;
const MAX_WASM_FORUM_CONSTITUTION_BYTES: usize = 1_048_576;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct WasmDecisionTransitionAdjudicatedRequest {
decision: decision_forum::decision_object::DecisionObject,
to_state: exo_core::bcts::BctsState,
actor_did: String,
timestamp_ms: u64,
timestamp_logical: u32,
action: exo_gatekeeper::kernel::ActionRequest,
context: exo_gatekeeper::kernel::AdjudicationContext,
}
fn parse_decision_transition_adjudicated_request(
request_json: &str,
) -> Result<WasmDecisionTransitionAdjudicatedRequest, JsValue> {
let request_value: serde_json::Value = from_json_str(request_json)?;
if request_value.get("invariant_set").is_some() {
return Err(JsValue::from_str(
"caller-supplied invariant_set is rejected; WASM decision transitions enforce canonical constitutional invariants",
));
}
serde_json::from_value(request_value).map_err(|_| JsValue::from_str("JSON parse error"))
}
#[wasm_bindgen]
pub fn wasm_create_decision(
decision_id: &str,
title: &str,
class_json: &str,
constitution_hash_hex: &str,
created_at_ms: u64,
created_at_logical: u32,
) -> Result<JsValue, JsValue> {
let id = parse_uuid(decision_id)?;
let class: decision_forum::decision_object::DecisionClass = from_json_str(class_json)?;
let hash = parse_hash(constitution_hash_hex, "hash")?;
let created_at = exo_core::types::Timestamp::new(created_at_ms, created_at_logical);
let decision = decision_forum::decision_object::DecisionObject::new(
decision_forum::decision_object::DecisionObjectInput {
id,
title: title.into(),
class,
constitutional_hash: hash,
created_at,
},
)
.map_err(|e| JsValue::from_str(&format!("Decision error: {e}")))?;
to_js_value(&decision)
}
#[wasm_bindgen]
pub fn wasm_transition_decision(
decision_json: &str,
to_state_json: &str,
actor_did: &str,
timestamp_ms: u64,
timestamp_logical: u32,
) -> Result<JsValue, JsValue> {
let _ = (
decision_json,
to_state_json,
actor_did,
timestamp_ms,
timestamp_logical,
);
Err(JsValue::from_str(
"unadjudicated decision transitions are disabled; use wasm_transition_decision_adjudicated",
))
}
#[wasm_bindgen]
pub fn wasm_transition_decision_adjudicated(
request_json: &str,
constitution: &[u8],
) -> Result<JsValue, JsValue> {
ensure_constitution_bytes(constitution.len())?;
let WasmDecisionTransitionAdjudicatedRequest {
mut decision,
to_state,
actor_did,
timestamp_ms,
timestamp_logical,
action,
context,
} = parse_decision_transition_adjudicated_request(request_json)?;
let actor = exo_core::Did::new(&actor_did)
.map_err(|e| JsValue::from_str(&format!("DID error: {e}")))?;
let ts = exo_core::types::Timestamp::new(timestamp_ms, timestamp_logical);
let kernel = exo_gatekeeper::kernel::Kernel::new(
constitution,
exo_gatekeeper::invariants::InvariantSet::all(),
);
decision
.transition_adjudicated_at(to_state, &actor, ts, &kernel, &action, &context)
.map_err(|e| JsValue::from_str(&format!("Transition error: {e}")))?;
to_js_value(&decision)
}
#[wasm_bindgen]
pub fn wasm_add_vote(decision_json: &str, vote_json: &str) -> Result<JsValue, JsValue> {
let mut decision: decision_forum::decision_object::DecisionObject =
from_json_str(decision_json)?;
let vote: decision_forum::decision_object::Vote = from_json_str(vote_json)?;
decision
.add_vote(vote)
.map_err(|e| JsValue::from_str(&format!("Vote error: {e}")))?;
to_js_value(&decision)
}
#[wasm_bindgen]
pub fn wasm_add_evidence(decision_json: &str, evidence_json: &str) -> Result<JsValue, JsValue> {
let mut decision: decision_forum::decision_object::DecisionObject =
from_json_str(decision_json)?;
let evidence: decision_forum::decision_object::EvidenceItem = from_json_str(evidence_json)?;
decision
.add_evidence(evidence)
.map_err(|e| JsValue::from_str(&format!("Evidence error: {e}")))?;
to_js_value(&decision)
}
#[wasm_bindgen]
pub fn wasm_decision_is_terminal(decision_json: &str) -> Result<bool, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
Ok(decision.is_terminal())
}
#[wasm_bindgen]
pub fn wasm_decision_content_hash(decision_json: &str) -> Result<String, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let hash = decision
.content_hash()
.map_err(|e| JsValue::from_str(&format!("Hash error: {e}")))?;
Ok(hex::encode(hash.as_bytes()))
}
#[wasm_bindgen]
pub fn wasm_file_challenge(
challenge_id: &str,
challenger_did: &str,
decision_id: &str,
ground_json: &str,
evidence_hash_hex: &str,
created_at_ms: u64,
created_at_logical: u32,
) -> Result<JsValue, JsValue> {
let challenge_id = parse_uuid(challenge_id)?;
let challenger = exo_core::Did::new(challenger_did)
.map_err(|e| JsValue::from_str(&format!("DID error: {e}")))?;
let decision_id = parse_uuid(decision_id)?;
let ground: exo_governance::challenge::ChallengeGround = from_json_str(ground_json)?;
let evidence_hash = parse_hash(evidence_hash_hex, "evidence hash")?;
let created_at = exo_core::types::Timestamp::new(created_at_ms, created_at_logical);
let challenge = decision_forum::contestation::file_challenge(
decision_forum::contestation::ChallengeInput {
id: challenge_id,
decision_id,
challenger,
ground,
evidence_hash,
created_at,
},
)
.map_err(|e| JsValue::from_str(&format!("Challenge error: {e}")))?;
to_js_value(&challenge)
}
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn wasm_propose_accountability(
action_id: &str,
target_did: &str,
proposer_did: &str,
action_type_json: &str,
reason: &str,
evidence_hash_hex: &str,
proposed_at_ms: u64,
proposed_at_logical: u32,
) -> Result<JsValue, JsValue> {
let action_id = parse_uuid(action_id)?;
let target = exo_core::Did::new(target_did)
.map_err(|e| JsValue::from_str(&format!("DID error: {e}")))?;
let proposer = exo_core::Did::new(proposer_did)
.map_err(|e| JsValue::from_str(&format!("DID error: {e}")))?;
let action_type: decision_forum::accountability::AccountabilityActionType =
from_json_str(action_type_json)?;
let evidence_hash = parse_hash(evidence_hash_hex, "evidence hash")?;
let proposed_at = exo_core::types::Timestamp::new(proposed_at_ms, proposed_at_logical);
let action = decision_forum::accountability::propose(
decision_forum::accountability::AccountabilityInput {
id: action_id,
action_type,
target,
proposer,
reason: reason.into(),
evidence_hash,
proposed_at,
},
)
.map_err(|e| JsValue::from_str(&format!("Accountability error: {e}")))?;
to_js_value(&action)
}
#[wasm_bindgen]
pub fn wasm_workflow_stages() -> Result<JsValue, JsValue> {
let stages = vec![
"Draft",
"Submitted",
"IdentityResolved",
"ConsentValidated",
"Deliberated",
"Verified",
"Governed",
"Approved",
"Executed",
"Recorded",
"Closed",
"Denied",
"Escalated",
"Remediated",
];
to_js_value(&stages)
}
#[wasm_bindgen]
pub fn wasm_ratify_constitution(
corpus_json: &str,
signatures_json: &str,
quorum_json: &str,
public_keys_json: &str,
timestamp_ms: u64,
) -> Result<JsValue, JsValue> {
let _ = (
corpus_json,
signatures_json,
quorum_json,
public_keys_json,
timestamp_ms,
);
Err(JsValue::from_str(
"constitutional ratification requires a trusted core runtime adapter; public WASM callers cannot supply signer keys or eligible signer sets",
))
}
#[wasm_bindgen]
pub fn wasm_amend_constitution(
corpus_json: &str,
amendment_json: &str,
signatures_json: &str,
quorum_json: &str,
public_keys_json: &str,
timestamp_ms: u64,
) -> Result<JsValue, JsValue> {
let _ = (
corpus_json,
amendment_json,
signatures_json,
quorum_json,
public_keys_json,
timestamp_ms,
);
Err(JsValue::from_str(
"constitutional amendment requires a trusted core runtime adapter; public WASM callers cannot supply signer keys or eligible signer sets",
))
}
#[wasm_bindgen]
pub fn wasm_dry_run_amendment(corpus_json: &str, proposed_json: &str) -> Result<JsValue, JsValue> {
let corpus: decision_forum::constitution::ConstitutionCorpus = from_json_str(corpus_json)?;
let proposed: decision_forum::constitution::Article = from_json_str(proposed_json)?;
let conflicts = decision_forum::constitution::dry_run_amendment(&corpus, &proposed)
.map_err(|e| JsValue::from_str(&format!("Dry-run error: {e}")))?;
to_js_value(&conflicts)
}
#[derive(serde::Deserialize)]
struct TncFlags {
#[serde(default)]
constitutional_hash_valid: bool,
#[serde(default)]
consent_verified: bool,
#[serde(default)]
identity_verified: bool,
#[serde(default)]
evidence_complete: bool,
#[serde(default)]
quorum_met: bool,
#[serde(default)]
human_gate_satisfied: bool,
#[serde(default)]
authority_chain_verified: bool,
#[serde(default)]
ai_ceilings_externally_verified: bool,
}
impl TncFlags {
fn has_any_asserted_claim(&self) -> bool {
self.constitutional_hash_valid
|| self.consent_verified
|| self.identity_verified
|| self.evidence_complete
|| self.quorum_met
|| self.human_gate_satisfied
|| self.authority_chain_verified
|| self.ai_ceilings_externally_verified
}
}
fn parse_untrusted_tnc_flags(flags_json: &str) -> Result<TncFlags, JsValue> {
let flags: TncFlags = from_json_str(flags_json)?;
let _caller_asserted_proofs = flags.has_any_asserted_claim();
Ok(flags)
}
fn build_tnc_ctx<'a>(
decision: &'a decision_forum::decision_object::DecisionObject,
_flags: &TncFlags,
) -> decision_forum::tnc_enforcer::TncContext<'a> {
decision_forum::tnc_enforcer::TncContext {
decision,
constitutional_hash_valid: false,
consent_verified: false,
identity_verified: false,
evidence_complete: false,
quorum_met: false,
human_gate_satisfied: false,
authority_chain_verified: false,
ai_ceilings_externally_verified: false,
}
}
fn tnc_result(r: decision_forum::error::Result<()>) -> Result<JsValue, JsValue> {
match r {
Ok(()) => to_js_value(&serde_json::json!({"ok": true})),
Err(e) => to_js_value(&serde_json::json!({"ok": false, "error": e.to_string()})),
}
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_01(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_01(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_02(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_02(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_03(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_03(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_04(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_04(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_05(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_05(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_06(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_06(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_07(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_07(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_08(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_08(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_09(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_09(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_tnc_10(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
tnc_result(decision_forum::tnc_enforcer::enforce_tnc_10(
&build_tnc_ctx(&decision, &flags),
))
}
#[wasm_bindgen]
pub fn wasm_enforce_all_tnc(decision_json: &str, flags_json: &str) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
match decision_forum::tnc_enforcer::enforce_all(&build_tnc_ctx(&decision, &flags)) {
Ok(()) => to_js_value(&serde_json::json!({"ok": true, "violations": []})),
Err(e) => to_js_value(&serde_json::json!({"ok": false, "error": e.to_string()})),
}
}
#[wasm_bindgen]
pub fn wasm_collect_tnc_violations(
decision_json: &str,
flags_json: &str,
) -> Result<JsValue, JsValue> {
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let flags = parse_untrusted_tnc_flags(flags_json)?;
let violations =
decision_forum::tnc_enforcer::collect_violations(&build_tnc_ctx(&decision, &flags));
let descriptions: Vec<String> = violations.iter().map(|e| e.to_string()).collect();
to_js_value(&serde_json::json!({"violations": descriptions}))
}
#[wasm_bindgen]
pub fn wasm_enforce_human_gate(policy_json: &str, decision_json: &str) -> Result<JsValue, JsValue> {
let policy: decision_forum::human_gate::HumanGatePolicy = from_json_str(policy_json)?;
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
match decision_forum::human_gate::enforce_human_gate(&policy, &decision) {
Ok(()) => to_js_value(&serde_json::json!({"ok": true})),
Err(e) => to_js_value(&serde_json::json!({"ok": false, "error": e.to_string()})),
}
}
#[wasm_bindgen]
pub fn wasm_requires_human_approval(policy_json: &str, class_json: &str) -> Result<bool, JsValue> {
let policy: decision_forum::human_gate::HumanGatePolicy = from_json_str(policy_json)?;
let class: decision_forum::decision_object::DecisionClass = from_json_str(class_json)?;
Ok(decision_forum::human_gate::requires_human_approval(
&policy, class,
))
}
#[wasm_bindgen]
pub fn wasm_ai_within_ceiling(policy_json: &str, class_json: &str) -> Result<bool, JsValue> {
let policy: decision_forum::human_gate::HumanGatePolicy = from_json_str(policy_json)?;
let class: decision_forum::decision_object::DecisionClass = from_json_str(class_json)?;
Ok(decision_forum::human_gate::ai_within_ceiling(
&policy, class,
))
}
#[wasm_bindgen]
pub fn wasm_is_human_vote(vote_json: &str) -> Result<bool, JsValue> {
let vote: decision_forum::decision_object::Vote = from_json_str(vote_json)?;
Ok(decision_forum::human_gate::is_human_vote(&vote))
}
#[wasm_bindgen]
pub fn wasm_is_ai_vote(vote_json: &str) -> Result<bool, JsValue> {
let vote: decision_forum::decision_object::Vote = from_json_str(vote_json)?;
Ok(decision_forum::human_gate::is_ai_vote(&vote))
}
#[wasm_bindgen]
pub fn wasm_check_quorum(registry_json: &str, decision_json: &str) -> Result<JsValue, JsValue> {
use decision_forum::quorum::QuorumCheckResult;
let registry: decision_forum::quorum::QuorumRegistry = from_json_str(registry_json)?;
let decision: decision_forum::decision_object::DecisionObject = from_json_str(decision_json)?;
let result = decision_forum::quorum::check_quorum(®istry, &decision)
.map_err(|e| JsValue::from_str(&format!("Quorum error: {e}")))?;
let json = match result {
QuorumCheckResult::Met {
total_votes,
approve_count,
approve_pct,
} => serde_json::json!({
"status": "Met",
"total_votes": total_votes,
"approve_count": approve_count,
"approve_pct": approve_pct,
}),
QuorumCheckResult::NotMet { reason } => serde_json::json!({
"status": "NotMet",
"reason": reason,
}),
QuorumCheckResult::Degraded {
reason,
available,
required,
} => serde_json::json!({
"status": "Degraded",
"reason": reason,
"available": available,
"required": required,
}),
};
to_js_value(&json)
}
#[wasm_bindgen]
pub fn wasm_verify_quorum_precondition(
registry_json: &str,
class_json: &str,
eligible_voters: usize,
eligible_human_voters: usize,
) -> Result<bool, JsValue> {
let registry: decision_forum::quorum::QuorumRegistry = from_json_str(registry_json)?;
let class: decision_forum::decision_object::DecisionClass = from_json_str(class_json)?;
decision_forum::quorum::verify_quorum_precondition(
®istry,
class,
eligible_voters,
eligible_human_voters,
)
.map_err(|e| JsValue::from_str(&format!("Precondition error: {e}")))
}
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn wasm_create_emergency_action(
action_id: &str,
action_type_json: &str,
actor_did: &str,
justification: &str,
monetary_cap_cents: u64,
evidence_hash_hex: &str,
policy_json: &str,
timestamp_ms: u64,
timestamp_logical: u32,
prior_actions_json: &str,
) -> Result<JsValue, JsValue> {
let action_id = parse_uuid(action_id)?;
let action_type: decision_forum::emergency::EmergencyActionType =
from_json_str(action_type_json)?;
let actor =
exo_core::Did::new(actor_did).map_err(|e| JsValue::from_str(&format!("DID error: {e}")))?;
let evidence_hash = parse_hash(evidence_hash_hex, "evidence hash")?;
let policy: decision_forum::emergency::EmergencyPolicy = from_json_str(policy_json)?;
let ts = exo_core::types::Timestamp::new(timestamp_ms, timestamp_logical);
let prior_actions: Vec<decision_forum::emergency::EmergencyAction> = from_json_bounded_vec(
prior_actions_json,
"forum emergency actions",
MAX_WASM_FORUM_EMERGENCY_ACTIONS,
)?;
let action = decision_forum::emergency::create_emergency_action(
decision_forum::emergency::EmergencyActionInput {
id: action_id,
action_type,
actor,
justification: justification.into(),
monetary_cap_cents,
evidence_hash,
created_at: ts,
},
&policy,
&prior_actions,
)
.map_err(|e| JsValue::from_str(&format!("Emergency error: {e}")))?;
to_js_value(&action)
}
#[wasm_bindgen]
pub fn wasm_ratify_emergency(
action_json: &str,
decision_id: &str,
timestamp_ms: u64,
) -> Result<JsValue, JsValue> {
let mut action: decision_forum::emergency::EmergencyAction = from_json_str(action_json)?;
let id: uuid::Uuid = decision_id
.parse()
.map_err(|e| JsValue::from_str(&format!("UUID error: {e}")))?;
let ts = exo_core::types::Timestamp::new(timestamp_ms, 0);
decision_forum::emergency::ratify_emergency(&mut action, id, ts)
.map_err(|e| JsValue::from_str(&format!("Ratify error: {e}")))?;
to_js_value(&action)
}
#[wasm_bindgen]
pub fn wasm_check_expiry(action_json: &str, now_ms: u64) -> Result<JsValue, JsValue> {
let mut action: decision_forum::emergency::EmergencyAction = from_json_str(action_json)?;
let now = exo_core::types::Timestamp::new(now_ms, 0);
let expired = decision_forum::emergency::check_expiry(&mut action, &now);
to_js_value(&serde_json::json!({"expired": expired, "action": action}))
}
#[wasm_bindgen]
pub fn wasm_needs_governance_review(
actions_json: &str,
policy_json: &str,
) -> Result<bool, JsValue> {
let actions: Vec<decision_forum::emergency::EmergencyAction> = from_json_bounded_vec(
actions_json,
"forum emergency actions",
MAX_WASM_FORUM_EMERGENCY_ACTIONS,
)?;
let policy: decision_forum::emergency::EmergencyPolicy = from_json_str(policy_json)?;
Ok(decision_forum::emergency::needs_governance_review(
&actions, &policy,
))
}
#[wasm_bindgen]
pub fn wasm_begin_review(challenge_json: &str) -> Result<JsValue, JsValue> {
let mut challenge: decision_forum::contestation::ChallengeObject =
from_json_str(challenge_json)?;
decision_forum::contestation::begin_review(&mut challenge)
.map_err(|e| JsValue::from_str(&format!("Review error: {e}")))?;
to_js_value(&challenge)
}
#[wasm_bindgen]
pub fn wasm_withdraw_challenge(challenge_json: &str) -> Result<JsValue, JsValue> {
let mut challenge: decision_forum::contestation::ChallengeObject =
from_json_str(challenge_json)?;
decision_forum::contestation::withdraw(&mut challenge)
.map_err(|e| JsValue::from_str(&format!("Withdraw error: {e}")))?;
to_js_value(&challenge)
}
#[wasm_bindgen]
pub fn wasm_is_contested(challenges_json: &str, decision_id: &str) -> Result<bool, JsValue> {
let challenges: Vec<decision_forum::contestation::ChallengeObject> = from_json_bounded_vec(
challenges_json,
"forum challenges",
MAX_WASM_FORUM_CHALLENGES,
)?;
let id: uuid::Uuid = decision_id
.parse()
.map_err(|e| JsValue::from_str(&format!("UUID error: {e}")))?;
Ok(decision_forum::contestation::is_contested(&challenges, id))
}
#[wasm_bindgen]
pub fn wasm_begin_due_process(action_json: &str) -> Result<JsValue, JsValue> {
let mut action: decision_forum::accountability::AccountabilityAction =
from_json_str(action_json)?;
decision_forum::accountability::begin_due_process(&mut action)
.map_err(|e| JsValue::from_str(&format!("Due-process error: {e}")))?;
to_js_value(&action)
}
#[wasm_bindgen]
pub fn wasm_enact_accountability(
action_json: &str,
decision_id: &str,
timestamp_ms: u64,
) -> Result<JsValue, JsValue> {
let mut action: decision_forum::accountability::AccountabilityAction =
from_json_str(action_json)?;
let id: uuid::Uuid = decision_id
.parse()
.map_err(|e| JsValue::from_str(&format!("UUID error: {e}")))?;
let ts = exo_core::types::Timestamp::new(timestamp_ms, 0);
decision_forum::accountability::enact(&mut action, id, ts)
.map_err(|e| JsValue::from_str(&format!("Enact error: {e}")))?;
to_js_value(&action)
}
#[wasm_bindgen]
pub fn wasm_reverse_accountability(action_json: &str) -> Result<JsValue, JsValue> {
let mut action: decision_forum::accountability::AccountabilityAction =
from_json_str(action_json)?;
decision_forum::accountability::reverse(&mut action)
.map_err(|e| JsValue::from_str(&format!("Reverse error: {e}")))?;
to_js_value(&action)
}
#[wasm_bindgen]
pub fn wasm_is_due_process_expired(action_json: &str, now_ms: u64) -> Result<bool, JsValue> {
let action: decision_forum::accountability::AccountabilityAction = from_json_str(action_json)?;
let now = exo_core::types::Timestamp::new(now_ms, 0);
Ok(decision_forum::accountability::is_due_process_expired(
&action, &now,
))
}
#[wasm_bindgen]
pub fn wasm_verify_forum_authority(authority_json: &str) -> Result<JsValue, JsValue> {
let authority: decision_forum::authority::ForumAuthority = from_json_str(authority_json)?;
match decision_forum::authority::verify_forum_authority(&authority) {
Ok(()) => to_js_value(&serde_json::json!({"ok": true})),
Err(e) => to_js_value(&serde_json::json!({"ok": false, "error": e.to_string()})),
}
}
#[wasm_bindgen]
pub fn wasm_verify_forum_authority_with_key(
authority_json: &str,
root_public_key_hex: &str,
) -> Result<JsValue, JsValue> {
let authority: decision_forum::authority::ForumAuthority = from_json_str(authority_json)?;
let root_public_key = parse_public_key_hex(root_public_key_hex)?;
match decision_forum::authority::verify_forum_authority_with_key(&authority, &root_public_key) {
Ok(()) => to_js_value(&serde_json::json!({"ok": true})),
Err(e) => to_js_value(&serde_json::json!({"ok": false, "error": e.to_string()})),
}
}
fn parse_uuid(value: &str) -> Result<uuid::Uuid, JsValue> {
value
.parse()
.map_err(|e| JsValue::from_str(&format!("UUID error: {e}")))
}
fn parse_hash(value: &str, label: &str) -> Result<exo_core::Hash256, JsValue> {
let bytes = hex::decode(value).map_err(|e| JsValue::from_str(&format!("hex: {e}")))?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| JsValue::from_str(&format!("{label} must be 32 bytes")))?;
Ok(exo_core::Hash256::from_bytes(arr))
}
fn parse_public_key_hex(public_key_hex: &str) -> Result<exo_core::PublicKey, JsValue> {
let bytes = hex::decode(public_key_hex).map_err(|e| JsValue::from_str(&format!("hex: {e}")))?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| JsValue::from_str("public key must be 32 bytes"))?;
Ok(exo_core::PublicKey::from_bytes(arr))
}
fn ensure_constitution_bytes(len: usize) -> Result<(), JsValue> {
if len > MAX_WASM_FORUM_CONSTITUTION_BYTES {
return Err(JsValue::from_str(
"constitution exceeds maximum WASM decision-forum size",
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use decision_forum::decision_object::{
ActorKind, AuthorityLink, DecisionClass, DecisionObject, DecisionObjectInput,
};
use exo_core::{Hash256, Timestamp, types::Did};
#[test]
fn decision_forum_bridge_does_not_synthesize_clock_metadata() {
let source = include_str!("decision_forum_bindings.rs");
let production = source
.split("#[cfg(test)]")
.next()
.expect("production section");
assert!(
!production.contains("HybridClock::new()"),
"decision-forum WASM exports must require caller-supplied HLC metadata"
);
assert!(
!production.contains("Uuid::new_v4"),
"decision-forum WASM exports must require caller-supplied UUID metadata"
);
}
#[test]
fn wasm_tnc_adapter_does_not_trust_caller_supplied_flags() {
let mut decision = DecisionObject::new(DecisionObjectInput {
id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000901").expect("valid UUID"),
title: "Caller-supplied TNC flags".into(),
class: DecisionClass::Routine,
constitutional_hash: Hash256::from_bytes([1_u8; 32]),
created_at: Timestamp::new(1_700_000_000_000, 0),
})
.expect("valid decision");
decision
.add_authority_link(AuthorityLink {
actor_did: Did::new("did:exo:authority-root").expect("valid DID"),
actor_kind: ActorKind::Human,
delegation_hash: Hash256::from_bytes([2_u8; 32]),
timestamp: Timestamp::new(1_700_000_000_001, 0),
})
.expect("valid authority link");
let caller_flags = super::TncFlags {
constitutional_hash_valid: true,
consent_verified: true,
identity_verified: true,
evidence_complete: true,
quorum_met: true,
human_gate_satisfied: true,
authority_chain_verified: true,
ai_ceilings_externally_verified: true,
};
let ctx = super::build_tnc_ctx(&decision, &caller_flags);
let err = decision_forum::tnc_enforcer::enforce_all(&ctx)
.expect_err("caller-provided WASM flags cannot satisfy TNC proof obligations");
assert!(
err.to_string().contains("authority chain not verified"),
"unexpected TNC rejection: {err}"
);
}
#[test]
fn forum_authority_wasm_verifier_requires_trusted_public_key() {
let source = include_str!("decision_forum_bindings.rs");
let production = source
.split("#[cfg(test)]")
.next()
.expect("production section");
assert!(
production.contains("wasm_verify_forum_authority_with_key"),
"WASM authority verification must expose a trusted-public-key verification path"
);
assert!(
production.contains("verify_forum_authority_with_key"),
"WASM authority verification must call the cryptographic core verifier"
);
}
#[test]
fn wasm_constitution_exports_reject_caller_supplied_signer_keys() {
let source = include_str!("decision_forum_bindings.rs");
let production = source
.split("#[cfg(test)]")
.next()
.expect("production section");
for export_name in ["wasm_ratify_constitution", "wasm_amend_constitution"] {
let body = production
.split(&format!("pub fn {export_name}"))
.nth(1)
.unwrap_or_else(|| panic!("{export_name} export must exist"))
.split("///")
.next()
.expect("export body must be bounded by next doc comment");
assert!(
body.contains("trusted core runtime adapter"),
"{export_name} must fail closed at the public WASM boundary"
);
assert!(
!body.contains("parse_public_key_pairs(public_keys_json)"),
"{export_name} must not parse caller-supplied signer keys"
);
assert!(
!body.contains("public_keys.keys().cloned().collect()"),
"{export_name} must not derive eligible signers from caller-supplied keys"
);
}
}
#[test]
fn wasm_emergency_create_requires_bounded_prior_action_history() {
let source = include_str!("decision_forum_bindings.rs");
let production = source
.split("#[cfg(test)]")
.next()
.expect("production section");
let body = production
.split("pub fn wasm_create_emergency_action")
.nth(1)
.expect("emergency create export exists")
.split("/// Ratify an emergency action")
.next()
.expect("emergency create export body is bounded by ratify docs");
assert!(
body.contains("prior_actions_json: &str"),
"WASM emergency creation must require caller-supplied prior action history"
);
assert!(
body.contains("from_json_bounded_vec(")
&& body.contains("prior_actions_json")
&& body.contains("MAX_WASM_FORUM_EMERGENCY_ACTIONS"),
"WASM emergency creation must parse bounded prior action history"
);
assert!(
body.contains("&prior_actions"),
"WASM emergency creation must pass prior action history into the core constructor"
);
}
}