Skip to main content

parse_rust_server/
config.rs

1//! Server configuration.
2//!
3//! A deliberately small slice of upstream's ~292 options: what `/serverInfo` and the header
4//! layer actually need. Options are added when a route needs one, not speculatively, so that
5//! every field here has a behavior behind it.
6
7/// The parse-server version parse-rust reports as its own.
8///
9/// **This is a decision, not an oversight.** `/serverInfo` returns `parseServerVersion`, and
10/// SDKs branch on it: the Ruby SDK warns below 7.0.0, and features gate on version comparisons.
11/// Reporting `parse-rust 0.0.0` would fail every one of those checks, so the wire-compatible
12/// answer is the parse-server version whose behavior this server implements. It is the same
13/// number recorded in `PIN`.
14///
15/// If parse-rust ever needs to advertise itself distinctly, that belongs in a separate field
16/// that upstream does not define, not in this one.
17pub const REPORTED_PARSE_SERVER_VERSION: &str = "9.10.1-alpha.6";
18
19/// What this server can actually do, as reported by `GET /serverInfo`.
20///
21/// **The key set and the nesting of the `features` object are wire contract. The booleans are
22/// not.** Upstream hardcodes nearly all of them to `true` (`FeaturesRouter.js`) because upstream
23/// implements the subsystems behind them. Transcribing those literals would advertise a schema
24/// API, cloud jobs, hooks, a global config and a log API that all answer 404 here.
25///
26/// That matters because the object is not documentation: Parse Dashboard builds its UI from it,
27/// so an advertised capability becomes a button that fails when a user presses it. This is a
28/// deliberate difference from upstream, in the direction of telling the truth. Every field is
29/// `false` until the subsystem behind it exists, and flipping one is part of landing that
30/// subsystem rather than a follow-up.
31#[derive(Debug, Clone, Default)]
32pub struct FeatureSupport {
33    /// `/config`. Not implemented.
34    pub global_config: bool,
35    /// `/hooks`. Not implemented.
36    pub hooks: bool,
37    /// Cloud Code jobs. Not implemented; `TriggerHost` is the milestone that lands them.
38    pub cloud_code_jobs: bool,
39    /// The log API. Not implemented.
40    pub logs: bool,
41    /// The schema API, `/schemas`. Not implemented. Note this is the *API*: schema inference
42    /// and enforcement do work, but no route exposes them.
43    pub schemas: bool,
44    /// Push, including audiences and localization. Not implemented.
45    pub push_audiences: bool,
46}
47
48/// The keys and identity a request is checked against.
49#[derive(Debug, Clone)]
50pub struct ServerConfig {
51    pub app_id: String,
52    pub master_key: String,
53    /// The read-only master key sets `isMaster` upstream (`Auth.js:63`), with the restriction
54    /// enforced by scattered checks. Not implemented yet; recorded so the gap is visible.
55    pub maintenance_key: Option<String>,
56    pub javascript_key: Option<String>,
57    pub rest_api_key: Option<String>,
58    pub client_key: Option<String>,
59    pub dot_net_key: Option<String>,
60    /// Where the API is mounted, e.g. `/parse`. A **builder input, never inferred from the
61    /// request path**: axum's `nest` and Express's `app.use` differ here, and every generated
62    /// file URL is built from this value.
63    pub mount_path: String,
64    /// `enableSanitizedErrorResponse`, default true (`Options/Definitions.js:253-258`). When
65    /// true, a 403 from the master-key gate says `Permission denied` rather than naming the
66    /// reason.
67    pub enable_sanitized_error_response: bool,
68    pub has_push_support: bool,
69    pub has_push_scheduled_support: bool,
70    pub security_check_enabled: bool,
71    /// What `/serverInfo` advertises. Defaults to the truth: nothing unimplemented.
72    pub features: FeatureSupport,
73}
74
75impl ServerConfig {
76    pub fn new(app_id: impl Into<String>, master_key: impl Into<String>) -> Self {
77        Self {
78            app_id: app_id.into(),
79            master_key: master_key.into(),
80            maintenance_key: None,
81            javascript_key: None,
82            rest_api_key: None,
83            client_key: None,
84            dot_net_key: None,
85            mount_path: "/parse".to_string(),
86            enable_sanitized_error_response: true,
87            has_push_support: false,
88            has_push_scheduled_support: false,
89            security_check_enabled: false,
90            features: FeatureSupport::default(),
91        }
92    }
93
94    pub fn javascript_key(mut self, k: impl Into<String>) -> Self {
95        self.javascript_key = Some(k.into());
96        self
97    }
98
99    pub fn rest_api_key(mut self, k: impl Into<String>) -> Self {
100        self.rest_api_key = Some(k.into());
101        self
102    }
103
104    pub fn mount_path(mut self, p: impl Into<String>) -> Self {
105        self.mount_path = p.into();
106        self
107    }
108
109    /// True when any client key is configured. Upstream's rule is all-or-nothing: if *any* of
110    /// these is set, a non-master request must present one that matches
111    /// (`middlewares.js:255-265`). If none is configured, none is required.
112    pub fn requires_client_key(&self) -> bool {
113        self.javascript_key.is_some()
114            || self.rest_api_key.is_some()
115            || self.client_key.is_some()
116            || self.dot_net_key.is_some()
117    }
118}