{ depot, config, pkgs, lib, ... }: let cfg = config.my.services.bsky-pds; marshalValue = key: value: if builtins.isString value then value else if builtins.isInt value then toString value else if builtins.isBool value then (if value then "true" else "false") else throw "unknown value type for key ${key}"; cfgFile = pkgs.writeTextFile { name = "bsky-pds.env"; text = lib.concatStringsSep "\n" (lib.mapAttrsToList (attrName: attrVal: lib.optionalString (attrVal != null) '' ${lib.toUpper attrName}=${marshalValue attrName attrVal} '') cfg.settings); }; inSecretsDir = v: builtins.isString v && (builtins.match "/var/lib/bsky-pds/secrets/.*" (toString v)) != null; testAndGenerate = how: path: '' test -f "${path}" || { ${how} >> "${path}" } ''; testAndGenerateHex = testAndGenerate "${pkgs.openssl}/bin/openssl rand --hex 16"; testAndGenerateK256 = testAndGenerate "${pkgs.openssl}/bin/openssl ecparam --name secp256k1 --genkey --noout --outform DER"; in { options.my.services.bsky-pds = { enable = lib.mkEnableOption "Bluesky Personal Data Server"; package = lib.mkOption { type = lib.types.package; default = depot.nix.pkgs.bsky-pds; description = '' Package containing the PDS code. ''; }; configureCaddy = (lib.mkEnableOption "configure Caddy to serve the PDS") // { default = true; }; settings = lib.mkOption { type = with lib.types; submodule { freeformType = attrsOf (nullOr (oneOf [ str bool int ])); options.log_enabled = lib.mkOption { type = lib.types.bool; default = true; description = '' Whether or not to output any log messages. ''; }; options.port = lib.mkOption { type = lib.types.port; default = 3000; description = '' Port number that the PDS will listen on. ''; }; options.pds_hostname = lib.mkOption { type = lib.types.str; description = '' Hostname under which the PDS will run. ''; }; options.pds_admin_email = lib.mkOption { type = lib.types.str; description = '' Email address of the PDS operator. ''; }; options.pds_data_directory = lib.mkOption { type = lib.types.path; default = "/var/lib/bsky-pds/data"; description = '' Path to data directory. ''; }; options.pds_blobstore_disk_location = lib.mkOption { type = lib.types.nullOr lib.types.path; default = "/var/lib/bsky-pds/blobs"; description = '' Path to location for blobstore storage, if using on-disk storage. ''; }; options.pds_blob_upload_limit = lib.mkOption { type = lib.types.int; default = 52428800; description = '' Maximum allowable blob size for upload. ''; }; options.pds_did_plc_url = lib.mkOption { type = lib.types.str; default = "https://plc.directory"; description = '' URL of the PLC directory. ''; }; options.pds_bsky_app_view_url = lib.mkOption { type = lib.types.str; default = "https://api.bsky.app"; description = '' URL of the Bluesky AppView. ''; }; options.pds_bsky_app_view_did = lib.mkOption { type = lib.types.str; default = "did:web:api.bsky.app"; description = '' DID of the Bluesky AppView. ''; }; options.pds_report_service_url = lib.mkOption { type = lib.types.str; default = "https://mod.bsky.app"; description = '' URL of the Bluesky moderation service. ''; }; options.pds_report_service_did = lib.mkOption { type = lib.types.str; default = "did:plc:ar7c4by46qjdydhdevvrndac"; description = '' DID of the Bluesky moderation service. ''; }; options.crawlers = lib.mkOption { type = lib.types.commas; default = "https://bsky.network"; description = '' URLs of hosts to notify of new data. ''; }; }; default = {}; description = '' Configuration for Bluesky PDS. ''; }; generateSecrets = lib.mkEnableOption "automatically generate required PDS secrets"; secrets = lib.mkOption { type = with lib.types; submodule { freeformType = attrsOf (nullOr path); options.pds_jwt_secret = lib.mkOption { type = nullOr path; default = null; description = '' Path to a file containing a 16 character hex secret used for JWT secrets. ''; }; options.pds_admin_password = lib.mkOption { type = nullOr path; default = null; description = '' Path to a file containing the PDS admin password. ''; }; options.pds_plc_rotation_key_k256_private_key = lib.mkOption { type = nullOr path; default = null; description = '' Path to a file containing a secp256k1 private key in DER format. ''; }; }; default = {}; description = '' Path to files containing secrets for Bluesky PDS. ''; }; }; config = lib.mkMerge [(lib.mkIf cfg.generateSecrets { my.services.bsky-pds.secrets = { pds_jwt_secret = lib.mkDefault "/var/lib/bsky-pds/secrets/pds_jwt_secret"; pds_admin_password = lib.mkDefault "/var/lib/bsky-pds/secrets/pds_admin_password"; pds_plc_rotation_key_k256_private_key = lib.mkDefault "/var/lib/bsky-pds/secrets/pds_plc_rotation_key_k256_private_key"; }; }) (lib.mkIf cfg.enable { systemd.services.bsky-pds = { wantedBy = [ "multi-user.target" ]; unitConfig = { Description = "Bluesky PDS Service"; Documentation = "https://github.com/bluesky-social/pds"; }; script = '' mkdir -p /var/lib/bsky-pds "$PDS_DATA_DIRECTORY" if [[ ! -z "$PDS_BLOBSTORE_DISK_LOCATION" ]]; then mkdir -p "$PDS_BLOBSTORE_DISK_LOCATION" fi old_umask=$(umask) umask 077 mkdir -p /var/lib/bsky-pds/secrets ${lib.optionalString (inSecretsDir cfg.secrets.pds_jwt_secret) (testAndGenerateHex cfg.secrets.pds_jwt_secret)} ${lib.optionalString (inSecretsDir cfg.secrets.pds_admin_password) (testAndGenerateHex cfg.secrets.pds_admin_password)} ${lib.optionalString (inSecretsDir cfg.secrets.pds_plc_rotation_key_k256_private_key) (testAndGenerateK256 cfg.secrets.pds_plc_rotation_key_k256_private_key)} umask "$old_umask" ${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: lib.optionalString (v != null) '' export ${lib.toUpper k}="$(cat "${v}")" '') cfg.secrets)} ${lib.optionalString (cfg.secrets.pds_plc_rotation_key_k256_private_key != null) '' if [[ -z "$PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX ]]; then export PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="$(cat "${cfg.secrets.pds_plc_rotation_key_k256_private_key}" | ${pkgs.coreutils}/bin/tail --bytes=+8 | ${pkgs.coreutils}/bin/head --bytes=32 | ${pkgs.xxd}/bin/xxd --plain --cols 32)" fi ''} exec ${cfg.package}/bin/bsky-pds ''; serviceConfig = { User = "bsky-pds"; DynamicUser = true; StateDirectory = "bsky-pds"; EnvironmentFile = cfgFile; }; }; }) (lib.mkIf (cfg.enable && cfg.configureCaddy) { services.caddy = { enable = lib.mkDefault true; globalConfig = '' email ${cfg.settings.pds_admin_email} on_demand_tls { ask http://localhost:${toString cfg.settings.port}/tls-check } ''; virtualHosts."${cfg.settings.pds_hostname}" = { serverAliases = [ "*.${cfg.settings.pds_hostname}" ]; extraConfig = '' tls { on_demand } reverse_proxy http://localhost:${toString cfg.settings.port} ''; }; }; })]; }