Skip to main content

rns_ctl/
api.rs

1use serde_json::{json, Value};
2
3use rns_net::{
4    Destination, QueryRequest, QueryResponse, RnsNode,
5    DestHash, IdentityHash, ProofStrategy,
6};
7use rns_crypto::identity::Identity;
8
9use crate::auth::check_auth;
10use crate::config::CtlConfig;
11use crate::encode::{from_base64, hex_to_array, to_base64, to_hex};
12use crate::http::{parse_query, HttpRequest, HttpResponse};
13use crate::state::{DestinationEntry, SharedState};
14
15/// Handle for the node, wrapped so shutdown() can consume it.
16pub type NodeHandle = std::sync::Arc<std::sync::Mutex<Option<RnsNode>>>;
17
18/// Execute a closure with a reference to the node, returning an error response if the node is gone.
19fn with_node<F>(node: &NodeHandle, f: F) -> HttpResponse
20where
21    F: FnOnce(&RnsNode) -> HttpResponse,
22{
23    let guard = node.lock().unwrap();
24    match guard.as_ref() {
25        Some(n) => f(n),
26        None => HttpResponse::internal_error("Node is shutting down"),
27    }
28}
29
30/// Route dispatch: match method + path and call the appropriate handler.
31pub fn handle_request(
32    req: &HttpRequest,
33    node: &NodeHandle,
34    state: &SharedState,
35    config: &CtlConfig,
36) -> HttpResponse {
37    // Health check — no auth required
38    if req.method == "GET" && req.path == "/health" {
39        return HttpResponse::ok(json!({"status": "healthy"}));
40    }
41
42    // All other endpoints require auth
43    if let Err(resp) = check_auth(req, config) {
44        return resp;
45    }
46
47    match (req.method.as_str(), req.path.as_str()) {
48        // Read endpoints
49        ("GET", "/api/info") => handle_info(node, state),
50        ("GET", "/api/interfaces") => handle_interfaces(node),
51        ("GET", "/api/destinations") => handle_destinations(node, state),
52        ("GET", "/api/paths") => handle_paths(req, node),
53        ("GET", "/api/links") => handle_links(node),
54        ("GET", "/api/resources") => handle_resources(node),
55        ("GET", "/api/announces") => handle_event_list(req, state, "announces"),
56        ("GET", "/api/packets") => handle_event_list(req, state, "packets"),
57        ("GET", "/api/proofs") => handle_event_list(req, state, "proofs"),
58
59        // Identity recall: /api/identity/<dest_hash>
60        ("GET", path) if path.starts_with("/api/identity/") => {
61            let hash_str = &path["/api/identity/".len()..];
62            handle_recall_identity(hash_str, node)
63        }
64
65        // Action endpoints
66        ("POST", "/api/destination") => handle_post_destination(req, node, state),
67        ("POST", "/api/announce") => handle_post_announce(req, node, state),
68        ("POST", "/api/send") => handle_post_send(req, node, state),
69        ("POST", "/api/link") => handle_post_link(req, node),
70        ("POST", "/api/link/send") => handle_post_link_send(req, node),
71        ("POST", "/api/link/close") => handle_post_link_close(req, node),
72        ("POST", "/api/channel") => handle_post_channel(req, node),
73        ("POST", "/api/resource") => handle_post_resource(req, node),
74        ("POST", "/api/path/request") => handle_post_path_request(req, node),
75
76        _ => HttpResponse::not_found(),
77    }
78}
79
80// --- Read handlers ---
81
82fn handle_info(node: &NodeHandle, state: &SharedState) -> HttpResponse {
83    with_node(node, |n| {
84        let transport_id = match n.query(QueryRequest::TransportIdentity) {
85            Ok(QueryResponse::TransportIdentity(id)) => id,
86            _ => None,
87        };
88        let s = state.read().unwrap();
89        HttpResponse::ok(json!({
90            "transport_id": transport_id.map(|h| to_hex(&h)),
91            "identity_hash": s.identity_hash.map(|h| to_hex(&h)),
92            "uptime_seconds": s.uptime_seconds(),
93        }))
94    })
95}
96
97fn handle_interfaces(node: &NodeHandle) -> HttpResponse {
98    with_node(node, |n| {
99        match n.query(QueryRequest::InterfaceStats) {
100            Ok(QueryResponse::InterfaceStats(stats)) => {
101                let ifaces: Vec<Value> = stats
102                    .interfaces
103                    .iter()
104                    .map(|i| {
105                        json!({
106                            "name": i.name,
107                            "status": if i.status { "up" } else { "down" },
108                            "mode": i.mode,
109                            "interface_type": i.interface_type,
110                            "rxb": i.rxb,
111                            "txb": i.txb,
112                            "rx_packets": i.rx_packets,
113                            "tx_packets": i.tx_packets,
114                            "bitrate": i.bitrate,
115                            "started": i.started,
116                            "ia_freq": i.ia_freq,
117                            "oa_freq": i.oa_freq,
118                        })
119                    })
120                    .collect();
121                HttpResponse::ok(json!({
122                    "interfaces": ifaces,
123                    "transport_enabled": stats.transport_enabled,
124                    "transport_uptime": stats.transport_uptime,
125                    "total_rxb": stats.total_rxb,
126                    "total_txb": stats.total_txb,
127                }))
128            }
129            _ => HttpResponse::internal_error("Query failed"),
130        }
131    })
132}
133
134fn handle_destinations(node: &NodeHandle, state: &SharedState) -> HttpResponse {
135    with_node(node, |n| {
136        match n.query(QueryRequest::LocalDestinations) {
137            Ok(QueryResponse::LocalDestinations(dests)) => {
138                let s = state.read().unwrap();
139                let list: Vec<Value> = dests
140                    .iter()
141                    .map(|d| {
142                        let name = s
143                            .destinations
144                            .get(&d.hash)
145                            .map(|e| e.full_name.as_str())
146                            .unwrap_or("");
147                        json!({
148                            "hash": to_hex(&d.hash),
149                            "type": d.dest_type,
150                            "name": name,
151                        })
152                    })
153                    .collect();
154                HttpResponse::ok(json!({"destinations": list}))
155            }
156            _ => HttpResponse::internal_error("Query failed"),
157        }
158    })
159}
160
161fn handle_paths(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
162    let params = parse_query(&req.query);
163    let filter_hash: Option<[u8; 16]> = params
164        .get("dest_hash")
165        .and_then(|s| hex_to_array(s));
166
167    with_node(node, |n| {
168        match n.query(QueryRequest::PathTable { max_hops: None }) {
169            Ok(QueryResponse::PathTable(paths)) => {
170                let list: Vec<Value> = paths
171                    .iter()
172                    .filter(|p| filter_hash.map_or(true, |h| p.hash == h))
173                    .map(|p| {
174                        json!({
175                            "hash": to_hex(&p.hash),
176                            "via": to_hex(&p.via),
177                            "hops": p.hops,
178                            "expires": p.expires,
179                            "interface": p.interface_name,
180                            "timestamp": p.timestamp,
181                        })
182                    })
183                    .collect();
184                HttpResponse::ok(json!({"paths": list}))
185            }
186            _ => HttpResponse::internal_error("Query failed"),
187        }
188    })
189}
190
191fn handle_links(node: &NodeHandle) -> HttpResponse {
192    with_node(node, |n| {
193        match n.query(QueryRequest::Links) {
194            Ok(QueryResponse::Links(links)) => {
195                let list: Vec<Value> = links
196                    .iter()
197                    .map(|l| {
198                        json!({
199                            "link_id": to_hex(&l.link_id),
200                            "state": l.state,
201                            "is_initiator": l.is_initiator,
202                            "dest_hash": to_hex(&l.dest_hash),
203                            "remote_identity": l.remote_identity.map(|h| to_hex(&h)),
204                            "rtt": l.rtt,
205                        })
206                    })
207                    .collect();
208                HttpResponse::ok(json!({"links": list}))
209            }
210            _ => HttpResponse::internal_error("Query failed"),
211        }
212    })
213}
214
215fn handle_resources(node: &NodeHandle) -> HttpResponse {
216    with_node(node, |n| {
217        match n.query(QueryRequest::Resources) {
218            Ok(QueryResponse::Resources(resources)) => {
219                let list: Vec<Value> = resources
220                    .iter()
221                    .map(|r| {
222                        json!({
223                            "link_id": to_hex(&r.link_id),
224                            "direction": r.direction,
225                            "total_parts": r.total_parts,
226                            "transferred_parts": r.transferred_parts,
227                            "complete": r.complete,
228                        })
229                    })
230                    .collect();
231                HttpResponse::ok(json!({"resources": list}))
232            }
233            _ => HttpResponse::internal_error("Query failed"),
234        }
235    })
236}
237
238fn handle_event_list(req: &HttpRequest, state: &SharedState, kind: &str) -> HttpResponse {
239    let params = parse_query(&req.query);
240    let clear = params.get("clear").map_or(false, |v| v == "true");
241
242    let mut s = state.write().unwrap();
243    let items: Vec<Value> = match kind {
244        "announces" => {
245            let v: Vec<Value> = s
246                .announces
247                .iter()
248                .map(|r| serde_json::to_value(r).unwrap_or_default())
249                .collect();
250            if clear {
251                s.announces.clear();
252            }
253            v
254        }
255        "packets" => {
256            let v: Vec<Value> = s
257                .packets
258                .iter()
259                .map(|r| serde_json::to_value(r).unwrap_or_default())
260                .collect();
261            if clear {
262                s.packets.clear();
263            }
264            v
265        }
266        "proofs" => {
267            let v: Vec<Value> = s
268                .proofs
269                .iter()
270                .map(|r| serde_json::to_value(r).unwrap_or_default())
271                .collect();
272            if clear {
273                s.proofs.clear();
274            }
275            v
276        }
277        _ => Vec::new(),
278    };
279
280    let mut obj = serde_json::Map::new();
281    obj.insert(kind.to_string(), Value::Array(items));
282    HttpResponse::ok(Value::Object(obj))
283}
284
285fn handle_recall_identity(hash_str: &str, node: &NodeHandle) -> HttpResponse {
286    let dest_hash: [u8; 16] = match hex_to_array(hash_str) {
287        Some(h) => h,
288        None => return HttpResponse::bad_request("Invalid dest_hash hex (expected 32 hex chars)"),
289    };
290
291    with_node(node, |n| {
292        match n.recall_identity(&DestHash(dest_hash)) {
293            Ok(Some(ai)) => HttpResponse::ok(json!({
294                "dest_hash": to_hex(&ai.dest_hash.0),
295                "identity_hash": to_hex(&ai.identity_hash.0),
296                "public_key": to_hex(&ai.public_key),
297                "app_data": ai.app_data.as_ref().map(|d| to_base64(d)),
298                "hops": ai.hops,
299                "received_at": ai.received_at,
300            })),
301            Ok(None) => HttpResponse::not_found(),
302            Err(_) => HttpResponse::internal_error("Query failed"),
303        }
304    })
305}
306
307// --- Action handlers ---
308
309fn parse_json_body(req: &HttpRequest) -> Result<Value, HttpResponse> {
310    serde_json::from_slice(&req.body).map_err(|e| HttpResponse::bad_request(&format!("Invalid JSON: {}", e)))
311}
312
313fn handle_post_destination(
314    req: &HttpRequest,
315    node: &NodeHandle,
316    state: &SharedState,
317) -> HttpResponse {
318    let body = match parse_json_body(req) {
319        Ok(v) => v,
320        Err(r) => return r,
321    };
322
323    let dest_type_str = body["type"].as_str().unwrap_or("");
324    let app_name = match body["app_name"].as_str() {
325        Some(s) => s,
326        None => return HttpResponse::bad_request("Missing app_name"),
327    };
328    let aspects: Vec<&str> = body["aspects"]
329        .as_array()
330        .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
331        .unwrap_or_default();
332
333    let (identity_hash, identity_prv_key) = {
334        let s = state.read().unwrap();
335        let ih = s.identity_hash;
336        let prv = s.identity.as_ref().and_then(|i| i.get_private_key());
337        (ih, prv)
338    };
339
340    let (dest, signing_key) = match dest_type_str {
341        "single" => {
342            let direction = body["direction"].as_str().unwrap_or("in");
343            match direction {
344                "in" => {
345                    let ih = match identity_hash {
346                        Some(h) => IdentityHash(h),
347                        None => return HttpResponse::internal_error("No identity loaded"),
348                    };
349                    let dest = Destination::single_in(app_name, &aspects, ih)
350                        .set_proof_strategy(parse_proof_strategy(&body));
351                    (dest, identity_prv_key)
352                }
353                "out" => {
354                    let dh_str = match body["dest_hash"].as_str() {
355                        Some(s) => s,
356                        None => return HttpResponse::bad_request("OUT single requires dest_hash of remote"),
357                    };
358                    let dh: [u8; 16] = match hex_to_array(dh_str) {
359                        Some(h) => h,
360                        None => return HttpResponse::bad_request("Invalid dest_hash"),
361                    };
362                    return with_node(node, |n| {
363                        match n.recall_identity(&DestHash(dh)) {
364                            Ok(Some(recalled)) => {
365                                let dest = Destination::single_out(app_name, &aspects, &recalled);
366                                // Register in state
367                                let full_name = format_dest_name(app_name, &aspects);
368                                let mut s = state.write().unwrap();
369                                s.destinations.insert(dest.hash.0, DestinationEntry {
370                                    destination: dest.clone(),
371                                    full_name: full_name.clone(),
372                                });
373                                HttpResponse::created(json!({
374                                    "dest_hash": to_hex(&dest.hash.0),
375                                    "name": full_name,
376                                    "type": "single",
377                                    "direction": "out",
378                                }))
379                            }
380                            Ok(None) => HttpResponse::bad_request("No recalled identity for dest_hash"),
381                            Err(_) => HttpResponse::internal_error("Query failed"),
382                        }
383                    });
384                }
385                _ => return HttpResponse::bad_request("direction must be 'in' or 'out'"),
386            }
387        }
388        "plain" => {
389            let dest = Destination::plain(app_name, &aspects)
390                .set_proof_strategy(parse_proof_strategy(&body));
391            (dest, None)
392        }
393        "group" => {
394            let mut dest = Destination::group(app_name, &aspects)
395                .set_proof_strategy(parse_proof_strategy(&body));
396            if let Some(key_b64) = body["group_key"].as_str() {
397                match from_base64(key_b64) {
398                    Some(key) => {
399                        if let Err(e) = dest.load_private_key(key) {
400                            return HttpResponse::bad_request(&format!("Invalid group key: {}", e));
401                        }
402                    }
403                    None => return HttpResponse::bad_request("Invalid base64 group_key"),
404                }
405            } else {
406                dest.create_keys();
407            }
408            (dest, None)
409        }
410        _ => return HttpResponse::bad_request("type must be 'single', 'plain', or 'group'"),
411    };
412
413    with_node(node, |n| {
414        match n.register_destination_with_proof(&dest, signing_key) {
415            Ok(()) => {
416                let full_name = format_dest_name(app_name, &aspects);
417                let hash_hex = to_hex(&dest.hash.0);
418                let group_key_b64 = dest.get_private_key().map(to_base64);
419                let mut s = state.write().unwrap();
420                s.destinations.insert(
421                    dest.hash.0,
422                    DestinationEntry {
423                        destination: dest,
424                        full_name: full_name.clone(),
425                    },
426                );
427                let mut resp = json!({
428                    "dest_hash": hash_hex,
429                    "name": full_name,
430                    "type": dest_type_str,
431                });
432                if let Some(gk) = group_key_b64 {
433                    resp["group_key"] = Value::String(gk);
434                }
435                HttpResponse::created(resp)
436            }
437            Err(_) => HttpResponse::internal_error("Failed to register destination"),
438        }
439    })
440}
441
442fn handle_post_announce(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
443    let body = match parse_json_body(req) {
444        Ok(v) => v,
445        Err(r) => return r,
446    };
447
448    let dh_str = match body["dest_hash"].as_str() {
449        Some(s) => s,
450        None => return HttpResponse::bad_request("Missing dest_hash"),
451    };
452    let dh: [u8; 16] = match hex_to_array(dh_str) {
453        Some(h) => h,
454        None => return HttpResponse::bad_request("Invalid dest_hash"),
455    };
456
457    let app_data: Option<Vec<u8>> = body["app_data"]
458        .as_str()
459        .and_then(from_base64);
460
461    let (dest, identity) = {
462        let s = state.read().unwrap();
463        let dest = match s.destinations.get(&dh) {
464            Some(entry) => entry.destination.clone(),
465            None => return HttpResponse::bad_request("Destination not registered via API"),
466        };
467        let identity = match s.identity.as_ref().and_then(|i| i.get_private_key()) {
468            Some(prv) => Identity::from_private_key(&prv),
469            None => return HttpResponse::internal_error("No identity loaded"),
470        };
471        (dest, identity)
472    };
473
474    with_node(node, |n| {
475        match n.announce(&dest, &identity, app_data.as_deref()) {
476            Ok(()) => HttpResponse::ok(json!({"status": "announced", "dest_hash": dh_str})),
477            Err(_) => HttpResponse::internal_error("Announce failed"),
478        }
479    })
480}
481
482fn handle_post_send(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
483    let body = match parse_json_body(req) {
484        Ok(v) => v,
485        Err(r) => return r,
486    };
487
488    let dh_str = match body["dest_hash"].as_str() {
489        Some(s) => s,
490        None => return HttpResponse::bad_request("Missing dest_hash"),
491    };
492    let dh: [u8; 16] = match hex_to_array(dh_str) {
493        Some(h) => h,
494        None => return HttpResponse::bad_request("Invalid dest_hash"),
495    };
496    let data = match body["data"].as_str().and_then(from_base64) {
497        Some(d) => d,
498        None => return HttpResponse::bad_request("Missing or invalid base64 data"),
499    };
500
501    let s = state.read().unwrap();
502    let dest = match s.destinations.get(&dh) {
503        Some(entry) => entry.destination.clone(),
504        None => return HttpResponse::bad_request("Destination not registered via API"),
505    };
506    drop(s);
507
508    with_node(node, |n| {
509        match n.send_packet(&dest, &data) {
510            Ok(ph) => HttpResponse::ok(json!({
511                "status": "sent",
512                "packet_hash": to_hex(&ph.0),
513            })),
514            Err(_) => HttpResponse::internal_error("Send failed"),
515        }
516    })
517}
518
519fn handle_post_link(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
520    let body = match parse_json_body(req) {
521        Ok(v) => v,
522        Err(r) => return r,
523    };
524
525    let dh_str = match body["dest_hash"].as_str() {
526        Some(s) => s,
527        None => return HttpResponse::bad_request("Missing dest_hash"),
528    };
529    let dh: [u8; 16] = match hex_to_array(dh_str) {
530        Some(h) => h,
531        None => return HttpResponse::bad_request("Invalid dest_hash"),
532    };
533
534    with_node(node, |n| {
535        // Recall identity to get signing public key
536        let recalled = match n.recall_identity(&DestHash(dh)) {
537            Ok(Some(ai)) => ai,
538            Ok(None) => return HttpResponse::bad_request("No recalled identity for dest_hash"),
539            Err(_) => return HttpResponse::internal_error("Query failed"),
540        };
541        // Extract Ed25519 public key (second 32 bytes of public_key)
542        let mut sig_pub = [0u8; 32];
543        sig_pub.copy_from_slice(&recalled.public_key[32..64]);
544
545        match n.create_link(dh, sig_pub) {
546            Ok(link_id) => HttpResponse::created(json!({
547                "link_id": to_hex(&link_id),
548            })),
549            Err(_) => HttpResponse::internal_error("Create link failed"),
550        }
551    })
552}
553
554fn handle_post_link_send(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
555    let body = match parse_json_body(req) {
556        Ok(v) => v,
557        Err(r) => return r,
558    };
559
560    let link_id: [u8; 16] = match body["link_id"]
561        .as_str()
562        .and_then(|s| hex_to_array(s))
563    {
564        Some(h) => h,
565        None => return HttpResponse::bad_request("Missing or invalid link_id"),
566    };
567    let data = match body["data"].as_str().and_then(from_base64) {
568        Some(d) => d,
569        None => return HttpResponse::bad_request("Missing or invalid base64 data"),
570    };
571    let context = body["context"].as_u64().unwrap_or(0) as u8;
572
573    with_node(node, |n| {
574        match n.send_on_link(link_id, data, context) {
575            Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
576            Err(_) => HttpResponse::internal_error("Send on link failed"),
577        }
578    })
579}
580
581fn handle_post_link_close(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
582    let body = match parse_json_body(req) {
583        Ok(v) => v,
584        Err(r) => return r,
585    };
586
587    let link_id: [u8; 16] = match body["link_id"]
588        .as_str()
589        .and_then(|s| hex_to_array(s))
590    {
591        Some(h) => h,
592        None => return HttpResponse::bad_request("Missing or invalid link_id"),
593    };
594
595    with_node(node, |n| {
596        match n.teardown_link(link_id) {
597            Ok(()) => HttpResponse::ok(json!({"status": "closed"})),
598            Err(_) => HttpResponse::internal_error("Teardown link failed"),
599        }
600    })
601}
602
603fn handle_post_channel(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
604    let body = match parse_json_body(req) {
605        Ok(v) => v,
606        Err(r) => return r,
607    };
608
609    let link_id: [u8; 16] = match body["link_id"]
610        .as_str()
611        .and_then(|s| hex_to_array(s))
612    {
613        Some(h) => h,
614        None => return HttpResponse::bad_request("Missing or invalid link_id"),
615    };
616    let msgtype = body["msgtype"].as_u64().unwrap_or(0) as u16;
617    let payload = match body["payload"].as_str().and_then(from_base64) {
618        Some(d) => d,
619        None => return HttpResponse::bad_request("Missing or invalid base64 payload"),
620    };
621
622    with_node(node, |n| {
623        match n.send_channel_message(link_id, msgtype, payload) {
624            Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
625            Err(_) => HttpResponse::internal_error("Channel message failed"),
626        }
627    })
628}
629
630fn handle_post_resource(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
631    let body = match parse_json_body(req) {
632        Ok(v) => v,
633        Err(r) => return r,
634    };
635
636    let link_id: [u8; 16] = match body["link_id"]
637        .as_str()
638        .and_then(|s| hex_to_array(s))
639    {
640        Some(h) => h,
641        None => return HttpResponse::bad_request("Missing or invalid link_id"),
642    };
643    let data = match body["data"].as_str().and_then(from_base64) {
644        Some(d) => d,
645        None => return HttpResponse::bad_request("Missing or invalid base64 data"),
646    };
647    let metadata = body["metadata"]
648        .as_str()
649        .and_then(from_base64);
650
651    with_node(node, |n| {
652        match n.send_resource(link_id, data, metadata) {
653            Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
654            Err(_) => HttpResponse::internal_error("Resource send failed"),
655        }
656    })
657}
658
659fn handle_post_path_request(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
660    let body = match parse_json_body(req) {
661        Ok(v) => v,
662        Err(r) => return r,
663    };
664
665    let dh_str = match body["dest_hash"].as_str() {
666        Some(s) => s,
667        None => return HttpResponse::bad_request("Missing dest_hash"),
668    };
669    let dh: [u8; 16] = match hex_to_array(dh_str) {
670        Some(h) => h,
671        None => return HttpResponse::bad_request("Invalid dest_hash"),
672    };
673
674    with_node(node, |n| {
675        match n.request_path(&DestHash(dh)) {
676            Ok(()) => HttpResponse::ok(json!({"status": "requested"})),
677            Err(_) => HttpResponse::internal_error("Path request failed"),
678        }
679    })
680}
681
682// --- Helpers ---
683
684fn format_dest_name(app_name: &str, aspects: &[&str]) -> String {
685    if aspects.is_empty() {
686        app_name.to_string()
687    } else {
688        format!("{}.{}", app_name, aspects.join("."))
689    }
690}
691
692fn parse_proof_strategy(body: &Value) -> ProofStrategy {
693    match body["proof_strategy"].as_str() {
694        Some("all") => ProofStrategy::ProveAll,
695        Some("app") => ProofStrategy::ProveApp,
696        _ => ProofStrategy::ProveNone,
697    }
698}