diff --git a/README.md b/README.md index 5d8c2d0..49c1140 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,35 @@ Or from a checkout: `docker compose up -d` (see `docker-compose.yml`). 2. Attach a **volume** mounted at `/data`. 3. That's it — the server listens on Railway's `PORT` automatically. +### NixOS +Add this repository as a flake input and add the module to your configuration: +```nix +{ + crosspoint-sync.url = "github:rogierknoester/crosspoint-sync"; + crosspoint-sync.inputs.nixpkgs.follows = "nixpkgs"; + ... +}: { + nixosConfigurations = { + myServer = nixpkgs.lib.nixosSystem { + ... + modules = [ + ./configuration.nix + crosspoint-sync.nixosModules.crosspoint-sync + ]; + }; + }; +} +``` + +Now you can enable it in your `configuration.nix`: +```nix +services.crosspoint-sync = { + enable = true; + port = 8080; + registration = true; +}; +``` + ### Bare Node (≥ 22.13) ```sh diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..bb3513c --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1786534138, + "narHash": "sha256-fBJMdnKUTUDtfi/BYLr71HLaC9dG382arxLF2Egg2uo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "044bfe75bfe4c7bbe043dc17b5e42ea823b84a09", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..478a3ed --- /dev/null +++ b/flake.nix @@ -0,0 +1,33 @@ +{ + description = "crosspoint-sync flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + nixpkgs, + flake-utils, + ... + }: + { + nixosModules = { + crosspoint-sync = import ./nix/module.nix; + }; + } + // flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + crosspoint-sync = pkgs.callPackage ./nix/package.nix { }; + in + { + packages = { + inherit crosspoint-sync; + default = crosspoint-sync; + }; + } + ); +} diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..11e4dde --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,150 @@ +{ + pkgs, + lib, + config, + ... +}: + +let + + inherit (lib) + mkOption + mkEnableOption + mkIf + types + ; + cfg = config.services.crosspoint-sync; + crosspoint-sync = pkgs.callPackage ./package.nix { }; + + filename = + types.addCheck types.str (v: v != "" && !(lib.hasInfix "/" v) && v != "." && v != "..") + // { + description = "a filename; cannot traverse directories or be an absolute path"; + }; +in +{ + + options.services.crosspoint-sync = { + enable = mkEnableOption "Enable crosspoint-sync server"; + + port = mkOption { + type = types.port; + description = "Port to run crosspoint-sync server on"; + default = 8080; + }; + + databaseFile = mkOption { + type = filename; + description = "Filename of the SQLite database in the state directory; "; + default = "crosspoint.db"; + }; + + registration = mkOption { + type = types.bool; + description = "Have registration enabled or not"; + default = true; + }; + + tokenEncryptionKeyFile = mkOption { + type = types.nullOr types.str; + description = "Path to the secret that contains the encryption key"; + }; + + authRateLimit = mkOption { + type = types.int; + description = "Per-IP limit on registrations"; + default = 30; + }; + + user = mkOption { + type = types.str; + description = "User to run crosspoint-sync with"; + default = "crosspoint-sync"; + }; + + group = mkOption { + type = types.str; + description = "Group to run crosspoint-sync with"; + default = "crosspoint-sync"; + }; + + }; + + config = mkIf cfg.enable { + systemd.services.crosspoint-sync = { + description = "crosspoint-sync server"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + ExecStart = + if (cfg.tokenEncryptionKeyFile != null) then + pkgs.writeShellScript "crosspoint-sync-credential-loader" '' + export TOKEN_ENC_KEY="$(cat "$CREDENTIALS_DIRECTORY/TOKEN_ENC_KEY_FILE")" + exec ${lib.getExe crosspoint-sync} + '' + else + "${lib.getExe crosspoint-sync}"; + Restart = "on-failure"; + + User = cfg.user; + Group = cfg.group; + LoadCredential = mkIf ( + cfg.tokenEncryptionKeyFile != null + ) "TOKEN_ENC_KEY_FILE:${cfg.tokenEncryptionKeyFile}"; + StateDirectory = "crosspoint-sync"; + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = !(cfg.port < 1024); + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectClock = true; + ProtectHostname = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + LockPersonality = true; + UMask = "0077"; + RemoveIPC = true; + AmbientCapabilities = if (cfg.port < 1024) then "CAP_NET_BIND_SERVICE" else lib.mkForce ""; + CapabilityBoundingSet = if (cfg.port < 1024) then "CAP_NET_BIND_SERVICE" else lib.mkForce ""; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + SystemCallFilter = [ "@system-service" ]; + SystemCallErrorNumber = "EPERM"; + RestrictRealtime = true; + + }; + + environment = { + PORT = builtins.toString cfg.port; + DATABASE_PATH = "/var/lib/crosspoint-sync/${cfg.databaseFile}"; + REGISTRATION_DISABLED = if !cfg.registration then "true" else "false"; + AUTH_RATE_LIMIT_PER_MINUTE = builtins.toString cfg.authRateLimit; + }; + + }; + + networking.firewall.allowedTCPPorts = [ cfg.port ]; + + users = { + users.crosspoint-sync = mkIf (cfg.user == "crosspoint-sync") { + description = "crosspoint-sync service user"; + isSystemUser = true; + group = cfg.group; + }; + + groups.crosspoint-sync = mkIf (cfg.group == "crosspoint-sync") { }; + }; + + }; + +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..53d1d73 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,35 @@ +{ pkgs, lib }: + +with pkgs; +buildNpmPackage { + pname = "crosspoint-sync"; + version = "git"; + src = ../.; + + npmDeps = importNpmLock { + npmRoot = ../.; + }; + + npmConfigHook = importNpmLock.npmConfigHook; + + buildPhase = '' + npm run build + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/lib $out/bin + cp -r package.json dist node_modules assets migrations $out/lib/ + makeWrapper ${lib.getExe nodejs} $out/bin/crosspoint-sync \ + --add-flags "$out/lib/dist/index.js" + runHook postInstall + ''; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + + meta = { + description = "Lightweight KoSync Server for Syncing Crosspoint/CrossInk stats & progress"; + homepage = "https://github.com/crosspoint-reader/crosspoint-sync"; + mainProgram = "crosspoint-sync"; + }; +}