{ config, lib, pkgs, ... }: let cfg = config.services.kotsukotsu; launcher = pkgs.writeShellScript "kotsukotsu-launch" '' exec ${cfg.currentPath}/bin/kotsukotsu ''; ensureCurrent = pkgs.writeShellScript "kotsukotsu-ensure-current" '' if [ ! -e ${cfg.currentPath} ]; then ln -sfn ${cfg.package} ${cfg.currentPath} fi ''; in { options.services.kotsukotsu = { enable = lib.mkEnableOption "kotsukotsu typing app"; package = lib.mkOption { type = lib.types.nullOr lib.types.package; default = null; description = "The kotsukotsu package to run, typically inputs.kotsukotsu.packages.${pkgs.system}.default."; }; host = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = "Listen address for the Bun server."; }; port = lib.mkOption { type = lib.types.port; default = 5174; description = "Listen port for the Bun server."; }; domain = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "typing.example.com"; description = "Public domain used for WebAuthn RP ID and cookie origin."; }; dataDir = lib.mkOption { type = lib.types.str; default = "/var/lib/kotsukotsu"; description = "Directory for the persistent SQLite database."; }; currentPath = lib.mkOption { type = lib.types.str; default = "/var/lib/kotsukotsu/current"; description = "Stable symlink path that points at the active app package."; }; user = lib.mkOption { type = lib.types.str; default = "kotsukotsu"; description = "User account for the service."; }; group = lib.mkOption { type = lib.types.str; default = "kotsukotsu"; description = "Group for the service."; }; secureCookies = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to mark auth cookies as Secure."; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.package != null; message = "services.kotsukotsu.package must be set, usually from this flake input."; } { assertion = cfg.domain != null && cfg.domain != ""; message = "services.kotsukotsu.domain must be set."; } { assertion = lib.hasPrefix "/" cfg.dataDir; message = "services.kotsukotsu.dataDir must be an absolute path."; } { assertion = lib.hasPrefix "/" cfg.currentPath; message = "services.kotsukotsu.currentPath must be an absolute path."; } ]; users.groups = lib.mkIf (cfg.group == "kotsukotsu") { kotsukotsu = { }; }; users.users = lib.mkIf (cfg.user == "kotsukotsu") { kotsukotsu = { isSystemUser = true; group = cfg.group; home = toString cfg.dataDir; createHome = false; }; }; systemd.tmpfiles.rules = [ "d ${toString cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -" ]; systemd.services.kotsukotsu = { description = "kotsukotsu typing app"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; environment = { APP_ORIGIN = "https://${cfg.domain}"; HOST = cfg.host; PORT = builtins.toString cfg.port; SESSION_COOKIE_SECURE = lib.boolToString cfg.secureCookies; SQLITE_PATH = "${cfg.dataDir}/leo-typing.db"; }; serviceConfig = { ExecStart = launcher; ExecStartPre = ensureCurrent; Group = cfg.group; Restart = "on-failure"; User = cfg.user; WorkingDirectory = cfg.dataDir; }; }; }; }