1
//! Canonical host-fn registry for the 32 server commands.
2
//!
3
//! Each domain owns a submodule that mirrors the `server::command::*` shape and
4
//! holds one file per server command. Commands register their host fn through a
5
//! per-domain `register` entry point; the top-level `register_all` aggregates
6
//! every domain so a wasmtime [`Linker`](wasmtime::Linker) gains the full
7
//! canonical surface in one call.
8
//!
9
//! Subsequent commits fill each domain. This commit lays down the directory
10
//! shape so per-command commits stay focused on one server command at a time.
11

            
12
use wasmtime::{ArrayRef, Linker, Rooted};
13

            
14
use crate::session::SessionData;
15

            
16
/// One stringly-typed wasm host-fn argument as it lands on the
17
/// `linker.func_wrap_async` closure: nullable because the arg can be
18
/// absent, `Rooted<ArrayRef>` because the wasm side encodes strings as
19
/// `(ref null $i8_array)`.
20
pub(crate) type StringArg = Option<Rooted<ArrayRef>>;
21
/// Three-arg tuple shape used by `account_set_account_tag` and other
22
/// host fns that take three string args.
23
pub(crate) type StringArgTriple = (StringArg, StringArg, StringArg);
24

            
25
pub mod account;
26
pub mod catch_each;
27
pub mod commodity;
28
pub mod config;
29
pub mod env_io;
30
mod generated_specs;
31
pub mod meta;
32
pub mod raise;
33
mod render;
34
pub mod report;
35
pub mod split;
36
pub mod ssh_key;
37
pub mod template;
38
pub mod transaction;
39

            
40
pub use generated_specs::all_compiler_specs;
41
pub use render::{RENDER_NATIVE_ALLOWLIST, link_render, render_compiler_specs};
42

            
43
/// Aggregator: registers every domain's host fns on `linker` in one call.
44
/// Bound to [`SessionData`] now that `meta` natives consult `ScriptCtx`
45
/// (user_id) — generic-T register fns from the empty per-domain modules
46
/// still satisfy this concrete type because they don't touch the Store data.
47
7432
pub fn link(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
48
7432
    meta::register(linker)?;
49
7432
    env_io::register(linker)?;
50
7432
    raise::register(linker)?;
51
7432
    catch_each::register(linker)?;
52
7432
    account::register(linker)?;
53
7432
    commodity::register(linker)?;
54
7432
    config::register(linker)?;
55
7432
    report::register(linker)?;
56
7432
    split::register(linker)?;
57
7432
    ssh_key::register(linker)?;
58
7432
    template::register(linker)?;
59
7432
    transaction::register(linker)?;
60
7432
    Ok(())
61
7432
}
62

            
63
// `all_compiler_specs` lives in `generated_specs` (tangled from
64
// `doc/scripting/native_reference.org` by `rpc/build.rs`) — see the
65
// `pub use` import above. Adding or modifying a native means editing
66
// the org table; cargo build regenerates the Rust.
67

            
68
#[must_use]
69
5
pub fn all_registered_commands() -> Vec<&'static str> {
70
5
    [
71
5
        account::REGISTERED_COMMANDS,
72
5
        commodity::REGISTERED_COMMANDS,
73
5
        config::REGISTERED_COMMANDS,
74
5
        report::REGISTERED_COMMANDS,
75
5
        split::REGISTERED_COMMANDS,
76
5
        ssh_key::REGISTERED_COMMANDS,
77
5
        transaction::REGISTERED_COMMANDS,
78
5
    ]
79
5
    .iter()
80
35
    .flat_map(|d| d.iter().copied())
81
5
    .collect()
