Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

feat: nix module

+803 -6
+21 -6
flake.nix
··· 7 7 # for now we important that PR as well purely for its fetchDenoDeps 8 8 nixpkgs-fetch-deno.url = "github:aMOPel/nixpkgs/feat/fetchDenoDeps"; 9 9 }; 10 - 11 - outputs = { self, nixpkgs, ... } @ inputs : let 12 - forAllSystems = 13 - function: 10 + 11 + outputs = { 12 + self, 13 + nixpkgs, 14 + ... 15 + } @ inputs: let 16 + forAllSystems = function: 14 17 nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( 15 18 system: (function system nixpkgs.legacyPackages.${system}) 16 19 ); 17 20 in { 18 21 packages = forAllSystems (system: pkgs: { 19 - tranquil-pds = pkgs.callPackage ./default.nix { }; 22 + tranquil-pds = pkgs.callPackage ./default.nix {}; 20 23 tranquil-frontend = pkgs.callPackage ./frontend.nix { 21 24 inherit (inputs.nixpkgs-fetch-deno.legacyPackages.${system}) fetchDenoDeps; 22 25 }; ··· 24 27 }); 25 28 26 29 devShells = forAllSystems (system: pkgs: { 27 - default = pkgs.callPackage ./shell.nix { }; 30 + default = pkgs.callPackage ./shell.nix {}; 28 31 }); 32 + 33 + nixosModules.default = import ./module.nix; 34 + 35 + checks.x86_64-linux.integration = import ./test.nix { 36 + pkgs = nixpkgs.legacyPackages.x86_64-linux; 37 + inherit self; 38 + }; 39 + 40 + checks.aarch64-linux.integration = import ./test.nix { 41 + pkgs = nixpkgs.legacyPackages.aarch64-linux; 42 + inherit self; 43 + }; 29 44 }; 30 45 }
+707
module.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: let 7 + cfg = config.services.tranquil-pds; 8 + 9 + optionalStr = lib.types.nullOr lib.types.str; 10 + optionalInt = lib.types.nullOr lib.types.int; 11 + optionalBool = lib.types.nullOr lib.types.bool; 12 + optionalPath = lib.types.nullOr lib.types.str; 13 + optionalPort = lib.types.nullOr lib.types.port; 14 + 15 + filterNulls = lib.filterAttrs (_: v: v != null); 16 + 17 + boolToStr = b: 18 + if b == true 19 + then "true" 20 + else if b == false 21 + then "false" 22 + else null; 23 + 24 + settingsToEnv = settings: let 25 + raw = { 26 + SERVER_HOST = settings.server.host; 27 + SERVER_PORT = settings.server.port; 28 + PDS_HOSTNAME = settings.server.pdsHostname; 29 + 30 + DATABASE_URL = settings.database.url; 31 + DATABASE_MAX_CONNECTIONS = settings.database.maxConnections; 32 + DATABASE_MIN_CONNECTIONS = settings.database.minConnections; 33 + DATABASE_ACQUIRE_TIMEOUT_SECS = settings.database.acquireTimeoutSecs; 34 + 35 + BLOB_STORAGE_BACKEND = settings.storage.blobBackend; 36 + BLOB_STORAGE_PATH = settings.storage.blobPath; 37 + S3_ENDPOINT = settings.storage.s3Endpoint; 38 + AWS_REGION = settings.storage.awsRegion; 39 + S3_BUCKET = settings.storage.s3Bucket; 40 + 41 + BACKUP_ENABLED = boolToStr settings.backup.enabled; 42 + BACKUP_STORAGE_BACKEND = settings.backup.backend; 43 + BACKUP_STORAGE_PATH = settings.backup.path; 44 + BACKUP_S3_BUCKET = settings.backup.s3Bucket; 45 + BACKUP_RETENTION_COUNT = settings.backup.retentionCount; 46 + BACKUP_INTERVAL_SECS = settings.backup.intervalSecs; 47 + 48 + VALKEY_URL = settings.cache.valkeyUrl; 49 + 50 + TRANQUIL_PDS_ALLOW_INSECURE_SECRETS = boolToStr settings.security.allowInsecureSecrets; 51 + 52 + PLC_DIRECTORY_URL = settings.plc.directoryUrl; 53 + PLC_TIMEOUT_SECS = settings.plc.timeoutSecs; 54 + PLC_CONNECT_TIMEOUT_SECS = settings.plc.connectTimeoutSecs; 55 + PLC_ROTATION_KEY = settings.plc.rotationKey; 56 + 57 + DID_CACHE_TTL_SECS = settings.did.cacheTtlSecs; 58 + 59 + CRAWLERS = settings.relay.crawlers; 60 + 61 + FIREHOSE_BUFFER_SIZE = settings.firehose.bufferSize; 62 + FIREHOSE_MAX_LAG = settings.firehose.maxLag; 63 + 64 + NOTIFICATION_BATCH_SIZE = settings.notifications.batchSize; 65 + NOTIFICATION_POLL_INTERVAL_MS = settings.notifications.pollIntervalMs; 66 + MAIL_FROM_ADDRESS = settings.notifications.mailFromAddress; 67 + MAIL_FROM_NAME = settings.notifications.mailFromName; 68 + SENDMAIL_PATH = settings.notifications.sendmailPath; 69 + SIGNAL_CLI_PATH = settings.notifications.signalCliPath; 70 + SIGNAL_SENDER_NUMBER = settings.notifications.signalSenderNumber; 71 + 72 + MAX_BLOB_SIZE = settings.limits.maxBlobSize; 73 + 74 + ACCEPTING_REPO_IMPORTS = boolToStr settings.import.accepting; 75 + MAX_IMPORT_SIZE = settings.import.maxSize; 76 + MAX_IMPORT_BLOCKS = settings.import.maxBlocks; 77 + SKIP_IMPORT_VERIFICATION = boolToStr settings.import.skipVerification; 78 + 79 + INVITE_CODE_REQUIRED = boolToStr settings.registration.inviteCodeRequired; 80 + AVAILABLE_USER_DOMAINS = settings.registration.availableUserDomains; 81 + ENABLE_SELF_HOSTED_DID_WEB = boolToStr settings.registration.enableSelfHostedDidWeb; 82 + 83 + PRIVACY_POLICY_URL = settings.metadata.privacyPolicyUrl; 84 + TERMS_OF_SERVICE_URL = settings.metadata.termsOfServiceUrl; 85 + CONTACT_EMAIL = settings.metadata.contactEmail; 86 + 87 + DISABLE_RATE_LIMITING = boolToStr settings.rateLimiting.disable; 88 + 89 + SCHEDULED_DELETE_CHECK_INTERVAL_SECS = settings.scheduling.deleteCheckIntervalSecs; 90 + 91 + REPORT_SERVICE_URL = settings.moderation.reportServiceUrl; 92 + REPORT_SERVICE_DID = settings.moderation.reportServiceDid; 93 + 94 + PDS_AGE_ASSURANCE_OVERRIDE = boolToStr settings.misc.ageAssuranceOverride; 95 + ALLOW_HTTP_PROXY = boolToStr settings.misc.allowHttpProxy; 96 + 97 + SSO_GITHUB_ENABLED = boolToStr settings.sso.github.enabled; 98 + SSO_GITHUB_CLIENT_ID = settings.sso.github.clientId; 99 + 100 + SSO_DISCORD_ENABLED = boolToStr settings.sso.discord.enabled; 101 + SSO_DISCORD_CLIENT_ID = settings.sso.discord.clientId; 102 + 103 + SSO_GOOGLE_ENABLED = boolToStr settings.sso.google.enabled; 104 + SSO_GOOGLE_CLIENT_ID = settings.sso.google.clientId; 105 + 106 + SSO_GITLAB_ENABLED = boolToStr settings.sso.gitlab.enabled; 107 + SSO_GITLAB_CLIENT_ID = settings.sso.gitlab.clientId; 108 + SSO_GITLAB_ISSUER = settings.sso.gitlab.issuer; 109 + 110 + SSO_OIDC_ENABLED = boolToStr settings.sso.oidc.enabled; 111 + SSO_OIDC_CLIENT_ID = settings.sso.oidc.clientId; 112 + SSO_OIDC_ISSUER = settings.sso.oidc.issuer; 113 + SSO_OIDC_NAME = settings.sso.oidc.name; 114 + 115 + SSO_APPLE_ENABLED = boolToStr settings.sso.apple.enabled; 116 + SSO_APPLE_CLIENT_ID = settings.sso.apple.clientId; 117 + SSO_APPLE_TEAM_ID = settings.sso.apple.teamId; 118 + SSO_APPLE_KEY_ID = settings.sso.apple.keyId; 119 + }; 120 + in 121 + lib.mapAttrs (_: v: toString v) (filterNulls raw); 122 + in { 123 + options.services.tranquil-pds = { 124 + enable = lib.mkEnableOption "tranquil-pds AT Protocol personal data server"; 125 + 126 + package = lib.mkPackageOption pkgs "tranquil-pds" {}; 127 + 128 + user = lib.mkOption { 129 + type = lib.types.str; 130 + default = "tranquil-pds"; 131 + description = "User under which tranquil-pds runs"; 132 + }; 133 + 134 + group = lib.mkOption { 135 + type = lib.types.str; 136 + default = "tranquil-pds"; 137 + description = "Group under which tranquil-pds runs"; 138 + }; 139 + 140 + dataDir = lib.mkOption { 141 + type = lib.types.str; 142 + default = "/var/lib/tranquil-pds"; 143 + description = "Directory for tranquil-pds data (blobs, backups)"; 144 + }; 145 + 146 + secretsFile = lib.mkOption { 147 + type = lib.types.nullOr lib.types.path; 148 + default = null; 149 + description = '' 150 + Path to a file containing secrets in EnvironmentFile format. 151 + Should contain: JWT_SECRET, DPOP_SECRET, MASTER_KEY 152 + May also contain: DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN, 153 + TELEGRAM_WEBHOOK_SECRET, SSO_*_CLIENT_SECRET, SSO_APPLE_PRIVATE_KEY, 154 + AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 155 + ''; 156 + }; 157 + 158 + settings = { 159 + server = { 160 + host = lib.mkOption { 161 + type = lib.types.str; 162 + default = "127.0.0.1"; 163 + description = "Address to bind the server to"; 164 + }; 165 + 166 + port = lib.mkOption { 167 + type = lib.types.port; 168 + default = 3000; 169 + description = "Port to bind the server to"; 170 + }; 171 + 172 + pdsHostname = lib.mkOption { 173 + type = lib.types.str; 174 + description = "Public-facing hostname of the PDS (used in DID documents, JWTs, etc)"; 175 + }; 176 + }; 177 + 178 + database = { 179 + url = lib.mkOption { 180 + type = lib.types.str; 181 + description = "PostgreSQL connection string"; 182 + }; 183 + 184 + maxConnections = lib.mkOption { 185 + type = optionalInt; 186 + default = null; 187 + description = "Maximum database connections"; 188 + }; 189 + 190 + minConnections = lib.mkOption { 191 + type = optionalInt; 192 + default = null; 193 + description = "Minimum database connections"; 194 + }; 195 + 196 + acquireTimeoutSecs = lib.mkOption { 197 + type = optionalInt; 198 + default = null; 199 + description = "Connection acquire timeout in seconds"; 200 + }; 201 + }; 202 + 203 + storage = { 204 + blobBackend = lib.mkOption { 205 + type = lib.types.enum ["filesystem" "s3"]; 206 + default = "filesystem"; 207 + description = "Backend for blob storage"; 208 + }; 209 + 210 + blobPath = lib.mkOption { 211 + type = optionalPath; 212 + default = null; 213 + description = "Path for filesystem blob storage"; 214 + }; 215 + 216 + s3Endpoint = lib.mkOption { 217 + type = optionalStr; 218 + default = null; 219 + description = "S3 endpoint URL (for object storage)"; 220 + }; 221 + 222 + awsRegion = lib.mkOption { 223 + type = optionalStr; 224 + default = null; 225 + description = "Region for objsto"; 226 + }; 227 + 228 + s3Bucket = lib.mkOption { 229 + type = optionalStr; 230 + default = null; 231 + description = "Bucket name for objsto"; 232 + }; 233 + }; 234 + 235 + backup = { 236 + enabled = lib.mkOption { 237 + type = optionalBool; 238 + default = null; 239 + description = "Enable automatic repo backups"; 240 + }; 241 + 242 + backend = lib.mkOption { 243 + type = lib.types.enum ["filesystem" "s3"]; 244 + default = "filesystem"; 245 + description = "Backend for backup storage"; 246 + }; 247 + 248 + path = lib.mkOption { 249 + type = optionalPath; 250 + default = null; 251 + description = "Path for filesystem backup storage"; 252 + }; 253 + 254 + s3Bucket = lib.mkOption { 255 + type = optionalStr; 256 + default = null; 257 + description = "Object storage bucket name for backups"; 258 + }; 259 + 260 + retentionCount = lib.mkOption { 261 + type = optionalInt; 262 + default = null; 263 + description = "Number of backups to retain"; 264 + }; 265 + 266 + intervalSecs = lib.mkOption { 267 + type = optionalInt; 268 + default = null; 269 + description = "Backup interval in seconds"; 270 + }; 271 + }; 272 + 273 + cache = { 274 + valkeyUrl = lib.mkOption { 275 + type = optionalStr; 276 + default = null; 277 + description = "Valkey URL for caching"; 278 + }; 279 + }; 280 + 281 + security = { 282 + allowInsecureSecrets = lib.mkOption { 283 + type = optionalBool; 284 + default = null; 285 + description = "Allow default/weak secrets (development only, NEVER in production ofc)"; 286 + }; 287 + }; 288 + 289 + plc = { 290 + directoryUrl = lib.mkOption { 291 + type = optionalStr; 292 + default = null; 293 + description = "PLC directory URL"; 294 + }; 295 + 296 + timeoutSecs = lib.mkOption { 297 + type = optionalInt; 298 + default = null; 299 + description = "PLC request timeout in seconds"; 300 + }; 301 + 302 + connectTimeoutSecs = lib.mkOption { 303 + type = optionalInt; 304 + default = null; 305 + description = "PLC connection timeout in seconds"; 306 + }; 307 + 308 + rotationKey = lib.mkOption { 309 + type = optionalStr; 310 + default = null; 311 + description = "Rotation key for PLC operations (did:key:xyz)"; 312 + }; 313 + }; 314 + 315 + did = { 316 + cacheTtlSecs = lib.mkOption { 317 + type = optionalInt; 318 + default = null; 319 + description = "DID document cache TTL in seconds"; 320 + }; 321 + }; 322 + 323 + relay = { 324 + crawlers = lib.mkOption { 325 + type = optionalStr; 326 + default = null; 327 + description = "Comma-separated list of relay URLs to notify via requestCrawl"; 328 + }; 329 + }; 330 + 331 + firehose = { 332 + bufferSize = lib.mkOption { 333 + type = optionalInt; 334 + default = null; 335 + description = "Firehose broadcast channel buffer size"; 336 + }; 337 + 338 + maxLag = lib.mkOption { 339 + type = optionalInt; 340 + default = null; 341 + description = "Disconnect slow consumers after this many events of lag"; 342 + }; 343 + }; 344 + 345 + notifications = { 346 + batchSize = lib.mkOption { 347 + type = optionalInt; 348 + default = null; 349 + description = "Notification queue batch size"; 350 + }; 351 + 352 + pollIntervalMs = lib.mkOption { 353 + type = optionalInt; 354 + default = null; 355 + description = "Notification queue poll interval in ms"; 356 + }; 357 + 358 + mailFromAddress = lib.mkOption { 359 + type = optionalStr; 360 + default = null; 361 + description = "Email from address for notifications"; 362 + }; 363 + 364 + mailFromName = lib.mkOption { 365 + type = optionalStr; 366 + default = null; 367 + description = "Email from name for notifications"; 368 + }; 369 + 370 + sendmailPath = lib.mkOption { 371 + type = optionalPath; 372 + default = null; 373 + description = "Path to sendmail binary"; 374 + }; 375 + 376 + signalCliPath = lib.mkOption { 377 + type = optionalPath; 378 + default = null; 379 + description = "Path to signal-cli binary"; 380 + }; 381 + 382 + signalSenderNumber = lib.mkOption { 383 + type = optionalStr; 384 + default = null; 385 + description = "Signal sender phone number"; 386 + }; 387 + }; 388 + 389 + limits = { 390 + maxBlobSize = lib.mkOption { 391 + type = optionalInt; 392 + default = null; 393 + description = "Maximum blob size in bytes"; 394 + }; 395 + }; 396 + 397 + import = { 398 + accepting = lib.mkOption { 399 + type = optionalBool; 400 + default = null; 401 + description = "Accept repository imports"; 402 + }; 403 + 404 + maxSize = lib.mkOption { 405 + type = optionalInt; 406 + default = null; 407 + description = "Maximum import size in bytes"; 408 + }; 409 + 410 + maxBlocks = lib.mkOption { 411 + type = optionalInt; 412 + default = null; 413 + description = "Maximum blocks per import"; 414 + }; 415 + 416 + skipVerification = lib.mkOption { 417 + type = optionalBool; 418 + default = null; 419 + description = "Skip verification during import (testing only)"; 420 + }; 421 + }; 422 + 423 + registration = { 424 + inviteCodeRequired = lib.mkOption { 425 + type = optionalBool; 426 + default = null; 427 + description = "Require invite codes for registration"; 428 + }; 429 + 430 + availableUserDomains = lib.mkOption { 431 + type = optionalStr; 432 + default = null; 433 + description = "Comma-separated list of available user domains"; 434 + }; 435 + 436 + enableSelfHostedDidWeb = lib.mkOption { 437 + type = optionalBool; 438 + default = null; 439 + description = "Enable self-hosted did:web identities"; 440 + }; 441 + }; 442 + 443 + metadata = { 444 + privacyPolicyUrl = lib.mkOption { 445 + type = optionalStr; 446 + default = null; 447 + description = "Privacy policy URL"; 448 + }; 449 + 450 + termsOfServiceUrl = lib.mkOption { 451 + type = optionalStr; 452 + default = null; 453 + description = "Terms of service URL"; 454 + }; 455 + 456 + contactEmail = lib.mkOption { 457 + type = optionalStr; 458 + default = null; 459 + description = "Contact email address"; 460 + }; 461 + }; 462 + 463 + rateLimiting = { 464 + disable = lib.mkOption { 465 + type = optionalBool; 466 + default = null; 467 + description = "Disable rate limiting (testing only, NEVER in production you naughty!)"; 468 + }; 469 + }; 470 + 471 + scheduling = { 472 + deleteCheckIntervalSecs = lib.mkOption { 473 + type = optionalInt; 474 + default = null; 475 + description = "Scheduled deletion check interval in seconds"; 476 + }; 477 + }; 478 + 479 + moderation = { 480 + reportServiceUrl = lib.mkOption { 481 + type = optionalStr; 482 + default = null; 483 + description = "Moderation report service URL (like ozone)"; 484 + }; 485 + 486 + reportServiceDid = lib.mkOption { 487 + type = optionalStr; 488 + default = null; 489 + description = "Moderation report service DID"; 490 + }; 491 + }; 492 + 493 + misc = { 494 + ageAssuranceOverride = lib.mkOption { 495 + type = optionalBool; 496 + default = null; 497 + description = "Override age assurance checks"; 498 + }; 499 + 500 + allowHttpProxy = lib.mkOption { 501 + type = optionalBool; 502 + default = null; 503 + description = "Allow HTTP for proxy requests (development only)"; 504 + }; 505 + }; 506 + 507 + sso = { 508 + github = { 509 + enabled = lib.mkOption { 510 + type = optionalBool; 511 + default = null; 512 + description = "Enable GitHub SSO"; 513 + }; 514 + 515 + clientId = lib.mkOption { 516 + type = optionalStr; 517 + default = null; 518 + description = "GitHub OAuth client ID"; 519 + }; 520 + }; 521 + 522 + discord = { 523 + enabled = lib.mkOption { 524 + type = optionalBool; 525 + default = null; 526 + description = "Enable Discord SSO"; 527 + }; 528 + 529 + clientId = lib.mkOption { 530 + type = optionalStr; 531 + default = null; 532 + description = "Discord OAuth client ID"; 533 + }; 534 + }; 535 + 536 + google = { 537 + enabled = lib.mkOption { 538 + type = optionalBool; 539 + default = null; 540 + description = "Enable Google SSO"; 541 + }; 542 + 543 + clientId = lib.mkOption { 544 + type = optionalStr; 545 + default = null; 546 + description = "Google OAuth client ID"; 547 + }; 548 + }; 549 + 550 + gitlab = { 551 + enabled = lib.mkOption { 552 + type = optionalBool; 553 + default = null; 554 + description = "Enable GitLab SSO"; 555 + }; 556 + 557 + clientId = lib.mkOption { 558 + type = optionalStr; 559 + default = null; 560 + description = "GitLab OAuth client ID"; 561 + }; 562 + 563 + issuer = lib.mkOption { 564 + type = optionalStr; 565 + default = null; 566 + description = "GitLab issuer URL"; 567 + }; 568 + }; 569 + 570 + oidc = { 571 + enabled = lib.mkOption { 572 + type = optionalBool; 573 + default = null; 574 + description = "Enable generic OIDC SSO"; 575 + }; 576 + 577 + clientId = lib.mkOption { 578 + type = optionalStr; 579 + default = null; 580 + description = "OIDC client ID"; 581 + }; 582 + 583 + issuer = lib.mkOption { 584 + type = optionalStr; 585 + default = null; 586 + description = "OIDC issuer URL"; 587 + }; 588 + 589 + name = lib.mkOption { 590 + type = optionalStr; 591 + default = null; 592 + description = "OIDC provider display name"; 593 + }; 594 + }; 595 + 596 + apple = { 597 + enabled = lib.mkOption { 598 + type = optionalBool; 599 + default = null; 600 + description = "Enable Apple Sign-in"; 601 + }; 602 + 603 + clientId = lib.mkOption { 604 + type = optionalStr; 605 + default = null; 606 + description = "Apple Services ID"; 607 + }; 608 + 609 + teamId = lib.mkOption { 610 + type = optionalStr; 611 + default = null; 612 + description = "Apple Team ID"; 613 + }; 614 + 615 + keyId = lib.mkOption { 616 + type = optionalStr; 617 + default = null; 618 + description = "Apple Key ID"; 619 + }; 620 + }; 621 + }; 622 + }; 623 + }; 624 + 625 + config = let 626 + effectiveBlobPath = 627 + if cfg.settings.storage.blobPath != null 628 + then cfg.settings.storage.blobPath 629 + else "${cfg.dataDir}/blobs"; 630 + effectiveBackupPath = 631 + if cfg.settings.backup.path != null 632 + then cfg.settings.backup.path 633 + else "${cfg.dataDir}/backups"; 634 + envVars = 635 + (settingsToEnv cfg.settings) 636 + // { 637 + BLOB_STORAGE_PATH = effectiveBlobPath; 638 + BACKUP_STORAGE_PATH = effectiveBackupPath; 639 + }; 640 + in 641 + lib.mkIf cfg.enable { 642 + assertions = [ 643 + { 644 + assertion = config.systemd.enable or true; 645 + message = "services.tranquil-pds requires systemd"; 646 + } 647 + ]; 648 + 649 + users.users.${cfg.user} = { 650 + isSystemUser = true; 651 + inherit (cfg) group; 652 + home = cfg.dataDir; 653 + }; 654 + 655 + users.groups.${cfg.group} = {}; 656 + 657 + systemd.tmpfiles.rules = [ 658 + "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -" 659 + "d ${effectiveBlobPath} 0750 ${cfg.user} ${cfg.group} -" 660 + "d ${effectiveBackupPath} 0750 ${cfg.user} ${cfg.group} -" 661 + ]; 662 + 663 + systemd.services.tranquil-pds = { 664 + description = "Tranquil PDS - AT Protocol Personal Data Server"; 665 + after = ["network.target" "postgresql.service"]; 666 + wants = ["network.target"]; 667 + wantedBy = ["multi-user.target"]; 668 + 669 + environment = envVars; 670 + 671 + serviceConfig = { 672 + Type = "exec"; 673 + User = cfg.user; 674 + Group = cfg.group; 675 + ExecStart = "${cfg.package}/bin/tranquil-pds"; 676 + Restart = "on-failure"; 677 + RestartSec = 5; 678 + 679 + WorkingDirectory = cfg.dataDir; 680 + StateDirectory = "tranquil-pds"; 681 + 682 + EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile; 683 + 684 + NoNewPrivileges = true; 685 + ProtectSystem = "strict"; 686 + ProtectHome = true; 687 + PrivateTmp = true; 688 + PrivateDevices = true; 689 + ProtectKernelTunables = true; 690 + ProtectKernelModules = true; 691 + ProtectControlGroups = true; 692 + RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"]; 693 + RestrictNamespaces = true; 694 + LockPersonality = true; 695 + MemoryDenyWriteExecute = true; 696 + RestrictRealtime = true; 697 + RestrictSUIDSGID = true; 698 + RemoveIPC = true; 699 + 700 + ReadWritePaths = [ 701 + effectiveBlobPath 702 + effectiveBackupPath 703 + ]; 704 + }; 705 + }; 706 + }; 707 + }
+75
test.nix
··· 1 + { 2 + pkgs, 3 + self, 4 + ... 5 + }: 6 + pkgs.testers.nixosTest { 7 + name = "tranquil-pds"; 8 + 9 + nodes.server = { 10 + config, 11 + pkgs, 12 + ... 13 + }: { 14 + imports = [self.nixosModules.default]; 15 + 16 + services.postgresql = { 17 + enable = true; 18 + ensureDatabases = ["tranquil"]; 19 + ensureUsers = [ 20 + { 21 + name = "tranquil"; 22 + ensureDBOwnership = true; 23 + } 24 + ]; 25 + authentication = '' 26 + local all all trust 27 + host all all 127.0.0.1/32 trust 28 + host all all ::1/128 trust 29 + ''; 30 + }; 31 + 32 + services.tranquil-pds = { 33 + enable = true; 34 + package = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-pds; 35 + secretsFile = pkgs.writeText "tranquil-secrets" '' 36 + JWT_SECRET=test-jwt-secret-must-be-32-chars-long 37 + DPOP_SECRET=test-dpop-secret-must-be-32-chars-long 38 + MASTER_KEY=test-master-key-must-be-32-chars-long 39 + ''; 40 + 41 + settings = { 42 + server.pdsHostname = "test.local"; 43 + server.host = "0.0.0.0"; 44 + 45 + database.url = "postgres://tranquil@localhost/tranquil"; 46 + 47 + storage.blobBackend = "filesystem"; 48 + backup.backend = "filesystem"; 49 + }; 50 + }; 51 + 52 + networking.firewall.allowedTCPPorts = [3000]; 53 + }; 54 + 55 + testScript = '' 56 + server.wait_for_unit("postgresql.service") 57 + server.wait_for_unit("tranquil-pds.service") 58 + server.wait_for_open_port(3000) 59 + 60 + with subtest("service is running"): 61 + status = server.succeed("systemctl is-active tranquil-pds") 62 + assert "active" in status 63 + 64 + with subtest("blob storage directory exists"): 65 + server.succeed("test -d /var/lib/tranquil-pds/blobs") 66 + server.succeed("test -d /var/lib/tranquil-pds/backups") 67 + 68 + with subtest("healthcheck responds"): 69 + server.succeed("curl -sf http://localhost:3000/xrpc/_health") 70 + 71 + with subtest("describeServer returns valid response"): 72 + result = server.succeed("curl -sf http://localhost:3000/xrpc/com.atproto.server.describeServer") 73 + assert "availableUserDomains" in result 74 + ''; 75 + }