Add VS Code configuration and improve developer documentation

- Add VS Code launch configuration example and settings
- Create detailed BUILD.md with setup instructions
- Add keys.example.json for configuration reference
- Update .gitignore to exclude development files
- Remove unused InvitationActiveKeys functionality from config

+3
.gitignore
··· 39 39 localdev/calendars-data/ 40 40 41 41 **/.claude/settings.local.json 42 + CLAUDE.local.md 42 43 Cargo.lock 44 + /keys.json 45 + /.vscode/launch.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
··· 1 + { 2 + "files.associations": { 3 + "*.html": "jinja-html" 4 + } 5 + }
+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
··· 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
··· 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 - }