82
5
}
83

            
84
#[cfg(test)]
85
mod tests {
86
    use super::*;
87

            
88
    #[test]
89
1
    fn registry_lists_every_planned_command() {
90
1
        let names = all_registered_commands();
91
1
        assert_eq!(names.len(), 39, "command count drifted: {names:?}");
92
1
    }
93

            
94
    #[test]
95
1
    fn registry_uses_kebab_case_only() {
96
39
        for name in all_registered_commands() {
97
39
            assert!(
98
39
                name.chars()
99
639
                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
100
                "non-kebab-case name: {name}"
101
            );
102
39
            assert!(!name.starts_with('-'), "leading dash: {name}");
103
39
            assert!(!name.ends_with('-'), "trailing dash: {name}");
104
        }
105
1
    }
106

            
107
    #[test]
108
1
    fn registry_has_no_duplicates() {
109
1
        let names = all_registered_commands();
110
1
        let mut sorted = names.clone();
111
1
        sorted.sort_unstable();
112
1
        sorted.dedup();
113
1
        assert_eq!(
114
1
            names.len(),
115
1
            sorted.len(),
116
            "duplicate command names: {names:?}"
117
        );
118
1
    }
119

            
120
    #[test]
121
1
    fn registry_excludes_verify_user_password_by_design() {
122
1
        let names = all_registered_commands();
123
1
        assert!(
124
1
            !names.contains(&"verify-user-password"),
125
            "verify-user-password took an arbitrary email and password and \
126
             returned whether they matched — an unthrottled credential oracle \
127
             against every account, callable by any verified user over the eval \
128
             channel. It had no consumer. Do not re-add it without binding it to \
129
             the calling user."
130
        );
131
1
    }
132

            
133
    #[test]
134
1
    fn registry_excludes_add_ssh_key_by_design() {
135
1
        let names = all_registered_commands();
136
1
        assert!(
137
1
            !names.contains(&"add-ssh-key"),
138
            "add-ssh-key must not be exposed via the eval channel; pubkey upload uses the dedicated ssh-copy-id exec path"
139
        );
140
1
    }
141

            
142
    #[test]
143
1
    fn each_domain_has_at_least_one_command() {
144
1
        assert!(!account::REGISTERED_COMMANDS.is_empty());
145
1
        assert!(!commodity::REGISTERED_COMMANDS.is_empty());
146
1
        assert!(!config::REGISTERED_COMMANDS.is_empty());
147
1
        assert!(!report::REGISTERED_COMMANDS.is_empty());
148
1
        assert!(!split::REGISTERED_COMMANDS.is_empty());
149
1
        assert!(!ssh_key::REGISTERED_COMMANDS.is_empty());
150
1
        assert!(!transaction::REGISTERED_COMMANDS.is_empty());
151
1
    }
152

            
153
    #[test]
154
1
    fn link_succeeds_against_session_data_linker() {
155
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
156
1
            .expect("engine");
157
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
158
1
            wasmtime::Linker::new(&engine);
159
1
        link(&mut linker).expect("link must succeed");
160
1
    }
161

            
162
    #[test]
163
1
    fn each_empty_server_domain_register_succeeds_in_isolation() {
164
        // Domains that haven't shipped real natives yet keep their `register`
165
        // fns generic over T so empty composition is cheap to verify. As
166
        // domains gain real natives (each one becoming SessionData-specific),
167
        // they migrate out of this loop into dedicated tests.
168
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
169
1
            .expect("engine");
170
        // All domains now have SessionData-specific natives. Nothing left
171
        // in the generic-T empty-domain loop, but keep the test placeholder
172
        // so the next stub-domain (if any) has an obvious home to slot
173
        // back into.
174
1
        let _engine = engine;
175
1
    }
176

            
177
    #[test]
178
1
    fn account_register_succeeds_against_session_data() {
179
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
180
1
            .expect("engine");
181
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
182
1
            wasmtime::Linker::new(&engine);
183
1
        account::register(&mut linker).expect("account::register must succeed");
184
1
    }
185

            
186
    #[test]
187
1
    fn commodity_register_succeeds_against_session_data() {
188
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
189
1
            .expect("engine");
190
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
191
1
            wasmtime::Linker::new(&engine);
192
1
        commodity::register(&mut linker).expect("commodity::register must succeed");
193
1
    }
194

            
195
    #[test]
196
1
    fn transaction_register_succeeds_against_session_data() {
197
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
198
1
            .expect("engine");
199
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
200
1
            wasmtime::Linker::new(&engine);
201
1
        transaction::register(&mut linker).expect("transaction::register must succeed");
202
1
    }
203

            
204
    #[test]
205
1
    fn report_register_succeeds_against_session_data() {
206
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
207
1
            .expect("engine");
208
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
209
1
            wasmtime::Linker::new(&engine);
210
1
        report::register(&mut linker).expect("report::register must succeed");
211
1
    }
212

            
213
    #[test]
214
1
    fn split_register_succeeds_against_session_data() {
215
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
216
1
            .expect("engine");
217
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
218
1
            wasmtime::Linker::new(&engine);
219
1
        split::register(&mut linker).expect("split::register must succeed");
220
1
    }
221

            
222
    #[test]
223
1
    fn ssh_key_register_succeeds_against_session_data() {
224
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
225
1
            .expect("engine");
226
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
227
1
            wasmtime::Linker::new(&engine);
228
1
        ssh_key::register(&mut linker).expect("ssh_key::register must succeed");
229
1
    }
230

            
231
    #[test]
232
1
    fn config_register_succeeds_against_session_data() {
233
        // config now ships server-backed natives so its register fn is
234
        // SessionData-specific. Verified in isolation alongside the empty
235
        // domains. Async engine required because the host fns are
236
        // registered via func_wrap_async.
237
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
238
1
            .expect("engine");
239
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
240
1
            wasmtime::Linker::new(&engine);
241
1
        config::register(&mut linker).expect("config::register must succeed");
242
1
    }
243
}