diff --git a/Cargo.lock b/Cargo.lock index ddd825a..ac4e5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1834,6 +1834,7 @@ version = "0.1.0" dependencies = [ "atproto", "axum", + "bon", "http 1.3.1", "serde", "serde_json", diff --git a/entryway/src/main.rs b/entryway/src/main.rs index d91e7bf..63c6fc1 100644 --- a/entryway/src/main.rs +++ b/entryway/src/main.rs @@ -31,6 +31,7 @@ async fn main() { let mut router = Router::new(); let create_account_nsid: Nsid = "com.atproto.server.createAccount".parse::().expect("valid nsid"); + router = router.add_endpoint(XrpcEndpoint::not_implemented()); router = router.add_endpoint(XrpcEndpoint::new_procedure(create_account_nsid, create_account)); router.serve().await; } diff --git a/flake.lock b/flake.lock index 69e701e..c3be24b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,18 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1735563628, - "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", - "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", - "revCount": 637546, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.637546%2Brev-b134951a4c9f3c995fd7be05f3243f8ecd65d798/01941dc2-2ab2-7453-8ebd-88712e28efae/source.tar.gz" + "lastModified": 1752436162, + "narHash": "sha256-Kt1UIPi7kZqkSc5HVj6UY5YLHHEzPBkgpNUByuyxtlw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "dfcd5b901dbab46c9c6e80b265648481aafb01f8", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2405.%2A.tar.gz" + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs_2": { @@ -41,11 +43,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1749695868, - "narHash": "sha256-debjTLOyqqsYOUuUGQsAHskFXH5+Kx2t3dOo/FCoNRA=", + "lastModified": 1752547600, + "narHash": "sha256-0vUE42ji4mcCvQO8CI0Oy8LmC6u2G4qpYldZbZ26MLc=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "55f914d5228b5c8120e9e0f9698ed5b7214d09cd", + "rev": "9127ca1f5a785b23a2fc1c74551a27d3e8b9a28b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 48d94bf..014140f 100644 --- a/flake.nix +++ b/flake.nix @@ -3,13 +3,15 @@ # Flake inputs inputs = { - nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2405.*.tar.gz"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; rust-overlay.url = "github:oxalica/rust-overlay"; # A helper for Rust + Nix }; # Flake outputs outputs = { self, nixpkgs, rust-overlay }: let + pdsDirectory = "/home/pan/prog/atproto/appview"; + # Overlays enable you to customize the Nixpkgs attribute set overlays = [ # Makes a `rust-bin` attribute available in Nixpkgs @@ -33,19 +35,88 @@ forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f { pkgs = import nixpkgs { inherit overlays system; }; }); + + # Systemd service configuration + createSystemdService = pkgs: pdsDir: pkgs.writeTextFile { + name = "pds.service"; + text = '' + [Unit] + Description=Development Environment Service + After=network-online.target + Wants=network-online.target + + [Service] + Type=simple + ExecStart=${pkgs.pds}/bin/pds + WorkingDirectory=${pdsDir} + EnvironmentFile=${pdsDir}/.env + Environment=PDS_DATA_DIRECTORY=${pdsDir}/.pds-data + Environment=PDS_BLOBSTORE_DISK_LOCATION=${pdsDir}/.pds-data/blocks + ''; + }; + + # Scripts for managing the systemd service + createServiceScripts = pkgs: pdsDir: + let + serviceFile = createSystemdService pkgs pdsDir; + serviceName = "pds"; + in { + startScript = pkgs.writeShellScript "start-dev-service" '' + set -e + + # Create user systemd directory if it doesn't exist + mkdir -p ~/.config/systemd/user + + # Copy service file + cp -f ${serviceFile} ~/.config/systemd/user/${serviceName}.service + + # Reload systemd and start service + systemctl --user daemon-reload + systemctl --user start ${serviceName} + systemctl --user enable ${serviceName} + + systemctl --user status ${serviceName} --no-pager + ''; + + stopScript = pkgs.writeShellScript "stop-dev-service" '' + set -e + if systemctl --user is-enabled --quiet ${serviceName}; then + # Stop and disable service + systemctl --user stop ${serviceName} || true + systemctl --user disable ${serviceName} || true + + # Remove service file + rm -f ~/.config/systemd/user/${serviceName}.service + + # Reload systemd + systemctl --user daemon-reload + fi + ''; + }; in { # Development environment output - devShells = forAllSystems ({ pkgs }: { - default = pkgs.mkShell { - # The Nix packages provided in the environment - packages = (with pkgs; [ - # The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt, - # rustdoc, rustfmt, and other tools. - sqlx-cli - rustToolchain - ]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]); - }; - }); + devShells = forAllSystems ({ pkgs }: + let + scripts = createServiceScripts pkgs pdsDirectory; + in { + default = pkgs.mkShell { + # The Nix packages provided in the environment + packages = (with pkgs; [ + # The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt, + # rustdoc, rustfmt, and other tools. + sqlx-cli + rustToolchain + ]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]); + + shellHook = pkgs.lib.optionalString pkgs.stdenv.isLinux '' + # Cleanup + ${scripts.stopScript} + + # Start the systemd service + ${scripts.startScript} + ''; + }; + }); }; } diff --git a/router/Cargo.toml b/router/Cargo.toml index 1a09bd8..2426dd6 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] atproto.workspace = true axum = { version = "0.8.3", features = ["json"] } +bon = "3.6.4" http = "1.3.1" serde.workspace = true serde_json.workspace = true diff --git a/router/src/lib.rs b/router/src/lib.rs index 1a3b212..bddad49 100644 --- a/router/src/lib.rs +++ b/router/src/lib.rs @@ -20,9 +20,7 @@ impl Default for Router { } impl Router { pub fn new() -> Self { - let mut router = AxumRouter::new(); - // TODO: Only add if there is at least one XRPC endpoint - router = XrpcEndpoint::not_implemented().add_to_router(router); + let router = AxumRouter::new(); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127,0,0,1)), 6702); Router { router, addr } } diff --git a/router/src/wellknown/oauth/authorization_server.rs b/router/src/wellknown/oauth/authorization_server.rs index 139597f..d2ae12f 100644 --- a/router/src/wellknown/oauth/authorization_server.rs +++ b/router/src/wellknown/oauth/authorization_server.rs @@ -1,2 +1,103 @@ +use serde::de::{ + Error, + Unexpected, +}; +use serde_json::{ + json, + Result, + Value +}; +use bon::Builder; +trait Metadata { + fn format_metadata(self, required: RequiredMetadata) -> Result; +} +pub struct RequiredMetadata { + issuer: String, + authorization_endpoint: String, + token_endpoint: String, +} + +impl RequiredMetadata { + fn new( + issuer: String, + authorization_endpoint: String, + token_endpoint: String + ) -> Self { + RequiredMetadata { + issuer, authorization_endpoint, token_endpoint + } + } +} + +#[derive(Builder)] +struct AtprotoMetadata { + additional_response_types_supported: Option>, + additional_grant_types_supported: Option>, + additional_code_challenge_methods_supported: Option>, + additional_token_endpoint_auth_methods_supported: Option>, + additional_token_endpoint_auth_signing_alg_values_supported: Option>, + additional_scopes_supported: Option>, + pushed_authorization_request_endpoint: String, + additional_dpop_signing_alg_values_supported: Option>, +} + +impl AtprotoMetadata { + fn check_fields(&self) -> Result<()> { + // TODO: Issuer check (https scheme, no default port, no path segments + + if self.additional_token_endpoint_auth_signing_alg_values_supported + .as_ref() + .is_none_or(|vec| vec.iter().any(|s| s == "none")) { + return Err(Error::invalid_value( + Unexpected::Other("\"none\" in token_endpoint_auth_signing_alg_values_supported"), + &"\"none\" to be omitted from token_endpoint_auth_signing_alg_values_supported" + )); + } + + Ok(()) + } +} + +impl Metadata for AtprotoMetadata { + fn format_metadata(self, required: RequiredMetadata) -> Result { + self.check_fields()?; + Ok(json!({ + "issuer": required.issuer, + "authorization_endpoint": required.authorization_endpoint, + "token_endpoint": required.token_endpoint, + "response_types_supported": + self.additional_response_types_supported.unwrap_or_default() + .extend(["code".to_string()]), + "grant_types_supported": + self.additional_grant_types_supported.unwrap_or_default() + .extend([ + "authorization_code".to_string(), + "refresh_token".to_string() + ]), + "code_challenge_methods_supported": + self.additional_code_challenge_methods_supported.unwrap_or_default() + .extend(["S256".to_string()]), + "token_endpoint_auth_methods_supported": + self.additional_token_endpoint_auth_methods_supported.unwrap_or_default() + .extend([ + "none".to_string(), + "private_key_jwt".to_string() + ]), + "token_endpoint_auth_signing_alg_values_supported": + self.additional_token_endpoint_auth_signing_alg_values_supported.unwrap_or_default() + .extend(["ES256".to_string()]), + "scopes_supported": + self.additional_scopes_supported.unwrap_or_default() + .extend(["atproto".to_string()]), + "authorization_response_iss_parameter_supported": true, + "require_pushed_authorization_requests": true, + "pushed_authorization_request_endpoint": self.pushed_authorization_request_endpoint, + "dpop_signing_alg_values_supported": + self.additional_dpop_signing_alg_values_supported.unwrap_or_default() + .extend(["ES256".to_string()]), + "client_id_metadata_document_supported": true, + })) + } +}