diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..6a49ee6 --- /dev/null +++ b/default.nix @@ -0,0 +1,69 @@ +{ self, rVersion }: +{ + pkgs, + lib, + nodejs, + ... +}: + +pkgs.buildNpmPackage { + pname = "spacebar-server-ts"; + nodejs = pkgs.nodejs_24; + version = "1.0.0-" + rVersion; + + meta = with lib; { + description = "Spacebar server, a FOSS reimplementation of the Discord backend."; + homepage = "https://github.com/spacebarchat/server"; + license = licenses.agpl3Plus; + platforms = platforms.all; + mainProgram = "start-bundle"; + maintainers = with maintainers; [ RorySys ]; # lol. + }; + + src = ./.; + npmDeps = pkgs.importNpmLock { npmRoot = ./.; }; + npmConfigHook = pkgs.importNpmLock.npmConfigHook; + + npmBuildScript = "build:src"; + makeCacheWritable = true; + nativeBuildInputs = with pkgs; [ + (pkgs.python3.withPackages (ps: with ps; [ setuptools ])) + ]; + + installPhase = + let + revsFile = pkgs.writeText "spacebar-server-rev.json" ( + builtins.toJSON { + rev = self.sourceInfo.rev or self.sourceInfo.dirtyRev; + shortRev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev; + lastModified = self.sourceInfo.lastModified; + } + ); + in + '' + runHook preInstall + # set -x + + # remove packages not needed for production, or at least try to... + npm prune --omit dev --no-save $npmInstallFlags "''${npmInstallFlagsArray[@]}" $npmFlags "''${npmFlagsArray[@]}" + ${./nix/trimNodeModules.sh} + + # Copy outputs + echo "Installing package into $out" + mkdir -p $out + cp -r assets dist node_modules package.json $out/ + cp ${revsFile} $out/.rev + + # Create wrappers for start scripts + echo "Creating wrappers for start scripts" + for i in dist/**/start.js + do + makeWrapper ${pkgs.nodejs_24}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags --enable-source-maps --add-flags $out/$i + done + + # set +x + runHook postInstall + ''; + + passthru.tests = pkgs.testers.runNixOSTest (import ./nix/tests/test-bundle-starts.nix self); +} diff --git a/flake.lock b/flake.lock index 5222f3c..388be40 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765779637, - "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", + "lastModified": 1766309749, + "narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", + "rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 32eb93a..c0fc2aa 100644 --- a/flake.nix +++ b/flake.nix @@ -33,67 +33,7 @@ in { packages = { - default = pkgs.buildNpmPackage { - pname = "spacebar-server-ts"; - nodejs = pkgs.nodejs_24; - version = "1.0.0-" + rVersion; - - meta = with lib; { - description = "Spacebar server, a FOSS reimplementation of the Discord backend."; - homepage = "https://github.com/spacebarchat/server"; - license = licenses.agpl3Plus; - platforms = platforms.all; - mainProgram = "start-bundle"; - maintainers = with maintainers; [ RorySys ]; # lol. - }; - - src = ./.; - npmDeps = pkgs.importNpmLock { npmRoot = ./.; }; - npmConfigHook = pkgs.importNpmLock.npmConfigHook; - - npmBuildScript = "build:src"; - makeCacheWritable = true; - nativeBuildInputs = with pkgs; [ - (pkgs.python3.withPackages (ps: with ps; [ setuptools ])) - ]; - - installPhase = - let - revsFile = pkgs.writeText "spacebar-server-rev.json" ( - builtins.toJSON { - rev = self.sourceInfo.rev or self.sourceInfo.dirtyRev; - shortRev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev; - lastModified = self.sourceInfo.lastModified; - } - ); - in - '' - runHook preInstall - # set -x - - # remove packages not needed for production, or at least try to... - npm prune --omit dev --no-save $npmInstallFlags "''${npmInstallFlagsArray[@]}" $npmFlags "''${npmFlagsArray[@]}" - ${./nix/trimNodeModules.sh} - - # Copy outputs - echo "Installing package into $out" - mkdir -p $out - cp -r assets dist node_modules package.json $out/ - cp ${revsFile} $out/.rev - - # Create wrappers for start scripts - echo "Creating wrappers for start scripts" - for i in dist/**/start.js - do - makeWrapper ${pkgs.nodejs_24}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags --enable-source-maps --add-flags $out/$i - done - - # set +x - runHook postInstall - ''; - - passthru.tests = pkgs.testers.runNixOSTest (import ./nix/tests/test-bundle-starts.nix self); - }; + default = (pkgs.callPackage (import ./default.nix { inherit self rVersion; })) { }; }; containers = { diff --git a/nix/modules/default/default.nix b/nix/modules/default/default.nix index 1173352..2c8594f 100644 --- a/nix/modules/default/default.nix +++ b/nix/modules/default/default.nix @@ -1,103 +1,234 @@ -self: { config, lib, pkgs, spacebar, ... }: +self: +{ + config, + lib, + pkgs, + spacebar, + ... +}: let cfg = config.services.spacebarchat-server; - jsonFormat = pkgs.formats.json {}; - configFile = jsonFormat.generate "spacebarchat-server.json" cfg.settings; + jsonFormat = pkgs.formats.json { }; + configFile = jsonFormat.generate "spacebarchat-server.json" ( + lib.recursiveUpdate { + api = { + endpointPublic = "http${if cfg.apiEndpoint.useSsl then "s" else ""}://${cfg.apiEndpoint.host}:${toString cfg.apiEndpoint.publicPort}/"; + }; + cdn = { + endpointPublic = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://${cfg.cdnEndpoint.host}:${toString cfg.cdnEndpoint.publicPort}/"; + endpointPrivate = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://127.0.0.1:${toString cfg.cdnEndpoint.localPort}/"; + }; + gateway = { + endpointPublic = "ws${if cfg.gatewayEndpoint.useSsl then "s" else ""}://${cfg.gatewayEndpoint.host}:${toString cfg.gatewayEndpoint.publicPort}/"; + }; + } cfg.settings + ); in { - options.services.spacebarchat-server = { - enable = lib.mkEnableOption "spacebarchat-server"; - package = lib.mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "spacebar-server" { default = "default"; }; - databaseFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - description = '' - Path to a file containing a definition of the `DATABASE` environment variable database connection string. - Example content: `DATABASE=postgres//username:password@host-IP:port/databaseName`. - See https://docs.spacebar.chat/setup/server/database/. - ''; - }; - extraEnvironment = lib.mkOption { - default = {}; - description = '' - Environment variables passed to spacebarchat-server. - See https://docs.spacebar.chat/setup/server/configuration/env for supported values. - ''; - type = lib.types.submodule { - freeformType = with lib.types; attrsOf (oneOf [ str bool ]); - options = { - THREADS = lib.mkOption { - type = lib.types.ints.positive; - default = 1; - description = "Number of threads to run Spacebar on when using bundle. Make sure you've enabled RabbitMQ if using more than one."; + imports = [ ./integration-nginx.nix ]; + options.services.spacebarchat-server = + let + mkEndpointOptions = + defaultHost: defaultPort: + lib.mkOption { + type = lib.types.submodule { + options = { + useSsl = lib.mkEnableOption "Use SSL for this endpoint."; + host = lib.mkOption { + type = lib.types.str; + default = defaultHost; + description = "Host to bind to."; + }; + localPort = lib.mkOption { + type = lib.types.port; + default = defaultPort; + description = "Port to bind to."; + }; + publicPort = lib.mkOption { + type = lib.types.port; + default = 443; + description = "Public port to use in .well-known, defaults to 443."; + }; + }; }; - PORT = lib.mkOption { - type = lib.types.port; - default = 3001; - description = "Port to listen on. Used by all components, including bundle. If using bundle, all components run under the same port"; + default = { }; + }; + in + { + enable = lib.mkEnableOption "spacebar server"; + package = lib.mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "spacebar-server" { default = "default"; }; + databaseFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to a file containing a definition of the `DATABASE` environment variable database connection string. + Example content: `DATABASE=postgres//username:password@host-IP:port/databaseName`. + See https://docs.spacebar.chat/setup/server/database/. + ''; + }; + + apiEndpoint = mkEndpointOptions "api.sb.localhost" 3001; + gatewayEndpoint = mkEndpointOptions "gateway.sb.localhost" 3003; + cdnEndpoint = mkEndpointOptions "cdn.sb.localhost" 3003; + + extraEnvironment = lib.mkOption { + default = { }; + description = '' + Environment variables passed to spacebarchat-server. + See https://docs.spacebar.chat/setup/server/configuration/env for supported values. + ''; + type = lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + str + bool + ]); + options = { + THREADS = lib.mkOption { + type = lib.types.ints.positive; + default = 1; + description = "Number of threads to run Spacebar on when using bundle. Make sure you've enabled RabbitMQ if using more than one."; + }; + PORT = lib.mkOption { + type = lib.types.port; + default = 3001; + description = "Port to listen on. Used by all components, including bundle. If using bundle, all components run under the same port"; + }; }; }; }; + + settings = lib.mkOption { + type = jsonFormat.type; + default = { }; + description = '' + Configuration for spacebarchat-server. + See https://docs.spacebar.chat/setup/server/configuration for supported values. + ''; + }; }; - settings = lib.mkOption { - type = jsonFormat.type; - default = {}; - description = '' - Configuration for spacebarchat-server. - See https://docs.spacebar.chat/setup/server/configuration for supported values. - ''; - }; - }; + config = lib.mkIf cfg.enable ( + let + makeServerTsService = ( + conf: + lib.recursiveUpdate + (lib.recursiveUpdate { + documentation = [ "https://docs.spacebar.chat/" ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + User = "spacebarchat"; + Group = "spacebarchat"; + DynamicUser = true; + LockPersonality = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateUsers = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + StateDirectory = "spacebar-server"; + StateDirectoryMode = "0700"; - config = lib.mkIf cfg.enable { - assertions = [ { - assertion = !((cfg.extraEnvironment.THREADS > 1) && !config.services.rabbitmq.enable); - message = "Make sure you've setup RabbitMQ when using more than one thread with Spacebar"; - } ]; - - systemd.services.spacebarchat-server = { - description = "Spacebarchat Server"; - documentation = [ "https://docs.spacebar.chat/" ]; - wantedBy = [ "multi-user.target" ]; - wants = [ "network-online.target" ]; - after = [ "network-online.target" ]; - environment = builtins.mapAttrs (_: val: builtins.toString val) (cfg.extraEnvironment // { CONFIG_PATH = configFile; CONFIG_READONLY = 1; }); - serviceConfig = { - DynamicUser = true; - User = "spacebarchat-server"; - LockPersonality = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - PrivateDevices = true; - PrivateMounts = true; - PrivateUsers = true; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - ]; - RestrictNamespaces = true; - RestrictRealtime = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - "~@privileged" - ]; - StateDirectory = "spacebarchat-server"; - StateDirectoryMode = "0700"; - ExecStart = "${cfg.package}/bin/start-bundle"; - Restart = "on-failure"; - RestartSec = 10; - StartLimitBurst = 5; - UMask = "077"; - WorkingDirectory = "/var/lib/spacebarchat-server/"; - } - // lib.optionalAttrs (cfg.databaseFile != null) { EnvironmentFile = cfg.databaseFile; }; - }; - }; + Restart = "on-failure"; + RestartSec = 10; + StartLimitBurst = 5; + UMask = "077"; + # WorkingDirectory = "/var/lib/spacebarchat-server/"; + } + // lib.optionalAttrs (cfg.databaseFile != null) { EnvironmentFile = cfg.databaseFile; }; + } conf) + { + } + ); + in + { + assertions = [ +# { +# assertion = !((cfg.extraEnvironment.THREADS > 1) && !config.services.rabbitmq.enable); +# message = "Make sure you've setup RabbitMQ when using more than one thread with Spacebar"; +# } + ]; + + systemd.tmpfiles.rules = [ "d /run/spacebarchat 0750 spacebar spacebar" ]; + + systemd.services.spacebar-api = makeServerTsService { + description = "Spacebar Server - API"; + environment = builtins.mapAttrs (_: val: builtins.toString val) ( + { + # things we set by default... + EVENT_TRANSMISSION = "unix"; + EVENT_SOCKET_PATH = "/run/spacebarchat/"; + } + // cfg.extraEnvironment + // { + # things we force... + CONFIG_PATH = configFile; + CONFIG_READONLY = 1; + } + ); + serviceConfig = { + ExecStart = "${cfg.package}/bin/start-api"; + }; + }; + + systemd.services.spacebar-gateway = makeServerTsService { + description = "Spacebar Server - Gateway"; + environment = builtins.mapAttrs (_: val: builtins.toString val) ( + { + # things we set by default... + EVENT_TRANSMISSION = "unix"; + EVENT_SOCKET_PATH = "/run/spacebarchat/"; + } + // cfg.extraEnvironment + // { + # things we force... + CONFIG_PATH = configFile; + CONFIG_READONLY = 1; + } + ); + serviceConfig = { + ExecStart = "${cfg.package}/bin/start-gateway"; + }; + }; + + systemd.services.spacebar-cdn = makeServerTsService { + description = "Spacebar Server - CDN"; + environment = builtins.mapAttrs (_: val: builtins.toString val) ( + { + # things we set by default... + EVENT_TRANSMISSION = "unix"; + EVENT_SOCKET_PATH = "/run/spacebarchat/"; + } + // cfg.extraEnvironment + // { + # things we force... + CONFIG_PATH = configFile; + CONFIG_READONLY = 1; + } + ); + serviceConfig = { + ExecStart = "${cfg.package}/bin/start-cdn"; + }; + }; + } + ); } diff --git a/nix/modules/default/integration-nginx.nix b/nix/modules/default/integration-nginx.nix new file mode 100644 index 0000000..fb90fc2 --- /dev/null +++ b/nix/modules/default/integration-nginx.nix @@ -0,0 +1,45 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.spacebarchat-server; +in +{ + options.services.spacebarchat-server.nginx = { + enable = lib.mkEnableOption "nginx integration"; + }; + + config = lib.mkIf (cfg.enable && cfg.nginx.enable) { + services.nginx = lib.trace (cfg.apiEndpoint) { + virtualHosts = lib.mkIf cfg.enable { + "${cfg.apiEndpoint.host}" = { + enableACME = cfg.apiEndpoint.useSsl; + forceSSL = cfg.apiEndpoint.useSsl; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString cfg.apiEndpoint.localPort}/"; + }; + }; + "${cfg.gatewayEndpoint.host}" = { + enableACME = cfg.gatewayEndpoint.useSsl; + forceSSL = cfg.gatewayEndpoint.useSsl; + locations."/" = { + proxyWebsockets = true; + proxyPass = "http://127.0.0.1:${toString cfg.gatewayEndpoint.localPort}/"; + }; + }; + "${cfg.cdnEndpoint.host}" = { + enableACME = cfg.cdnEndpoint.useSsl; + forceSSL = cfg.cdnEndpoint.useSsl; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString cfg.cdnEndpoint.localPort}/"; + }; + }; + + }; + }; + }; +} diff --git a/nix/modules/default/lib.nix b/nix/modules/default/lib.nix new file mode 100644 index 0000000..886b78f --- /dev/null +++ b/nix/modules/default/lib.nix @@ -0,0 +1,14 @@ +{ + mkEndpoint = domain: port: ssl: { + host = domain; + localPort = port; + useSsl = ssl; + publicPort = + if ssl then + 443 + else if domain == "localhost" then + port + else + 80; + }; +} diff --git a/nix/tests/test-bundle-starts.nix b/nix/tests/test-bundle-starts.nix index 14108a4..5f1a3f9 100644 --- a/nix/tests/test-bundle-starts.nix +++ b/nix/tests/test-bundle-starts.nix @@ -1,23 +1,38 @@ self: -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: +let + sb = import ../modules/default/lib.nix; +in { name = "test-bundle-starts"; nodes.machine = { imports = [ self.nixosModules.default ]; - services.spacebarchat-server = { - enable = true; - settings = { - api = { endpointPublic = "http://localhost:3001/api/v9/"; }; - cdn = { endpointPublic = "http://localhost:3001/"; endpointPrivate = "http://localhost:3001/"; }; - gateway = { endpointPublic = "ws://localhost:3001/"; }; + services.spacebarchat-server = + let + cfg = { + enable = true; + apiEndpoint = sb.mkEndpoint "api.sb.localhost" 3001 false; + gatewayEndpoint = sb.mkEndpoint "gw.sb.localhost" 3002 false; + cdnEndpoint = sb.mkEndpoint "cdn.sb.localhost" 3003 false; + nginx.enable = true; }; - }; + in + lib.trace ("Testing with config: " + builtins.toJSON cfg) cfg; + services.nginx.enable = true; }; testScript = '' - machine.wait_for_unit("spacebarchat-server") + machine.wait_for_unit("spacebar-api") + machine.wait_for_unit("spacebar-cdn") + machine.wait_for_unit("spacebar-gateway") + # machine.succeed("curl -f http://api.sb.localhost/.well-known/spacebar/client") ''; }