From aa7439595f3278325f96b6c0b7906996741a5626 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Mon, 10 Nov 2025 13:26:59 +0200 Subject: [PATCH] nix/modules: add complete appview config paramaters Change-Id: xvuzrrtnyqmxmlmuytumkyqoyqwoxosl Secrets alone can be loaded from the environmentFile. Signed-off-by: Anirudh Oppiliappan --- nix/modules/appview.nix | 290 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 278 insertions(+), 12 deletions(-) diff --git a/nix/modules/appview.nix b/nix/modules/appview.nix index 031522dc..8b23f3ae 100644 --- a/nix/modules/appview.nix +++ b/nix/modules/appview.nix @@ -13,26 +13,235 @@ in default = false; description = "Enable tangled appview"; }; + package = mkOption { type = types.package; description = "Package to use for the appview"; }; + + # core configuration port = mkOption { - type = types.int; + type = types.port; default = 3000; description = "Port to run the appview on"; }; + + listenAddr = mkOption { + type = types.str; + default = "0.0.0.0:${toString cfg.port}"; + description = "Listen address for the appview service"; + }; + + dbPath = mkOption { + type = types.str; + default = "/var/lib/appview/appview.db"; + description = "Path to the SQLite database file"; + }; + + appviewHost = mkOption { + type = types.str; + default = "https://tangled.org"; + example = "https://example.com"; + description = "Public host URL for the appview instance"; + }; + + appviewName = mkOption { + type = types.str; + default = "Tangled"; + description = "Display name for the appview instance"; + }; + + dev = mkOption { + type = types.bool; + default = false; + description = "Enable development mode"; + }; + + disallowedNicknamesFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to file containing disallowed nicknames"; + }; + + # redis configuration + redis = { + addr = mkOption { + type = types.str; + default = "localhost:6379"; + description = "Redis server address"; + }; + + db = mkOption { + type = types.int; + default = 0; + description = "Redis database number"; + }; + }; + + # jetstream configuration + jetstream = { + endpoint = mkOption { + type = types.str; + default = "wss://jetstream1.us-east.bsky.network/subscribe"; + description = "Jetstream WebSocket endpoint"; + }; + }; + + # knotstream consumer configuration + knotstream = { + retryInterval = mkOption { + type = types.str; + default = "60s"; + description = "Initial retry interval for knotstream consumer"; + }; + + maxRetryInterval = mkOption { + type = types.str; + default = "120m"; + description = "Maximum retry interval for knotstream consumer"; + }; + + connectionTimeout = mkOption { + type = types.str; + default = "5s"; + description = "Connection timeout for knotstream consumer"; + }; + + workerCount = mkOption { + type = types.int; + default = 64; + description = "Number of workers for knotstream consumer"; + }; + + queueSize = mkOption { + type = types.int; + default = 100; + description = "Queue size for knotstream consumer"; + }; + }; + + # spindlestream consumer configuration + spindlestream = { + retryInterval = mkOption { + type = types.str; + default = "60s"; + description = "Initial retry interval for spindlestream consumer"; + }; + + maxRetryInterval = mkOption { + type = types.str; + default = "120m"; + description = "Maximum retry interval for spindlestream consumer"; + }; + + connectionTimeout = mkOption { + type = types.str; + default = "5s"; + description = "Connection timeout for spindlestream consumer"; + }; + + workerCount = mkOption { + type = types.int; + default = 64; + description = "Number of workers for spindlestream consumer"; + }; + + queueSize = mkOption { + type = types.int; + default = 100; + description = "Queue size for spindlestream consumer"; + }; + }; + + # resend configuration + resend = { + sentFrom = mkOption { + type = types.str; + default = "noreply@notifs.tangled.sh"; + description = "Email address to send notifications from"; + }; + }; + + # posthog configuration + posthog = { + endpoint = mkOption { + type = types.str; + default = "https://eu.i.posthog.com"; + description = "PostHog API endpoint"; + }; + }; + + # camo configuration + camo = { + host = mkOption { + type = types.str; + default = "https://camo.tangled.sh"; + description = "Camo proxy host URL"; + }; + }; + + # avatar configuration + avatar = { + host = mkOption { + type = types.str; + default = "https://avatar.tangled.sh"; + description = "Avatar service host URL"; + }; + }; + + plc = { + url = mkOption { + type = types.str; + default = "https://plc.directory"; + description = "PLC directory URL"; + }; + }; + + pds = { + host = mkOption { + type = types.str; + default = "https://tngl.sh"; + description = "PDS host URL"; + }; + }; + + label = { + defaults = mkOption { + type = types.listOf types.str; + default = [ + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/wontfix" + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue" + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/duplicate" + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/documentation" + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/assignee" + ]; + description = "Default label definitions"; + }; + + goodFirstIssue = mkOption { + type = types.str; + default = "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue"; + description = "Good first issue label definition"; + }; + }; + environmentFile = mkOption { type = with types; nullOr path; default = null; - example = "/etc-/appview.env"; + example = "/etc/appview.env"; description = '' Additional environment file as defined in {manpage}`systemd.exec(5)`. - Sensitive secrets such as {env}`TANGLED_COOKIE_SECRET` may be - passed to the service without makeing them world readable in the - nix store. - + Sensitive secrets such as {env}`TANGLED_COOKIE_SECRET`, + {env}`TANGLED_OAUTH_CLIENT_SECRET`, {env}`TANGLED_RESEND_API_KEY`, + {env}`TANGLED_CAMO_SHARED_SECRET`, {env}`TANGLED_AVATAR_SHARED_SECRET`, + {env}`TANGLED_REDIS_PASS`, {env}`TANGLED_PDS_ADMIN_SECRET`, + {env}`TANGLED_CLOUDFLARE_API_TOKEN`, {env}`TANGLED_CLOUDFLARE_ZONE_ID`, + {env}`TANGLED_CLOUDFLARE_TURNSTILE_SITE_KEY`, + {env}`TANGLED_CLOUDFLARE_TURNSTILE_SECRET_KEY`, + {env}`TANGLED_POSTHOG_API_KEY`, {env}`TANGLED_APP_PASSWORD`, + and {env}`TANGLED_ALT_APP_PASSWORD` may be passed to the service + without making them world readable in the nix store. ''; }; }; @@ -47,19 +256,76 @@ in systemd.services.appview = { description = "tangled appview service"; wantedBy = ["multi-user.target"]; - after = ["redis-appview.service"]; + after = ["redis-appview.service" "network-online.target"]; requires = ["redis-appview.service"]; + wants = ["network-online.target"]; serviceConfig = { - ListenStream = "0.0.0.0:${toString cfg.port}"; + Type = "simple"; ExecStart = "${cfg.package}/bin/appview"; Restart = "always"; - EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; - }; + RestartSec = "10s"; + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; - environment = { - TANGLED_DB_PATH = "appview.db"; + # state directory + StateDirectory = "appview"; + WorkingDirectory = "/var/lib/appview"; + + # security hardening + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = ["/var/lib/appview"]; }; + + environment = + { + TANGLED_DB_PATH = cfg.dbPath; + TANGLED_LISTEN_ADDR = cfg.listenAddr; + TANGLED_APPVIEW_HOST = cfg.appviewHost; + TANGLED_APPVIEW_NAME = cfg.appviewName; + TANGLED_DEV = + if cfg.dev + then "true" + else "false"; + } + // optionalAttrs (cfg.disallowedNicknamesFile != null) { + TANGLED_DISALLOWED_NICKNAMES_FILE = cfg.disallowedNicknamesFile; + } + // { + TANGLED_REDIS_ADDR = cfg.redis.addr; + TANGLED_REDIS_DB = toString cfg.redis.db; + + TANGLED_JETSTREAM_ENDPOINT = cfg.jetstream.endpoint; + + TANGLED_KNOTSTREAM_RETRY_INTERVAL = cfg.knotstream.retryInterval; + TANGLED_KNOTSTREAM_MAX_RETRY_INTERVAL = cfg.knotstream.maxRetryInterval; + TANGLED_KNOTSTREAM_CONNECTION_TIMEOUT = cfg.knotstream.connectionTimeout; + TANGLED_KNOTSTREAM_WORKER_COUNT = toString cfg.knotstream.workerCount; + TANGLED_KNOTSTREAM_QUEUE_SIZE = toString cfg.knotstream.queueSize; + + TANGLED_SPINDLESTREAM_RETRY_INTERVAL = cfg.spindlestream.retryInterval; + TANGLED_SPINDLESTREAM_MAX_RETRY_INTERVAL = cfg.spindlestream.maxRetryInterval; + TANGLED_SPINDLESTREAM_CONNECTION_TIMEOUT = cfg.spindlestream.connectionTimeout; + TANGLED_SPINDLESTREAM_WORKER_COUNT = toString cfg.spindlestream.workerCount; + TANGLED_SPINDLESTREAM_QUEUE_SIZE = toString cfg.spindlestream.queueSize; + + TANGLED_RESEND_SENT_FROM = cfg.resend.sentFrom; + + TANGLED_POSTHOG_ENDPOINT = cfg.posthog.endpoint; + + TANGLED_CAMO_HOST = cfg.camo.host; + + TANGLED_AVATAR_HOST = cfg.avatar.host; + + TANGLED_PLC_URL = cfg.plc.url; + + TANGLED_PDS_HOST = cfg.pds.host; + + TANGLED_LABEL_DEFAULTS = concatStringsSep "," cfg.label.defaults; + TANGLED_LABEL_GFI = cfg.label.goodFirstIssue; + }; }; }; } -- 2.43.0