Skip to main content

nostr_bbs_setup_skill/
lib.rs

1//! Provider-abstracted operator-onboarding skill for nostr-bbs deployments.
2//!
3//! Implements [ADR-079]: a single skill that walks an operator from
4//! `git clone` to "running forum" across five custody tiers / hosting
5//! providers. The skill emits a populated `forum.toml`, provisions the
6//! upstream resources (D1, KV, R2, Routes, Domains), and writes back the
7//! per-worker `wrangler.toml` overlay.
8//!
9//! # Status
10//!
11//! Sprint v9-v11: scaffold only. Each [`Provider`] impl returns
12//! [`SetupError::NotYetImplemented`] for unfinished methods; full
13//! implementation lands in Sprint v12+ per the PRD-012 Phase X3 plan.
14//!
15//! # Provider matrix (per ADR-079 §4)
16//!
17//! | Tier   | Provider                      | Custody             |
18//! |--------|-------------------------------|---------------------|
19//! | tier-1 | [`SelfHostProvider`]          | Operator-managed VM |
20//! | tier-2 | [`CloudflareWorkersProvider`] | CF Workers Secrets  |
21//! | tier-3 | [`FlyDotIoProvider`]          | Fly.io Secrets      |
22//! | tier-4 | [`TurnkeyProvider`]           | Hosted (this kit)   |
23//! | tier-x | [`KubernetesProvider`]        | K8s Secret resource |
24//!
25//! [ADR-079]: https://github.com/DreamLab-AI/nostr-rust-forum/blob/main/docs/adr/ADR-079.md
26
27#![warn(missing_docs)]
28
29use async_trait::async_trait;
30use thiserror::Error;
31
32use nostr_bbs_config::ForumConfig;
33
34/// Errors raised by setup providers.
35#[derive(Debug, Error)]
36pub enum SetupError {
37    /// Provider-specific API error.
38    #[error("provider: {0}")]
39    Provider(String),
40    /// Configuration validation error.
41    #[error("config: {0}")]
42    Config(String),
43    /// I/O error.
44    #[error("io: {0}")]
45    Io(String),
46    /// Operation not supported by this provider.
47    #[error("unsupported")]
48    Unsupported,
49    /// Provider method is scaffolded but not yet implemented.
50    #[error("{provider}::{method} not yet implemented — scheduled for Sprint v12+")]
51    NotYetImplemented {
52        /// Provider name (e.g. `"CloudflareWorkersProvider"`).
53        provider: &'static str,
54        /// Method name (e.g. `"provision"`).
55        method: &'static str,
56    },
57}
58
59/// One-shot record describing a provisioned resource.
60#[derive(Debug, Clone)]
61pub struct ProvisionedResource {
62    /// Logical resource type (`"d1"`, `"kv"`, `"r2"`, `"route"`, ...).
63    pub kind: String,
64    /// Provider-assigned resource identifier.
65    pub id: String,
66    /// Display name (e.g. `"nostr-bbs-auth"` for D1).
67    pub name: String,
68}
69
70/// Abstract setup provider — one impl per custody tier.
71#[async_trait(?Send)]
72pub trait Provider {
73    /// Provider tier identifier (`"tier-1"` .. `"tier-4"` or custom).
74    fn tier(&self) -> &'static str;
75
76    /// Provision deployment resources defined by `cfg`.
77    async fn provision(&self, cfg: &ForumConfig) -> Result<Vec<ProvisionedResource>, SetupError>;
78
79    /// Render the per-worker `wrangler.toml` overlay (or equivalent for the
80    /// provider) given a populated `cfg` and the resources from `provision`.
81    async fn render_wrangler(
82        &self,
83        cfg: &ForumConfig,
84        resources: &[ProvisionedResource],
85    ) -> Result<String, SetupError>;
86}
87
88/// Self-hosted (operator-managed VM) provider stub.
89pub struct SelfHostProvider;
90
91#[async_trait(?Send)]
92impl Provider for SelfHostProvider {
93    fn tier(&self) -> &'static str {
94        "tier-1"
95    }
96
97    async fn provision(&self, _cfg: &ForumConfig) -> Result<Vec<ProvisionedResource>, SetupError> {
98        // Provisions: nothing (operator already runs the host).
99        // Returns an empty vec; render_wrangler still writes a manifest.
100        Ok(Vec::new())
101    }
102
103    async fn render_wrangler(
104        &self,
105        _cfg: &ForumConfig,
106        _resources: &[ProvisionedResource],
107    ) -> Result<String, SetupError> {
108        // Self-host emits a docker-compose.yml or systemd unit instead of
109        // a wrangler manifest. Implementation: Sprint v12.
110        Err(SetupError::NotYetImplemented {
111            provider: "SelfHostProvider",
112            method: "render_wrangler",
113        })
114    }
115}
116
117/// Cloudflare Workers (default tier-2) provider stub.
118pub struct CloudflareWorkersProvider;
119
120#[async_trait(?Send)]
121impl Provider for CloudflareWorkersProvider {
122    fn tier(&self) -> &'static str {
123        "tier-2"
124    }
125
126    async fn provision(&self, _cfg: &ForumConfig) -> Result<Vec<ProvisionedResource>, SetupError> {
127        // Provisions: D1 db, KV namespaces (admin + nip98-replay + admin-ro),
128        // R2 bucket, Routes, Custom Domain. Implementation: Sprint v12+.
129        Err(SetupError::NotYetImplemented {
130            provider: "CloudflareWorkersProvider",
131            method: "provision",
132        })
133    }
134
135    async fn render_wrangler(
136        &self,
137        _cfg: &ForumConfig,
138        _resources: &[ProvisionedResource],
139    ) -> Result<String, SetupError> {
140        Err(SetupError::NotYetImplemented {
141            provider: "CloudflareWorkersProvider",
142            method: "render_wrangler",
143        })
144    }
145}
146
147/// Fly.io (tier-3) provider stub.
148pub struct FlyDotIoProvider;
149
150#[async_trait(?Send)]
151impl Provider for FlyDotIoProvider {
152    fn tier(&self) -> &'static str {
153        "tier-3"
154    }
155
156    async fn provision(&self, _cfg: &ForumConfig) -> Result<Vec<ProvisionedResource>, SetupError> {
157        Err(SetupError::NotYetImplemented {
158            provider: "FlyDotIoProvider",
159            method: "provision",
160        })
161    }
162
163    async fn render_wrangler(
164        &self,
165        _cfg: &ForumConfig,
166        _resources: &[ProvisionedResource],
167    ) -> Result<String, SetupError> {
168        Err(SetupError::NotYetImplemented {
169            provider: "FlyDotIoProvider",
170            method: "render_wrangler",
171        })
172    }
173}
174
175/// Turnkey hosted (tier-4) provider stub.
176pub struct TurnkeyProvider;
177
178#[async_trait(?Send)]
179impl Provider for TurnkeyProvider {
180    fn tier(&self) -> &'static str {
181        "tier-4"
182    }
183
184    async fn provision(&self, _cfg: &ForumConfig) -> Result<Vec<ProvisionedResource>, SetupError> {
185        Err(SetupError::NotYetImplemented {
186            provider: "TurnkeyProvider",
187            method: "provision",
188        })
189    }
190
191    async fn render_wrangler(
192        &self,
193        _cfg: &ForumConfig,
194        _resources: &[ProvisionedResource],
195    ) -> Result<String, SetupError> {
196        // Turnkey deploy never writes wrangler.toml on the operator's
197        // machine; the kit operator manages it. Return Unsupported.
198        Err(SetupError::Unsupported)
199    }
200}
201
202/// Kubernetes (tier-x) provider stub.
203pub struct KubernetesProvider;
204
205#[async_trait(?Send)]
206impl Provider for KubernetesProvider {
207    fn tier(&self) -> &'static str {
208        "tier-x"
209    }
210
211    async fn provision(&self, _cfg: &ForumConfig) -> Result<Vec<ProvisionedResource>, SetupError> {
212        Err(SetupError::NotYetImplemented {
213            provider: "KubernetesProvider",
214            method: "provision",
215        })
216    }
217
218    async fn render_wrangler(
219        &self,
220        _cfg: &ForumConfig,
221        _resources: &[ProvisionedResource],
222    ) -> Result<String, SetupError> {
223        Err(SetupError::NotYetImplemented {
224            provider: "KubernetesProvider",
225            method: "render_wrangler",
226        })
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn provider_tiers_match_taxonomy() {
236        assert_eq!(SelfHostProvider.tier(), "tier-1");
237        assert_eq!(CloudflareWorkersProvider.tier(), "tier-2");
238        assert_eq!(FlyDotIoProvider.tier(), "tier-3");
239        assert_eq!(TurnkeyProvider.tier(), "tier-4");
240        assert_eq!(KubernetesProvider.tier(), "tier-x");
241    }
242}