Skip to main content

secs_types/
local_api.rs

1//! Shared wire contract for the opt-in local HTTP and WebSocket API.
2//!
3//! The server and clients use these paths and payloads. The security and
4//! lifecycle decisions are recorded in ADR 0016.
5
6use serde::{Deserialize, Serialize};
7
8use crate::ipc::{ExportKind, JobEvent, JobSnapshot, ProjectInfo, ResultsSummary};
9
10/// Local API protocol version.
11pub const API_VERSION: u32 = 1;
12/// Default loopback TCP port.
13pub const DEFAULT_PORT: u16 = 4712;
14/// Token file name inside the application configuration directory.
15pub const TOKEN_FILE: &str = "local-api-token";
16/// Request dispatch endpoint.
17pub const CALL_PATH: &str = "/v1/call";
18/// Aggregate status endpoint.
19pub const STATUS_PATH: &str = "/v1/status";
20/// WebSocket event endpoint.
21pub const EVENTS_PATH: &str = "/v1/events";
22
23/// Aggregate state returned by [`STATUS_PATH`].
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Status {
26    /// [`API_VERSION`].
27    pub api: u32,
28    /// Product name.
29    pub app_name: String,
30    /// Version of the running application.
31    pub app_version: String,
32    /// Version of the linked `secs-service` crate.
33    pub library_version: String,
34    /// Open project, when present.
35    pub project: Option<ProjectInfo>,
36    /// Current and retained job snapshots.
37    pub jobs: Vec<JobSnapshot>,
38    /// Latest calibration and MIMO summary, when a project has results.
39    pub results: Option<ResultsSummary>,
40    /// Most recent successful export in this service session.
41    pub last_export: Option<LastExport>,
42}
43
44/// One successful export remembered by the service session.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct LastExport {
47    /// Export bundle kind.
48    pub kind: ExportKind,
49    /// Actual folder written by the export.
50    pub folder: String,
51    /// Whether the export included MIMO support paths.
52    pub mimo: bool,
53    /// Stable correction-mode name.
54    pub mode: String,
55    /// The design name a filter export carried (`ExportParams::design_name`),
56    /// `None` for an unnamed or REW mdat export.
57    #[serde(default)]
58    pub design_name: Option<String>,
59    /// The design identity in `secs-design.json`, for a filter export.
60    #[serde(default)]
61    pub design_id: Option<String>,
62    /// Completion time in UTC, formatted as RFC 3339.
63    pub time: String,
64}
65
66/// Event sent as one text frame on [`EVENTS_PATH`].
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(tag = "kind", rename_all = "snake_case")]
69pub enum ServerEvent {
70    /// A service job event, regardless of which caller started the job.
71    Job { event: JobEvent },
72    /// A successful local-API request changed observable state.
73    Changed { method: String },
74}
75
76/// Local-API policy for one IPC method.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct MethodAccess {
79    /// The method opens a window or program and cannot be called remotely.
80    pub ui_only: bool,
81    /// A successful call changes observable state or starts work.
82    pub mutates: bool,
83}
84
85const fn entry(method: &'static str, ui_only: bool, mutates: bool) -> (&'static str, MethodAccess) {
86    (method, MethodAccess { ui_only, mutates })
87}
88
89const ACCESS: &[(&str, MethodAccess)] = &[
90    entry("bootstrap", false, false),
91    entry("project_new", false, true),
92    entry("project_open", false, true),
93    entry("project_save", false, true),
94    entry("project_info", false, false),
95    entry("sample_load", false, true),
96    entry("calibrate", false, true),
97    entry("recommend_xo", false, true),
98    entry("mimo", false, true),
99    entry("results", false, false),
100    entry("export", false, true),
101    entry("job_cancel", false, true),
102    entry("jobs", false, false),
103    entry("measure_methods", false, false),
104    entry("measure_configure", false, true),
105    entry("measure_config_get", false, false),
106    entry("measurement_records", false, false),
107    entry("mic_calibration_get", false, false),
108    entry("mic_calibration_set", false, true),
109    entry("mic_calibration_clear", false, true),
110    entry("mic_calibration_attach", false, true),
111    entry("levels_get", false, false),
112    entry("levels_fill_defaults", false, true),
113    entry("level_stop", false, true),
114    entry("measure_position", false, true),
115    entry("position_clear", false, true),
116    entry("undo", false, true),
117    entry("rew_launch", true, true),
118    entry("mdat_scan", false, false),
119    entry("mdat_import", false, true),
120    entry("config_get", false, false),
121    entry("config_apply_preset", false, true),
122    entry("config_set_sub_count", false, true),
123    entry("config_set_speaker", false, true),
124    entry("config_add_speaker", false, true),
125    entry("config_remove_speaker", false, true),
126    entry("config_renumber", false, true),
127    entry("config_validate", false, false),
128    entry("patch_sheet_csv", false, true),
129    entry("dsp_get", false, false),
130    entry("dsp_set", false, true),
131    entry("layout_get", false, false),
132    entry("layout_set", false, true),
133    entry("position_estimate", false, false),
134    entry("target_get", false, false),
135    entry("target_set_points", false, true),
136    entry("target_apply_preset", false, true),
137    entry("target_save", false, true),
138    entry("target_load", false, true),
139    entry("calibration_settings_get", false, false),
140    entry("calibration_settings_set", false, true),
141    entry("calibration_settings_edit", false, true),
142    entry("sub_warnings", false, false),
143    entry("rew_info", false, false),
144    entry("rew_driver_set", false, true),
145    entry("mode_spec", false, false),
146    entry("app_settings_get", false, false),
147    entry("app_settings_set", false, true),
148    entry("apply_status", false, false),
149    entry("apply_camilla", false, true),
150    entry("apply_eqapo", false, true),
151    entry("revert_eqapo", false, true),
152    entry("apply_settings_set", false, true),
153    entry("host_install", true, true),
154    entry("host_uninstall", true, true),
155    entry("host_stop_managing", false, true),
156    // Installing a release replaces the app; only its own screen asks for it.
157    entry("update_status", false, false),
158    entry("update_check", true, false),
159    entry("update_download", true, true),
160    entry("update_restart", true, true),
161    entry("update_notice_dismiss", true, true),
162    entry("update_open_release", true, false),
163    entry("compute_status", false, false),
164    entry("open_gpu_help", true, false),
165    entry("measurement_curves", false, false),
166    entry("result_curves", false, false),
167    entry("result_lines", false, false),
168    entry("sub_take_values", false, true),
169    entry("dialog_open_project", true, false),
170    entry("dialog_save_project", true, false),
171    entry("dialog_pick_mdat", true, false),
172    entry("dialog_pick_folder", true, false),
173    entry("dialog_save_file", true, false),
174    entry("dialog_open_file", true, false),
175    entry("open_folder", true, true),
176    entry("level_monitor_start", false, true),
177    entry("level_monitor_stop", false, true),
178    entry("level_play", false, true),
179    entry("status_watch_start", false, true),
180    entry("status_watch_stop", false, true),
181    entry("export_folder_get", false, false),
182    entry("export_folder_set", false, true),
183    entry("local_api_status", true, false),
184    entry("local_api_token_regenerate", true, true),
185];
186
187/// Returns the local-API policy for a wire method name.
188pub fn access(method: &str) -> Option<MethodAccess> {
189    ACCESS
190        .iter()
191        .find_map(|(name, value)| (*name == method).then_some(*value))
192}
193
194#[cfg(test)]
195mod tests {
196    use std::collections::BTreeSet;
197
198    use super::*;
199    use crate::ipc::Request;
200
201    #[test]
202    fn access_table_covers_every_request_method_exactly() {
203        let known = Request::METHODS.iter().copied().collect::<BTreeSet<_>>();
204        let listed = ACCESS.iter().map(|(name, _)| *name).collect::<Vec<_>>();
205        let unique = listed.iter().copied().collect::<BTreeSet<_>>();
206        assert_eq!(listed.len(), unique.len(), "duplicate access-table method");
207        assert_eq!(unique, known);
208        for method in Request::METHODS {
209            assert!(access(method).is_some(), "missing {method}");
210        }
211    }
212}