+3
.gitignore
+3
.gitignore
+33
.vscode/launch.example.json
+33
.vscode/launch.example.json
···
1
+
{
2
+
"version": "0.2.0",
3
+
"configurations": [
4
+
{
5
+
"type": "lldb",
6
+
"request": "launch",
7
+
"name": "Debug executable 'smokesignal'",
8
+
"cargo": {
9
+
"args": [
10
+
"build",
11
+
"--bin=smokesignal"
12
+
],
13
+
"filter": {
14
+
"name": "smokesignal",
15
+
"kind": "bin"
16
+
}
17
+
},
18
+
"args": [],
19
+
"cwd": "${workspaceFolder}",
20
+
"env": {
21
+
"DEBUG": "true",
22
+
"HTTP_PORT": "3100",
23
+
"EXTERNAL_BASE": "your-hostname",
24
+
"HTTP_COOKIE_KEY": "Iteax8DsUgOrQJdES+zMa6JKYlbQkewl42Y1bO1ExSyB9jkUktrdKwwWSu+X58T20liLmsegL3LbQB0FvE1AEA",
25
+
"DATABASE_URL": "postgres://postgres:password@postgres/smokesignal",
26
+
"OAUTH_ACTIVE_KEYS": "01JV2SGY5K5KW5V4YS0YQ352FD",
27
+
"DESTINATION_KEY": "01JV2SGY5K5KW5V4YS0YQ352FD",
28
+
"SIGNING_KEYS": "/workspace/keys.json",
29
+
"RUST_LOG": "smokesignal=debug,html5ever=info,info"
30
+
}
31
+
}
32
+
]
33
+
}
+5
.vscode/settings.json
+5
.vscode/settings.json
+120
BUILD.md
+120
BUILD.md
···
1
+
# Build
2
+
3
+
This project uses the stable Rust toolchain (1.86 as of 5/12/25).
4
+
5
+
## Bare Metal
6
+
7
+
If you're not using devcontainers, you'll need to install Rust and the necessary dependencies on your system.
8
+
9
+
### Prerequisites
10
+
11
+
- Rust toolchain (1.86 or newer)
12
+
- PostgreSQL
13
+
- Redis or Valkey
14
+
- SQLx CLI: `cargo install sqlx-cli@0.8.3 --no-default-features --features postgres`
15
+
16
+
### Common Commands
17
+
18
+
- Build: `cargo build`
19
+
- Check: `cargo check`
20
+
- Lint: `cargo clippy`
21
+
- Run tests: `cargo test`
22
+
- Run server: `cargo run --bin smokesignal`
23
+
- Run with debug: `RUST_BACKTRACE=1 RUST_LOG=debug cargo run`
24
+
- Run database migrations: `sqlx migrate run`
25
+
26
+
### Build Options
27
+
28
+
- Build with embedded templates: `cargo build --bin smokesignal --no-default-features -F embed`
29
+
- Build with template reloading: `cargo build --bin smokesignal --no-default-features -F reload`
30
+
31
+
## Devcontainers (Recommended)
32
+
33
+
The easiest way to get started is by using the provided devcontainer configuration with Visual Studio Code.
34
+
35
+
### Setup
36
+
37
+
1. Install [Visual Studio Code](https://code.visualstudio.com/)
38
+
2. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
39
+
3. Clone this repository
40
+
4. Open the repository in VS Code
41
+
5. When prompted, click "Reopen in Container" or run the "Dev Containers: Reopen in Container" command
42
+
43
+
The devcontainer will set up the following services:
44
+
- Rust development environment with all dependencies
45
+
- PostgreSQL database
46
+
- Valkey (Redis-compatible) key-value store
47
+
- Tailscale networking (optional)
48
+
49
+
### Disabling Tailscale
50
+
51
+
If you don't need the Tailscale service, you can disable it by:
52
+
53
+
1. Open `.devcontainer/docker-compose.yml`
54
+
2. Comment out or remove the `tailscale` service section
55
+
3. Rebuild the devcontainer (Command Palette > "Dev Containers: Rebuild Container")
56
+
57
+
### VS Code Configuration
58
+
59
+
The devcontainer comes with recommended VS Code extensions for Rust development:
60
+
- Rust Analyzer
61
+
- Jinja HTML
62
+
- Even Better TOML
63
+
64
+
A launch configuration example is provided in `.vscode/launch.example.json`. Copy this to `.vscode/launch.json` to enable debugging in VS Code.
65
+
66
+
## Development Configuration
67
+
68
+
The application requires several environment variables for cryptographic operations. You can generate appropriate values using the included `crypto` binary.
69
+
70
+
### Generating Cryptographic Keys
71
+
72
+
Generate a random 64-byte key encoded in base64:
73
+
74
+
```
75
+
cargo run --bin crypto -- key
76
+
```
77
+
78
+
Generate a JWK (JSON Web Key):
79
+
80
+
```
81
+
cargo run --bin crypto -- jwk
82
+
```
83
+
84
+
The generated JWK should be added to a JWKS (JSON Web Key Set) in the file `keys.json`:
85
+
86
+
```json
87
+
{
88
+
"keys": [
89
+
{ "kid": "01J7PM272ZF0DYZAPR3499VBTM" ...},
90
+
{ "kid": "01J8G3J3CDVJ15C63PMCDS3K97" ...},
91
+
{ "kid": "01JF2QS2S86SG2R23HTZ0JKB76" ...}
92
+
]
93
+
}
94
+
```
95
+
96
+
### Environment Variables
97
+
98
+
Set the following environment variables with values generated from the commands above:
99
+
100
+
- `SIGNING_KEYS`: The path to the `keys.json` file
101
+
- `OAUTH_ACTIVE_KEYS`: A comma seperated list of JWK IDs used to actively sign OAuth sessions
102
+
- `DESTINATION_KEY`: A JWK ID used to sign destination (used in redirects) values
103
+
- `HTTP_COOKIE_KEY`: A key used to encrypt HTTP sessions
104
+
105
+
You can add these to your .env file or set them directly in your environment.
106
+
107
+
### Additional Configuration for Airgapped Development
108
+
109
+
For airgapped development, you can configure:
110
+
111
+
- `PLC_HOSTNAME`: Custom PLC hostname for development
112
+
- `DNS_NAMESERVERS`: Custom DNS nameservers
113
+
- `ADMIN_DIDS`: Comma-separated list of admin DIDs
114
+
115
+
Example:
116
+
```
117
+
PLC_HOSTNAME=localhost:3000
118
+
DNS_NAMESERVERS=1.1.1.1,1.0.0.1
119
+
ADMIN_DIDS=did:plc:yourdevdid1,did:plc:yourdevdid2
120
+
```
+13
keys.example.json
+13
keys.example.json
···
1
+
{
2
+
"keys": [
3
+
{
4
+
"kid": "01JV2SGY5K5KW5V4YS0YQ352FD",
5
+
"alg": "ES256",
6
+
"kty": "EC",
7
+
"crv": "P-256",
8
+
"x": "okqcK-HmgQ84GP8GkyrvcMM72o-mWZnf2vYgUbI7p0s",
9
+
"y": "9QFqzeTesLf8n2VDs2JYzuMGjNBIch2hI7UwWMaequ8",
10
+
"d": "dgR4jSo6ANIyVXuh86B8IhFkFfBMhAtaaii9KPzlHfk"
11
+
}
12
+
]
13
+
}
-71
src/config.rs
-71
src/config.rs
···
24
24
pub struct OAuthActiveKeys(Vec<String>);
25
25
26
26
#[derive(Clone)]
27
-
pub struct InvitationActiveKeys(Vec<String>);
28
-
29
-
#[derive(Clone)]
30
27
pub struct AdminDIDs(Vec<String>);
31
28
32
29
#[derive(Clone)]
···
44
41
pub plc_hostname: String,
45
42
pub signing_keys: SigningKeys,
46
43
pub oauth_active_keys: OAuthActiveKeys,
47
-
pub invitation_active_keys: InvitationActiveKeys,
48
44
pub destination_key: SecretKey,
49
45
pub redis_url: String,
50
46
pub admin_dids: AdminDIDs,
···
78
74
let oauth_active_keys: OAuthActiveKeys =
79
75
require_env("OAUTH_ACTIVE_KEYS").and_then(|value| value.try_into())?;
80
76
81
-
let invitation_active_keys: InvitationActiveKeys =
82
-
require_env("INVITATION_ACTIVE_KEYS").and_then(|value| value.try_into())?;
83
-
84
77
let destination_key = require_env("DESTINATION_KEY").and_then(|value| {
85
78
signing_keys
86
79
.0
···
105
98
database_url,
106
99
signing_keys,
107
100
oauth_active_keys,
108
-
invitation_active_keys,
109
101
http_cookie_key,
110
102
destination_key,
111
103
redis_url,
···
316
308
}
317
309
}
318
310
319
-
impl AsRef<Vec<String>> for InvitationActiveKeys {
320
-
fn as_ref(&self) -> &Vec<String> {
321
-
&self.0
322
-
}
323
-
}
324
-
325
-
impl TryFrom<String> for InvitationActiveKeys {
326
-
type Error = anyhow::Error;
327
-
fn try_from(value: String) -> Result<Self, Self::Error> {
328
-
let values = value
329
-
.split(';')
330
-
.map(|s| s.to_string())
331
-
.collect::<Vec<String>>();
332
-
if values.is_empty() {
333
-
return Err(ConfigError::EmptyInvitationActiveKeys.into());
334
-
}
335
-
Ok(Self(values))
336
-
}
337
-
}
338
-
339
311
impl AsRef<Vec<String>> for AdminDIDs {
340
312
fn as_ref(&self) -> &Vec<String> {
341
313
&self.0
···
387
359
Ok(Self(nameservers))
388
360
}
389
361
}
390
-
391
-
// Default implementation for testing
392
-
#[cfg(test)]
393
-
impl Default for Config {
394
-
fn default() -> Self {
395
-
// Create a random key for testing
396
-
let cookie_key_data = [0u8; 64];
397
-
let http_cookie_key = HttpCookieKey(Key::from(&cookie_key_data));
398
-
399
-
// Create empty collections
400
-
let signing_keys = SigningKeys(OrderMap::new());
401
-
let oauth_active_keys = OAuthActiveKeys(Vec::new());
402
-
let invitation_active_keys = InvitationActiveKeys(Vec::new());
403
-
let certificate_bundles = CertificateBundles(Vec::new());
404
-
405
-
// Create a default admin DID for testing
406
-
let admin_dids = AdminDIDs(vec!["did:plc:testadmin".to_string()]);
407
-
408
-
// Create empty DNS nameservers list for testing
409
-
let dns_nameservers = DnsNameservers(Vec::new());
410
-
411
-
Self {
412
-
version: "test-version".to_string(),
413
-
http_port: HttpPort(8080),
414
-
http_cookie_key,
415
-
external_base: "https://test.example".to_string(),
416
-
certificate_bundles,
417
-
user_agent: "smokesignal-test".to_string(),
418
-
database_url: "sqlite://test.db".to_string(),
419
-
plc_hostname: "plc.test".to_string(),
420
-
signing_keys,
421
-
oauth_active_keys,
422
-
invitation_active_keys,
423
-
// For testing, this needs to be a valid P-256 key
424
-
// This would normally come from the signing keys, but for tests
425
-
// we'll create a dummy one - note that it won't actually be used.
426
-
destination_key: SecretKey::random(&mut rand::thread_rng()),
427
-
redis_url: "redis://localhost:6379".to_string(),
428
-
admin_dids,
429
-
dns_nameservers,
430
-
}
431
-
}
432
-
}