Skip to main content

solid_pod_rs_forge/
preview.rs

1//! `POST <prefix>/api/repos/<owner>/<repo>/preview`: the **unstaged** tier.
2//!
3//! Follows the JSS `forge` `apiRepoPreview` and `EPHEMERAL_PREVIEW_KIND`
4//! (cited by name only). The endpoint publishes one signed, **ephemeral**
5//! Nostr event (kind [`EPHEMERAL_PREVIEW_KIND`], 21617, inside the NIP-01
6//! 20000-29999 ephemeral range) that carries the file content, and makes no
7//! commit, no ref update and no anchor, and writes nothing to disk. Relays do
8//! not store ephemeral events, so only viewers connected at that moment see
9//! the preview.
10//!
11//! Authorisation, body rules, size caps and CORS are exactly those of
12//! [`crate::web_edit`]: owner-signed unless
13//! [`ForgeConfig::open_edit`](crate::ForgeConfig::open_edit) is on. The event
14//! is signed with the forge-instance announce key and sent through the same
15//! relay publisher and SSRF policy as the repository announcements; it needs
16//! the `announce` feature (without it the endpoint validates and then answers
17//! `501`).
18//!
19//! The event tags are `["d", "<owner>/<repo>"]`, `["f", "<path>"]` and
20//! `["branch", "<branch>"]`; the content is the file content as sent.
21//!
22//! # Examples
23//!
24//! ```
25//! assert_eq!(solid_pod_rs_forge::preview::EPHEMERAL_PREVIEW_KIND, 21617);
26//! ```
27
28use crate::ownership::ForgeAgent;
29use crate::request::{ForgeRequest, ForgeResponse};
30use crate::web_edit::{cors_err, resolve_branch, validate};
31use crate::ForgeService;
32
33/// The NIP-01 ephemeral event kind used for previews (JSS
34/// `EPHEMERAL_PREVIEW_KIND`).
35pub const EPHEMERAL_PREVIEW_KIND: u64 = 21617;
36
37impl ForgeService {
38    /// `POST api/repos/<o>/<n>/preview` (JSS `apiRepoPreview`). Never returns
39    /// an error: every outcome is a CORS-readable JSON reply.
40    pub(crate) async fn h_api_preview(
41        &self,
42        req: &ForgeRequest,
43        agent: &ForgeAgent,
44        owner: &str,
45        name: &str,
46    ) -> ForgeResponse {
47        if !req.method.eq_ignore_ascii_case("POST") {
48            return crate::api::api_err(405, "method not allowed");
49        }
50        let ed = match validate(self.cfg.open_edit, req, agent, owner, "preview") {
51            Ok(e) => e,
52            Err(resp) => return resp,
53        };
54        let dir = match self.resolve_repo(owner, name).await {
55            Ok(d) => d,
56            Err(_) => return cors_err(404, "not found"),
57        };
58        let branch = match resolve_branch(&dir, ed.branch.as_ref()).await {
59            Ok(b) => b,
60            Err(resp) => return resp,
61        };
62        self.publish_preview(owner, name, &branch, &ed).await
63    }
64
65    #[cfg(feature = "announce")]
66    async fn publish_preview(
67        &self,
68        owner: &str,
69        name: &str,
70        branch: &str,
71        ed: &crate::web_edit::EditRequest,
72    ) -> ForgeResponse {
73        use crate::publish::{publish_event, RELAY_TIMEOUT};
74        use crate::web_edit::cors_json;
75        let a = &self.announcer;
76        if a.relays.is_empty() {
77            return cors_json(
78                200,
79                &serde_json::json!({
80                    "ok": true,
81                    "published": false,
82                    "reason": "no relays configured (set config.announceRelays)",
83                }),
84            );
85        }
86        let tags = vec![
87            vec!["d".to_string(), format!("{owner}/{name}")],
88            vec!["f".to_string(), ed.path.clone()],
89            vec!["branch".to_string(), branch.to_string()],
90        ];
91        let ev = match a
92            .signer
93            .sign(EPHEMERAL_PREVIEW_KIND, tags, &ed.content, crate::now_secs())
94        {
95            Ok(e) => e,
96            Err(e) => {
97                tracing::warn!("forge: preview {owner}/{name} signing failed: {e}");
98                return cors_err(500, "preview failed");
99            }
100        };
101        let relays = publish_event(&ev, &a.relays, RELAY_TIMEOUT, a.ssrf.as_ref()).await;
102        cors_json(
103            200,
104            &serde_json::json!({
105                "ok": true,
106                "published": true,
107                "kind": ev.kind,
108                "id": ev.id,
109                "path": ed.path,
110                "relays": serde_json::to_value(&relays).unwrap_or(serde_json::Value::Null),
111            }),
112        )
113    }
114
115    #[cfg(not(feature = "announce"))]
116    async fn publish_preview(
117        &self,
118        _owner: &str,
119        _name: &str,
120        _branch: &str,
121        _ed: &crate::web_edit::EditRequest,
122    ) -> ForgeResponse {
123        cors_err(501, "NIP-34 emission is not compiled into this build")
124    }
125}