Skip to main content

exochain_wasm/
lib.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! ExoChain WASM Bindings
18//!
19//! Exposes the full ExoChain governance engine to JavaScript/Node.js via WebAssembly.
20//! Covers 14 crates: core, identity, consent, authority, gatekeeper, governance,
21//! escalation, legal, dag, proofs, api, tenant, decision-forum, and messaging.
22
23#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
24
25pub mod authority_bindings;
26pub mod avc_bindings;
27pub mod catapult_bindings;
28pub mod consent_bindings;
29pub mod core_bindings;
30pub mod decision_forum_bindings;
31pub mod economy_bindings;
32pub mod escalation_bindings;
33pub mod gatekeeper_bindings;
34pub mod governance_bindings;
35pub mod identity_bindings;
36pub mod legal_bindings;
37pub mod messaging_bindings;
38mod serde_bridge;
39
40// Flat re-exports so integration tests and downstream rlib consumers can
41// access all WASM bindings as `exochain_wasm::wasm_*` without module prefixes.
42pub use authority_bindings::*;
43pub use avc_bindings::*;
44pub use catapult_bindings::*;
45pub use consent_bindings::*;
46pub use core_bindings::*;
47pub use decision_forum_bindings::*;
48pub use economy_bindings::*;
49pub use escalation_bindings::*;
50pub use gatekeeper_bindings::*;
51pub use governance_bindings::*;
52pub use identity_bindings::*;
53pub use legal_bindings::*;
54pub use messaging_bindings::*;
55
56#[cfg(test)]
57mod source_guard_tests {
58    #[test]
59    fn wasm_bridge_uses_deterministic_collections() {
60        let binding_sources = [
61            (
62                "authority_bindings.rs",
63                include_str!("authority_bindings.rs"),
64            ),
65            (
66                "governance_bindings.rs",
67                include_str!("governance_bindings.rs"),
68            ),
69            ("economy_bindings.rs", include_str!("economy_bindings.rs")),
70            ("avc_bindings.rs", include_str!("avc_bindings.rs")),
71        ];
72
73        for (path, source) in binding_sources {
74            assert!(
75                !source.contains("HashMap"),
76                "{path} must use deterministic BTreeMap-style collections at the WASM boundary"
77            );
78            assert!(
79                !source.contains("HashSet"),
80                "{path} must use deterministic BTreeSet-style collections at the WASM boundary"
81            );
82        }
83    }
84
85    #[test]
86    fn wasm_consent_bridge_requires_caller_supplied_time() {
87        let source = include_str!("consent_bindings.rs");
88        let forbidden = [
89            format!("{}{}", "Timestamp::", "now_utc()"),
90            format!("{}{}", "Uuid::", "new_v4()"),
91            "HybridClock::new()".to_string(),
92        ];
93
94        for pattern in forbidden {
95            assert!(
96                !source.contains(&pattern),
97                "consent WASM bindings must receive caller-supplied IDs and HLC timestamps"
98            );
99        }
100    }
101
102    #[test]
103    fn wasm_consent_termination_refuses_unsigned_actor_did_bridge() {
104        let source = include_str!("consent_bindings.rs");
105        let termination = source
106            .split("pub fn wasm_terminate_bailment(")
107            .nth(1)
108            .and_then(|section| {
109                section
110                    .split("pub fn wasm_terminate_bailment_signed(")
111                    .next()
112            })
113            .expect("wasm_terminate_bailment source");
114        let signed_termination = source
115            .split("pub fn wasm_terminate_bailment_signed(")
116            .nth(1)
117            .expect("wasm_terminate_bailment_signed source");
118
119        assert!(
120            termination.contains("unsigned bailment termination is disabled"),
121            "legacy WASM bailment termination must fail closed instead of trusting actor_did"
122        );
123        assert!(
124            !termination.contains("exo_consent::bailment::terminate(&mut bailment, &actor)"),
125            "legacy WASM bailment termination must not reach the core state transition"
126        );
127        assert!(
128            source.contains("pub fn wasm_bailment_termination_payload("),
129            "WASM consent bridge must expose the canonical termination payload for external signing"
130        );
131        assert!(
132            source.contains("pub fn wasm_terminate_bailment_signed("),
133            "WASM consent bridge must keep the signed termination entrypoint fail-closed"
134        );
135        assert!(
136            signed_termination.contains("untrusted_wasm_bailment_termination_error"),
137            "WASM signed termination must fail closed instead of trusting caller-supplied key material"
138        );
139        assert!(
140            !signed_termination.contains("terminate_verified"),
141            "WASM signed termination must not call core termination without trusted DID resolution"
142        );
143    }
144
145    #[test]
146    fn wasm_consent_acceptance_refuses_caller_supplied_bailee_keys() {
147        let source = include_str!("consent_bindings.rs");
148        let acceptance = source
149            .split("pub fn wasm_accept_bailment(")
150            .nth(1)
151            .and_then(|section| {
152                section
153                    .split("pub fn wasm_bailment_signing_payload(")
154                    .next()
155            })
156            .expect("wasm_accept_bailment source");
157
158        assert!(
159            acceptance.contains("untrusted_wasm_bailment_acceptance_error"),
160            "WASM bailment acceptance must fail closed without trusted DID resolution"
161        );
162        assert!(
163            !acceptance.contains("exo_consent::bailment::accept("),
164            "WASM bailment acceptance must not call core accept without trusted DID resolution"
165        );
166    }
167
168    #[test]
169    fn wasm_governance_bridge_requires_caller_supplied_metadata() {
170        let source = include_str!("governance_bindings.rs");
171        let forbidden = [
172            format!("{}{}", "Timestamp::", "now_utc()"),
173            format!("{}{}", "Uuid::", "new_v4()"),
174            "HybridClock::new()".to_string(),
175        ];
176
177        for pattern in forbidden {
178            assert!(
179                !source.contains(&pattern),
180                "governance WASM bindings must receive caller-supplied IDs and HLC timestamps"
181            );
182        }
183    }
184
185    #[test]
186    fn wasm_governance_close_requires_trusted_runtime_adapter() {
187        let source = include_str!("governance_bindings.rs");
188        let production = source
189            .split("#[cfg(test)]")
190            .next()
191            .expect("production section");
192        assert!(
193            production
194                .contains("verified deliberation closure requires a trusted core runtime adapter"),
195            "public WASM deliberation close must fail closed without a trusted runtime adapter"
196        );
197        assert!(
198            !production.contains("deliberation::close(&mut delib"),
199            "WASM deliberation close must not call the structural-only close path"
200        );
201        assert!(
202            !production.contains("close_verified(&mut delib, &policy, &resolver)"),
203            "public WASM deliberation close must not trust caller-supplied signer keys and roles"
204        );
205    }
206
207    #[test]
208    fn wasm_governance_clearance_requires_caller_supplied_registry() {
209        let source = include_str!("governance_bindings.rs");
210        assert!(
211            source.contains("registry_json"),
212            "WASM clearance checks must accept caller-supplied clearance registry data"
213        );
214        assert!(
215            !source.contains("ClearanceLevel::Governor"),
216            "WASM clearance checks must not fabricate Governor clearance"
217        );
218    }
219
220    #[test]
221    fn wasm_governance_bridge_bounds_untrusted_collection_inputs() {
222        let source = include_str!("governance_bindings.rs");
223        for required in [
224            "MAX_WASM_CLEARANCE_REGISTRY_ENTRIES",
225            "MAX_WASM_CONFLICT_DECLARATIONS",
226            "MAX_WASM_AUDIT_ENTRIES",
227            "MAX_WASM_DELIBERATION_PARTICIPANTS",
228            "MAX_WASM_INDEPENDENCE_ACTORS",
229            "MAX_WASM_REGISTRY_RELATIONSHIPS",
230            "MAX_WASM_COORDINATION_ACTIONS",
231            "MAX_WASM_PROPOSAL_BYTES",
232            "MAX_WASM_CHALLENGE_EVIDENCE_BYTES",
233            "parse_bounded_vec",
234        ] {
235            assert!(
236                source.contains(required),
237                "governance WASM boundary must define and use {required}"
238            );
239        }
240
241        for forbidden in [
242            "let key_pairs: Vec<(String, String)> = from_json_str(public_keys_json)?;",
243            "let entries: Vec<WasmClearanceRegistryEntry> = from_json_str(registry_json)?;",
244            "let approvals: Vec<exo_governance::quorum::Approval> = from_json_str(approvals_json)?;",
245            "let entries: Vec<exo_governance::audit::AuditEntry> = from_json_str(entries_json)?;",
246            "let did_strs: Vec<String> = from_json_str(participants_json)?;",
247            "let did_strs: Vec<String> = from_json_str(actors_json)?;",
248            "let actions: Vec<exo_governance::crosscheck::TimestampedAction> = from_json_str(actions_json)?;",
249        ] {
250            assert!(
251                !source.contains(forbidden),
252                "governance WASM boundary must not deserialize untrusted arrays without a count bound: {forbidden}"
253            );
254        }
255    }
256
257    #[test]
258    fn wasm_non_governance_vec_inputs_use_explicit_count_bounds() {
259        let required = [
260            (
261                "authority_bindings.rs",
262                include_str!("authority_bindings.rs"),
263                "MAX_WASM_AUTHORITY_LINKS",
264            ),
265            (
266                "authority_bindings.rs",
267                include_str!("authority_bindings.rs"),
268                "AUTHORITY_CHAIN_TRUSTED_ADAPTER_REQUIRED",
269            ),
270            (
271                "authority_bindings.rs",
272                include_str!("authority_bindings.rs"),
273                "AUTHORITY_CHAIN_CALLER_KEYS_REJECTED",
274            ),
275            (
276                "identity_bindings.rs",
277                include_str!("identity_bindings.rs"),
278                "MAX_WASM_SHAMIR_SHARES",
279            ),
280            (
281                "escalation_bindings.rs",
282                include_str!("escalation_bindings.rs"),
283                "MAX_WASM_DETECTION_SIGNALS",
284            ),
285            (
286                "escalation_bindings.rs",
287                include_str!("escalation_bindings.rs"),
288                "MAX_WASM_FEEDBACK_ENTRIES",
289            ),
290            (
291                "escalation_bindings.rs",
292                include_str!("escalation_bindings.rs"),
293                "MAX_WASM_ESCALATION_CASES",
294            ),
295            (
296                "messaging_bindings.rs",
297                include_str!("messaging_bindings.rs"),
298                "MAX_WASM_AUTHORIZED_TRUSTEES",
299            ),
300            (
301                "legal_bindings.rs",
302                include_str!("legal_bindings.rs"),
303                "MAX_WASM_LEGAL_AUDIT_ACTIONS",
304            ),
305            (
306                "legal_bindings.rs",
307                include_str!("legal_bindings.rs"),
308                "MAX_WASM_EDISCOVERY_CORPUS_ITEMS",
309            ),
310            (
311                "legal_bindings.rs",
312                include_str!("legal_bindings.rs"),
313                "MAX_WASM_RETENTION_RECORDS",
314            ),
315        ];
316
317        for (path, source, required_name) in required {
318            assert!(
319                source.contains(required_name),
320                "{path} must define and use {required_name}"
321            );
322        }
323
324        let forbidden = [
325            (
326                "authority_bindings.rs",
327                include_str!("authority_bindings.rs"),
328                "let links: Vec<exo_authority::AuthorityLink> = from_json_str(links_json)?;",
329            ),
330            (
331                "authority_bindings.rs",
332                include_str!("authority_bindings.rs"),
333                "let key_pairs: Vec<(String, String)> = from_json_str(keys_json)?;",
334            ),
335            (
336                "identity_bindings.rs",
337                include_str!("identity_bindings.rs"),
338                "let shares: Vec<exo_identity::shamir::Share> = from_json_str(shares_json)?;",
339            ),
340            (
341                "escalation_bindings.rs",
342                include_str!("escalation_bindings.rs"),
343                "let signals: Vec<exo_escalation::detector::DetectionSignal> = from_json_str(signals_json)?;",
344            ),
345            (
346                "escalation_bindings.rs",
347                include_str!("escalation_bindings.rs"),
348                "from_json_str(entries_json)?;",
349            ),
350            (
351                "escalation_bindings.rs",
352                include_str!("escalation_bindings.rs"),
353                "let feedbacks: Vec<exo_escalation::feedback::FeedbackEntry> = from_json_str(feedbacks_json)?;",
354            ),
355            (
356                "escalation_bindings.rs",
357                include_str!("escalation_bindings.rs"),
358                "let mut cases: Vec<exo_escalation::escalation::EscalationCase> = from_json_str(cases_json)?;",
359            ),
360            (
361                "messaging_bindings.rs",
362                include_str!("messaging_bindings.rs"),
363                "let trustees: Vec<WasmAuthorizedTrustee> = from_json_str(authorized_trustees_json)?;",
364            ),
365            (
366                "legal_bindings.rs",
367                include_str!("legal_bindings.rs"),
368                "let actions: Vec<exo_legal::fiduciary::AuditEntry> = from_json_str(actions_json)?;",
369            ),
370            (
371                "legal_bindings.rs",
372                include_str!("legal_bindings.rs"),
373                "let corpus: Vec<exo_legal::evidence::Evidence> = from_json_str(corpus_json)?;",
374            ),
375            (
376                "legal_bindings.rs",
377                include_str!("legal_bindings.rs"),
378                "let mut records: Vec<exo_legal::records::Record> = from_json_str(records_json)?;",
379            ),
380        ];
381
382        for (path, source, forbidden_pattern) in forbidden {
383            assert!(
384                !source.contains(forbidden_pattern),
385                "{path} must not deserialize untrusted JSON arrays without explicit count bounds: {forbidden_pattern}"
386            );
387        }
388    }
389
390    #[test]
391    fn wasm_decision_forum_vec_inputs_use_explicit_count_bounds() {
392        let source = include_str!("decision_forum_bindings.rs");
393
394        for required in [
395            "MAX_WASM_FORUM_EMERGENCY_ACTIONS",
396            "MAX_WASM_FORUM_CHALLENGES",
397        ] {
398            assert!(
399                source.contains(required),
400                "decision forum WASM boundary must define and use {required}"
401            );
402        }
403
404        for forbidden in [
405            "let actions: Vec<decision_forum::emergency::EmergencyAction> = from_json_str(actions_json)?;",
406            "from_json_str(challenges_json)?;",
407            "let sig_pairs: Vec<(String, String)> = from_json_str(signatures_json)?;",
408            "let key_pairs: Vec<(String, String)> = from_json_str(public_keys_json)?;",
409        ] {
410            assert!(
411                !source.contains(forbidden),
412                "decision forum WASM boundary must not deserialize untrusted arrays without a count bound: {forbidden}"
413            );
414        }
415    }
416
417    #[test]
418    fn wasm_decision_forum_human_vote_exports_fail_closed_without_verified_registry() {
419        let source = include_str!("decision_forum_bindings.rs");
420
421        assert!(
422            source.contains("decision_forum::human_gate::enforce_human_gate(&policy, &decision)"),
423            "legacy WASM human-gate export must use the fail-closed core helper when no trusted human registry is available"
424        );
425        assert!(
426            source.contains("decision_forum::human_gate::is_human_vote(&vote)"),
427            "legacy WASM human-vote export must use the fail-closed core helper, not declared actor metadata"
428        );
429        assert!(
430            source.contains("decision_forum::quorum::check_quorum(&registry, &decision)"),
431            "legacy WASM quorum export must use the fail-closed core helper when no trusted human registry is available"
432        );
433
434        for forbidden in [
435            "is_declared_human_vote",
436            "check_quorum_with_verified_humans",
437            "verified_human_voters",
438        ] {
439            assert!(
440                !source.contains(forbidden),
441                "legacy WASM decision-forum exports must not accept or synthesize verified human status via {forbidden}"
442            );
443        }
444    }
445
446    #[test]
447    fn wasm_decision_transition_requires_kernel_adjudication() {
448        let source = include_str!("decision_forum_bindings.rs");
449        let legacy_transition = source
450            .split("pub fn wasm_transition_decision(")
451            .nth(1)
452            .and_then(|section| {
453                section
454                    .split("pub fn wasm_transition_decision_adjudicated(")
455                    .next()
456            })
457            .expect("legacy decision transition export source");
458
459        assert!(
460            legacy_transition.contains("unadjudicated decision transitions are disabled"),
461            "legacy WASM decision transition must fail closed without a kernel verdict"
462        );
463        assert!(
464            !legacy_transition.contains(".transition_at("),
465            "legacy WASM decision transition must not reach the raw BCTS transition"
466        );
467        assert!(
468            source.contains("Kernel::new") && source.contains(".transition_adjudicated_at("),
469            "WASM decision bridge must expose a kernel-adjudicated transition entrypoint"
470        );
471        assert!(
472            source.contains("WasmDecisionTransitionAdjudicatedRequest")
473                && source.contains("request_json"),
474            "WASM adjudicated decision transition must use a typed bounded request JSON instead of a wide argument list"
475        );
476        let adjudicated_transition = source
477            .split("pub fn wasm_transition_decision_adjudicated(")
478            .nth(1)
479            .and_then(|section| section.split("/// Add a vote").next())
480            .expect("adjudicated decision transition export source");
481        assert!(
482            adjudicated_transition.contains("InvariantSet::all()"),
483            "WASM adjudicated decision transition must enforce the canonical complete invariant set"
484        );
485        assert!(
486            !adjudicated_transition.contains("Kernel::new(constitution, invariant_set)"),
487            "WASM adjudicated decision transition must not build a kernel from caller-supplied invariants"
488        );
489        assert!(
490            source
491                .split("struct WasmDecisionTransitionAdjudicatedRequest")
492                .nth(1)
493                .and_then(|section| section.split("/// Create a new DecisionObject").next())
494                .is_some_and(|section| !section.contains("invariant_set:")),
495            "WASM adjudicated decision transition request must not deserialize caller-supplied invariants"
496        );
497        assert!(
498            !source.contains(
499                "timestamp_logical: u32,\n    constitution: &[u8],\n    invariant_set_json: &str,"
500            ),
501            "WASM adjudicated decision transition must not expose a clippy-wide argument list"
502        );
503    }
504
505    #[test]
506    fn wasm_messaging_bridge_requires_caller_supplied_envelope_metadata() {
507        let source = include_str!("messaging_bindings.rs");
508        assert!(
509            source.contains("message_id") && source.contains("created_physical_ms"),
510            "messaging WASM encryption must expose caller-supplied envelope metadata"
511        );
512        assert!(
513            source.contains("ComposeMetadata::new"),
514            "messaging WASM encryption must validate caller-supplied envelope metadata"
515        );
516        assert!(
517            source.contains("DeathVerificationCreationMetadata::new")
518                && source.contains("DeathConfirmationMetadata::new"),
519            "messaging WASM death verification must validate caller-supplied state metadata"
520        );
521        let initial_payload = source
522            .split("pub fn wasm_death_verification_initial_signing_payload")
523            .nth(1)
524            .and_then(|section| {
525                section
526                    .split("/// Create a new death verification request")
527                    .next()
528            })
529            .expect("initial death-verification signing payload section");
530        assert!(
531            initial_payload.contains("created_physical_ms")
532                && initial_payload.contains("&metadata.created_at"),
533            "initial death-verification signatures must bind the submitted creation timestamp"
534        );
535        let confirmation_payload = source
536            .split("pub fn wasm_death_verification_confirmation_signing_payload")
537            .nth(1)
538            .and_then(|section| section.split("/// Add a trustee confirmation").next())
539            .expect("confirmation death-verification signing payload section");
540        assert!(
541            confirmation_payload.contains("confirmed_physical_ms")
542                && confirmation_payload.contains("&metadata.confirmed_at"),
543            "death-verification confirmation signatures must bind the submitted confirmation timestamp"
544        );
545
546        let forbidden = [
547            format!("{}{}", "Timestamp::", "now_utc()"),
548            format!("{}{}", "Uuid::", "new_v4()"),
549            "HybridClock::new()".to_string(),
550        ];
551
552        for pattern in forbidden {
553            assert!(
554                !source.contains(&pattern),
555                "messaging WASM bindings must receive caller-supplied IDs and HLC timestamps"
556            );
557        }
558    }
559
560    #[test]
561    fn wasm_messaging_bridge_does_not_export_x25519_secret_material() {
562        let source = include_str!("messaging_bindings.rs");
563        let forbidden = [
564            ["secret", "_key_hex"].concat(),
565            [".secret", ".to_hex()"].concat(),
566            [".secret", ".0"].concat(),
567        ];
568        for pattern in forbidden {
569            assert!(
570                !source.contains(&pattern),
571                "messaging WASM bindings must not export or tuple-access X25519 secret material via {pattern}"
572            );
573        }
574    }
575
576    #[test]
577    fn wasm_messaging_bridge_does_not_generate_x25519_keypairs() {
578        let source = include_str!("messaging_bindings.rs");
579        let production = source
580            .split("// ===========================================================================")
581            .next()
582            .unwrap_or(source);
583
584        assert!(
585            !production.contains("X25519KeyPair::generate"),
586            "messaging WASM must not generate X25519 key material inside the bridge"
587        );
588        assert!(
589            production.contains("ephemeral_x25519_secret_hex"),
590            "messaging WASM encryption must receive caller-supplied ephemeral X25519 material"
591        );
592    }
593
594    #[test]
595    fn wasm_messaging_bridge_does_not_decode_ed25519_signing_secrets() {
596        let source = include_str!("messaging_bindings.rs");
597        let production = source
598            .split("// ===========================================================================")
599            .next()
600            .unwrap_or(source);
601
602        assert!(
603            production.contains("wasm_prepare_encrypted_message"),
604            "messaging WASM must expose unsigned encrypted envelopes plus signing bytes"
605        );
606        assert!(
607            production.contains("wasm_attach_message_signature"),
608            "messaging WASM must attach caller-produced signatures without importing sender secrets"
609        );
610
611        for pattern in [
612            "parse_ed25519_signing_seed_hex",
613            "sender_signing_key_hex",
614            "SecretKey::from_bytes",
615            "lock_and_send(",
616        ] {
617            assert!(
618                !production.contains(pattern),
619                "messaging WASM bindings must not decode or use Ed25519 signing secrets via {pattern}"
620            );
621        }
622    }
623
624    #[test]
625    fn wasm_messaging_legacy_raw_secret_entrypoints_fail_closed() {
626        let source = include_str!("messaging_bindings.rs");
627        let production = source
628            .split("// ===========================================================================")
629            .next()
630            .unwrap_or(source);
631
632        assert!(
633            production
634                .contains("raw X25519 secret public derivation is disabled at the WASM boundary"),
635            "legacy X25519 raw-secret public derivation must fail closed"
636        );
637        assert!(
638            production.contains("raw Ed25519 sender signing is disabled at the WASM boundary"),
639            "legacy raw-secret message encryption must fail closed"
640        );
641    }
642
643    #[test]
644    fn wasm_identity_risk_bridge_requires_caller_supplied_signer_and_time() {
645        let source = include_str!("identity_bindings.rs");
646        assert!(
647            source.contains("attester_secret_hex")
648                && source.contains("now_physical_ms")
649                && source.contains("now_logical"),
650            "risk assessment must expose caller-supplied signer and HLC metadata"
651        );
652
653        let forbidden = [
654            "HybridClock::new()".to_string(),
655            "generate_keypair()".to_string(),
656            ["Timestamp::", "now_utc()"].concat(),
657        ];
658
659        for pattern in &forbidden {
660            assert!(
661                !source.contains(pattern),
662                "identity WASM risk binding must not fabricate signer or time with {pattern}"
663            );
664        }
665    }
666
667    #[test]
668    fn wasm_shamir_split_exposes_caller_supplied_entropy_boundary() {
669        let source = include_str!("identity_bindings.rs");
670        let production = source
671            .split("// ===========================================================================")
672            .next()
673            .unwrap_or(source);
674
675        assert!(
676            production.contains("wasm_shamir_split_with_entropy"),
677            "WASM Shamir splitting must expose a caller-supplied entropy entrypoint"
678        );
679        assert!(
680            production.contains("exo_identity::shamir::split_with_entropy"),
681            "WASM Shamir splitting must call the entropy-explicit core split API"
682        );
683        assert!(
684            production.contains("entropy"),
685            "WASM Shamir split boundary must keep entropy explicit in the public API"
686        );
687    }
688
689    #[test]
690    fn wasm_identity_secret_metadata_has_no_debug_surface() {
691        let source = include_str!("identity_bindings.rs");
692        let production = source
693            .split("// ===========================================================================")
694            .next()
695            .unwrap_or(source);
696        let metadata_def = production
697            .split("struct RiskAssessmentMetadata")
698            .next()
699            .expect("risk metadata definition must exist");
700
701        assert!(
702            production.contains("attester_secret_hex"),
703            "risk assessment metadata must keep the caller-supplied attester secret explicit"
704        );
705        assert!(
706            !metadata_def.contains("Debug"),
707            "secret-bearing risk metadata must not derive or expose Debug"
708        );
709    }
710
711    #[test]
712    fn wasm_core_event_bridge_requires_caller_supplied_metadata() {
713        let source = include_str!("core_bindings.rs");
714        assert!(
715            source.contains("event_id")
716                && source.contains("timestamp_physical_ms")
717                && source.contains("timestamp_logical"),
718            "signed event creation must expose caller-supplied event ID and HLC metadata"
719        );
720
721        let forbidden = [
722            "CorrelationId::new()".to_string(),
723            "HybridClock::new()".to_string(),
724            ["Timestamp::", "now_utc()"].concat(),
725        ];
726
727        for pattern in &forbidden {
728            assert!(
729                !source.contains(pattern),
730                "core WASM event bindings must not fabricate event metadata with {pattern}"
731            );
732        }
733    }
734
735    #[test]
736    fn wasm_core_bridge_does_not_decode_raw_secret_keys() {
737        let source = include_str!("core_bindings.rs");
738        let production = source
739            .split("// ===========================================================================")
740            .next()
741            .unwrap_or(source);
742
743        assert!(
744            production.contains("wasm_event_signing_payload"),
745            "core WASM events must expose canonical signing bytes for external signers"
746        );
747        assert!(
748            production.contains("wasm_create_event_with_signature"),
749            "core WASM events must accept caller-produced signatures without importing secrets"
750        );
751
752        for pattern in [
753            "parse_ed25519_secret_array_hex",
754            "parse_ed25519_signing_seed_hex",
755            "SecretKey::from_bytes",
756            "KeyPair::from_secret_bytes",
757            "exo_core::events::create_signed_event",
758        ] {
759            assert!(
760                !production.contains(pattern),
761                "core WASM bindings must not decode or use raw secret keys via {pattern}"
762            );
763        }
764    }
765
766    #[test]
767    fn wasm_core_legacy_raw_secret_entrypoints_fail_closed() {
768        let source = include_str!("core_bindings.rs");
769        let production = source
770            .split("// ===========================================================================")
771            .next()
772            .unwrap_or(source);
773
774        assert!(
775            production.contains("raw secret-key signing is disabled at the WASM boundary"),
776            "legacy raw-secret signing entrypoint must fail closed"
777        );
778        assert!(
779            production
780                .contains("raw secret-key public derivation is disabled at the WASM boundary"),
781            "legacy raw-secret public-key derivation entrypoint must fail closed"
782        );
783        assert!(
784            production.contains("raw secret-key event signing is disabled at the WASM boundary"),
785            "legacy raw-secret event creation entrypoint must fail closed"
786        );
787    }
788
789    #[test]
790    fn wasm_avc_bindings_externalize_subject_signing() {
791        let source = include_str!("avc_bindings.rs");
792        let production = source.split("\n#[cfg(test)]").next().unwrap_or(source);
793
794        assert!(
795            production.contains("from_json_str::<AutonomousVolitionCredential>"),
796            "AVC WASM credential inputs must use the bounded JSON bridge"
797        );
798        assert!(
799            production.contains("from_json_str::<AvcActionRequest>"),
800            "AVC WASM action inputs must use the bounded JSON bridge"
801        );
802        assert!(
803            production.contains("from_json_str::<Signature>"),
804            "AVC WASM signature inputs must use the bounded JSON bridge"
805        );
806        for pattern in [
807            "serde_json::from_str(credential_json)",
808            "serde_json::from_str(action_json)",
809            "serde_json::from_str(subject_signature_json)",
810            "hex::decode(value)",
811        ] {
812            assert!(
813                !production.contains(pattern),
814                "AVC WASM production bindings must not bypass input bounds via {pattern}"
815            );
816        }
817
818        assert!(
819            production.contains("wasm_avc_action_signing_payload"),
820            "AVC WASM bindings must expose canonical action bytes for external signers"
821        );
822        assert!(
823            production.contains("wasm_avc_build_emit_request_from_signature"),
824            "AVC WASM bindings must accept externally produced signatures"
825        );
826        assert!(
827            production.contains("raw AVC subject-key signing is disabled at the WASM boundary"),
828            "legacy AVC raw subject-key signing must fail closed"
829        );
830        assert!(
831            production.contains(
832                "raw AVC subject-key emit request building is disabled at the WASM boundary"
833            ),
834            "legacy AVC raw subject-key request building must fail closed"
835        );
836
837        for pattern in [
838            "subject_secret_hex",
839            "raw_subject_key_material",
840            "SecretKey::from_bytes",
841            "KeyPair::from_secret_bytes",
842            "crypto::sign(",
843        ] {
844            assert!(
845                !production.contains(pattern),
846                "AVC WASM production bindings must not decode, derive, or use raw subject keys via {pattern}"
847            );
848        }
849    }
850
851    #[test]
852    fn wasm_core_merkle_bindings_bound_untrusted_arrays() {
853        let source = include_str!("core_bindings.rs");
854        assert!(
855            source.contains("MAX_WASM_MERKLE_LEAVES"),
856            "WASM Merkle root/proof bindings must cap caller-supplied leaf arrays"
857        );
858        assert!(
859            source.contains("MAX_WASM_MERKLE_PROOF_HASHES"),
860            "WASM Merkle verification must cap caller-supplied proof arrays"
861        );
862    }
863
864    #[test]
865    fn wasm_secret_key_decoding_zeroizes_rust_owned_buffers() {
866        let sources = [
867            ("identity_bindings.rs", include_str!("identity_bindings.rs")),
868            (
869                "messaging_bindings.rs",
870                include_str!("messaging_bindings.rs"),
871            ),
872        ];
873
874        for (path, source) in sources {
875            assert!(
876                source.contains("Zeroizing"),
877                "{path} must wrap decoded secret-key buffers in zeroize::Zeroizing"
878            );
879        }
880
881        let messaging_source = include_str!("messaging_bindings.rs");
882        let x25519_helper = messaging_source
883            .split("fn parse_x25519_keypair_hex")
884            .nth(1)
885            .and_then(|rest| {
886                rest.split("/// Attach a caller-produced Ed25519 signature")
887                    .next()
888            })
889            .expect("messaging X25519 secret parser must be present");
890        assert!(
891            x25519_helper.contains("Zeroizing::new("),
892            "messaging X25519 secret parser must zeroize Rust-owned decoded buffers"
893        );
894    }
895
896    #[test]
897    fn wasm_gatekeeper_boundary_redacts_internal_errors_and_state() {
898        let source = include_str!("gatekeeper_bindings.rs");
899        assert!(
900            source.contains("gatekeeper_boundary_error"),
901            "gatekeeper WASM bindings must centralize sanitized boundary errors"
902        );
903        assert!(
904            source.contains("holon_state_label"),
905            "gatekeeper WASM bindings must expose explicit lifecycle labels"
906        );
907
908        let forbidden = [
909            "format!(\"Reduction error: {e}\")",
910            "format!(\"Step error: {e}\")",
911            "format!(\"{:?}\", holon.state)",
912        ];
913
914        for pattern in forbidden {
915            assert!(
916                !source.contains(pattern),
917                "gatekeeper WASM boundary must not expose internal debug/error details via {pattern}"
918            );
919        }
920    }
921
922    #[test]
923    fn wasm_escalation_kanban_validator_uses_bounded_json_bridge() {
924        let source = include_str!("escalation_bindings.rs");
925        assert!(
926            source.contains("from_json_str(column_json)"),
927            "Kanban column validation must use the bounded JSON bridge"
928        );
929        assert!(
930            !source.contains("serde_json::from_str(column_json)"),
931            "Kanban column validation must not bypass the bounded JSON bridge"
932        );
933        assert!(
934            !source.contains("\"error\": e.to_string()"),
935            "Kanban column validation must not return raw serde errors to WASM callers"
936        );
937    }
938}