Something went wrong. Try again.
Paper Mario mod hosting site - AT Protocol appview starhaven.dev
appview
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564{ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
systems.url = "github:nix-systems/default";
rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; };
outputs = inputs@{ self, flake-parts, ... }: let # Must match the NSID of every lexicon in src/atproto/lexicon. Shared # between perSystem and the NixOS module below, which are otherwise # separate scopes. lexiconNsids = [ "dev.starhaven.mod.listing" "dev.starhaven.mod.version" "dev.starhaven.mod.star" ]; tapCollections = builtins.concatStringsSep "," lexiconNsids; # The one collection that makes tap follow an account: tap takes a single # exact NSID here, so anyone who has published a mod is followed, and # all of their records in tapCollections reach the server. tapSignalCollection = "dev.starhaven.mod.listing"; in flake-parts.lib.mkFlake { inherit inputs; } { systems = import inputs.systems; perSystem = { config, self', pkgs, lib, system, ... }: let rustToolchain = pkgs.rust-bin.stable.latest.default.override { extensions = [ "rust-src" "rust-analyzer" "clippy" ]; }; devToolchain = pkgs.rust-bin.selectLatestNightlyWith ( toolchain: toolchain.default.override { extensions = [ "rust-src" "rust-analyzer" "clippy" ] ++ lib.optionals useCranelift [ "rustc-codegen-cranelift-preview" ]; } ); useCranelift = pkgs.stdenv.hostPlatform.isx86_64 || pkgs.stdenv.hostPlatform.isAarch64; useMold = pkgs.stdenv.isLinux; rustBuildInputs = [ pkgs.openssl pkgs.libiconv pkgs.pkg-config ];
tap = pkgs.buildGoModule { pname = "tap"; version = "0.1.0"; src = pkgs.fetchFromGitHub { owner = "bluesky-social"; repo = "indigo"; rev = "cbaa83aee9dd4aa015fd0c245e1fb3cfbbe32817"; sha256 = "sha256-QQvkfNjsfU3vReyd8xB2Dtdqninyv5Zem9SuRVTdnK4="; }; subPackages = [ "cmd/tap" ]; vendorHash = "sha256-s1S+b+QbptqJ2mxqkvsn7M5VWfLrlwpWgRjg6lq2WVE="; doCheck = false; meta.mainProgram = "tap"; };
devPds = pkgs.buildNpmPackage { pname = "starhaven-dev-pds"; version = "0.1.0"; src = ./dev-pds; npmDepsHash = "sha256-W/lr/QDLBVFi2zT8+sZUBcTPuynOWTmRbuvVAfBejpY="; nodejs = pkgs.nodejs_22; dontNpmBuild = true; meta.mainProgram = "starhaven-dev-pds"; };
# Builds assets/lib/codemirror.js for the Markdown editor: CodeMirror # and the plugins it uses from codemirror-live-markdown. The plugins # come from that project's source because its npm package bundles in # math and code highlighting. The package build and the dev shell # copy the bundle into assets/lib; it isn't checked in. codemirror = let liveMarkdown = pkgs.fetchFromGitHub { owner = "blueberrycongee"; repo = "codemirror-live-markdown"; rev = "f1438ab14f4641b761af40ccf764653624c923f1"; hash = "sha256-/RTcJ4zsVvzeT25t+QKWhCa7bYy0bSiLy7du33IeIX4="; }; in pkgs.buildNpmPackage { pname = "starhaven-codemirror"; version = "0.1.0"; src = ./vendor/codemirror; npmDepsHash = "sha256-P7VGcYw5+w24RL6KC2zrxj+3NxmCd2TwXFAbZPMCkLg="; nodejs = pkgs.nodejs_22; nativeBuildInputs = [ pkgs.esbuild ]; postPatch = '' cp -r ${liveMarkdown} live-markdown chmod -R u+w live-markdown patch -d live-markdown -p1 < hide-mark-spaces.patch ''; buildPhase = '' runHook preBuild esbuild index.js --bundle --minify --format=esm --legal-comments=none \ --banner:js="/* CodeMirror (MIT, Marijn Haverbeke and others) and codemirror-live-markdown ${liveMarkdown.rev} (MIT, blueberrycongee). */" \ --outfile=codemirror.js runHook postBuild ''; installPhase = '' install -Dm644 codemirror.js $out/codemirror.js ''; };
# For tests/, a Playwright-driven E2E suite. pythonEnv = pkgs.python3.withPackages (ps: [ ps.pytest ps.pytest-playwright ps.playwright ps.requests ]); in { _module.args.pkgs = import inputs.nixpkgs { inherit system; overlays = [ inputs.rust-overlay.overlays.default ]; };
formatter = pkgs.nixfmt; packages = { # Exposed (unlike dev-pds) for the NixOS module's tap service. inherit tap;
default = let cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); rev = toString (self.shortRev or self.dirtyShortRev or self.lastModified or "unknown"); in pkgs.rustPlatform.buildRustPackage { pname = cargoToml.package.name; version = "${cargoToml.package.version}-${rev}"; src = ./.; strictDeps = true; buildInputs = rustBuildInputs; nativeBuildInputs = with pkgs; [ rustToolchain rustPlatform.bindgenHook sqlx-cli ] ++ rustBuildInputs; cargoLock.lockFile = ./Cargo.lock; preBuild = '' cp ${codemirror}/codemirror.js assets/lib/codemirror.js
export DATABASE_URL="sqlite:///tmp/sqlx-check.db" sqlx database create sqlx migrate run ''; meta.mainProgram = "server"; }; };
devShells.default = pkgs.mkShell { name = "starhaven-dev"; buildInputs = rustBuildInputs; nativeBuildInputs = [ devToolchain pkgs.sqlx-cli pkgs.just # For stylelint, which `just check` runs. pkgs.nodejs_22 pkgs.watchexec pythonEnv tap pkgs.seaweedfs pkgs.oxfmt ] ++ lib.optionals useMold [ pkgs.mold ]; shellHook = '' # Create and migrate the dev database so sqlx's query! macros # can verify SQL at compile time without a separate prepare step. export DATABASE_URL="sqlite:./database.db" sqlx database create sqlx migrate run
# Nix rebuilds this only when vendor/codemirror/ changes. install -m644 ${codemirror}/codemirror.js assets/lib/codemirror.js
# For rust-analyzer 'hover' tooltips to work. export RUST_SRC_PATH="${devToolchain}/lib/rustlib/src/rust/library";
# Browsers for tests/'s Playwright suite. export PLAYWRIGHT_BROWSERS_PATH="${pkgs.playwright-driver.browsers}"; export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1; # Chromium crashes loading a web font where fontconfig has no # config, as in CI's container, so the shell brings its own. export FONTCONFIG_FILE="${pkgs.makeFontsConf { fontDirectories = [ pkgs.dejavu_fonts ]; }}";
# Lets tests/conftest.py spawn its own throwaway PDS+PLC instances. export STARHAVEN_DEV_PDS_BIN="${devPds}/bin/starhaven-dev-pds"
# Local PDS + PLC directory runs in the background, keeping its # accounts in state/ across restarts. Delete that to start over. dev_pds_dir="$HOME/.local/share/starhaven-dev-pds" mkdir -p "$dev_pds_dir" dev_pds_pidfile="$dev_pds_dir/pds.pid" if ! kill -0 "$(cat "$dev_pds_pidfile" 2>/dev/null)" 2>/dev/null; then TMPDIR="$dev_pds_dir" DEV_PDS_STATE="$dev_pds_dir/state" \ ${devPds}/bin/starhaven-dev-pds &> "$dev_pds_dir/pds.log" 3>&- & echo $! > "$dev_pds_pidfile" disown fi
# tap, pointed at the local dev PDS/PLC rather than the real network. tap_pidfile=/tmp/starhaven-tap.pid if ! kill -0 "$(cat "$tap_pidfile" 2>/dev/null)" 2>/dev/null; then TAP_BIND=:2480 \ TAP_DATABASE_URL=sqlite:///tmp/starhaven-tap.db \ TAP_PLC_URL=http://localhost:2582 \ TAP_RELAY_URL=http://localhost:2583 \ TAP_SIGNAL_COLLECTION=${tapSignalCollection} \ TAP_COLLECTION_FILTERS=${tapCollections} \ TAP_NO_REPLAY=true \ ${tap}/bin/tap run &> /tmp/starhaven-tap.log 3>&- & echo $! > "$tap_pidfile" disown fi
# Object store for mod deliverables, on the endpoint config.rs # defaults to. Credentials match the loopback defaults there, # which is the only kind of endpoint they are accepted against. # # It reflects any Origin back on a CORS preflight, so browser # uploads work from a test appview's random port. A hosted # provider will not be this permissive - that the presigned # upload works here does not mean the bucket policy in # production is right. # # There's no anonymous identity, so like the production bucket, # objects can only be read through a signed URL. # # Each bucket takes volumes of its own, so they're kept small and # plentiful: the tests' bucket needs room beside the dev server's. seaweed_pidfile=/tmp/starhaven-seaweedfs.pid if ! kill -0 "$(cat "$seaweed_pidfile" 2>/dev/null)" 2>/dev/null; then mkdir -p /tmp/starhaven-dev-s3 cat > /tmp/starhaven-dev-s3/identities.json <<'IDENTITIES' { "identities": [ { "name": "starhaven", "credentials": [ { "accessKey": "starhaven", "secretKey": "starhaven-dev-secret" } ], "actions": ["Admin", "Read", "Write", "List", "Tagging"] } ] } IDENTITIES ${pkgs.seaweedfs}/bin/weed server \ -dir=/tmp/starhaven-dev-s3 \ -ip=127.0.0.1 \ -s3 -s3.port=9000 \ -master.volumeSizeLimitMB=256 \ -volume.max=100 \ -s3.config=/tmp/starhaven-dev-s3/identities.json \ &> /tmp/starhaven-seaweedfs.log 3>&- & echo $! > "$seaweed_pidfile" disown fi
# parallel frontend stops paying off after 8 threads rustc_threads=$(( $(nproc) < 8 ? $(nproc) : 8 ))
export RUSTFLAGS="${lib.optionalString useMold "-Clink-arg=-fuse-ld=mold "}-Zthreads=$rustc_threads" '' + lib.optionalString useCranelift '' export CARGO_UNSTABLE_CODEGEN_BACKEND=true export CARGO_PROFILE_DEV_CODEGEN_BACKEND=cranelift export CARGO_PROFILE_DEBUGGING_CODEGEN_BACKEND=llvm ''; }; };
flake.nixosModules.default = { config, lib, pkgs, ... }: let cfg = config.services.starhaven; stateDirectory = "starhaven"; in { imports = [ (lib.mkRemovedOptionModule [ "services" "starhaven" "s3" "publicBase" ] '' The deliverables bucket is private: downloads go through starhaven, which redirects to a short-lived signed URL, so there is no public base to set. '') ];
options.services.starhaven = { enable = lib.mkEnableOption "the starhaven.dev server";
package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.default; description = "The starhaven package to run."; };
externalBase = lib.mkOption { type = lib.types.str; example = "https://starhaven.dev"; description = "External base URL (with scheme)."; };
listen = lib.mkOption { type = lib.types.str; default = "127.0.0.1:3000"; description = "Address:port to listen on."; };
mail = { from = lib.mkOption { type = lib.types.str; default = "Star Haven <noreply@starhaven.dev>"; description = "Who email comes from."; };
smtpCredentialsFile = lib.mkOption { type = lib.types.path; description = '' Path to a JSON file holding `host`, `port`, `username` and `password` for the SMTP server email is sent through. ''; }; };
s3 = { endpoint = lib.mkOption { type = lib.types.str; example = "https://fsn1.your-objectstorage.com"; description = "Endpoint of the S3-compatible object store holding mod deliverables."; };
bucket = lib.mkOption { type = lib.types.str; description = "Bucket within the object store. Created on startup if missing."; };
region = lib.mkOption { type = lib.types.str; default = "us-east-1"; description = "Region to sign object store requests for."; };
virtualHost = lib.mkOption { type = lib.types.bool; default = true; description = '' Address the bucket as `bucket.endpoint/key` rather than `endpoint/bucket/key`. Most hosted providers want this. ''; };
credentialsFile = lib.mkOption { type = lib.types.path; description = '' Path to a JSON file holding `accessKey` and `secretKey` for the object store. A path rather than the credentials themselves, because ExecStart below is world-readable in the Nix store. ''; }; };
staffDid = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "did:plc:tjgdahiw3u2djgnigyqeummg"; description = '' The account whose mods can credit people by name rather than by account, for mods it lists on their makers' behalf. ''; };
signup = { pdsUrl = lib.mkOption { type = lib.types.str; example = "https://pds.starhaven.dev"; description = "Base URL of the PDS that sign-up creates accounts on."; };
pdsInternalUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "http://127.0.0.1:3000"; description = '' Base URL starhaven calls the sign-up PDS at, when it can reach it directly rather than through its public URL. The PDS must trust `X-Forwarded-For` from this connection, so it rate-limits each visitor rather than starhaven: Tranquil PDS on loopback does, but not through a reverse proxy that overwrites the header. Defaults to `pdsUrl`. ''; };
handleDomain = lib.mkOption { type = lib.types.str; example = "starhaven.dev"; description = "Domain that sign-up's handles are under."; };
pdsAdminCredentialsFile = lib.mkOption { type = lib.types.path; description = '' Path to a JSON file holding the `identifier` and `password` of an admin account on the sign-up PDS, which mints each new account's invite code. ''; };
turnstileCredentialsFile = lib.mkOption { type = lib.types.path; description = '' Path to a JSON file holding the `siteKey` and `secretKey` of the Cloudflare Turnstile widget sign-up shows. ''; }; };
tap = { enable = lib.mkOption { type = lib.types.bool; default = cfg.enable; description = "Whether to run the tap indexer. Defaults on with starhaven, since starhaven depends on it."; };
package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.tap; description = "The tap package to run."; };
bind = lib.mkOption { type = lib.types.str; default = "127.0.0.1:2480"; description = "Address:port for tap's HTTP/WebSocket server. starhaven connects to this same address."; };
adminPasswordFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "EnvironmentFile containing `TAP_ADMIN_PASSWORD=...`."; }; }; };
config = lib.mkIf cfg.enable { systemd.services.starhaven = { description = "starhaven.dev server"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ] ++ lib.optionals cfg.tap.enable [ "starhaven-tap.service" ]; wants = [ "network-online.target" ] ++ lib.optionals cfg.tap.enable [ "starhaven-tap.service" ];
serviceConfig = { ExecStart = lib.escapeShellArgs ( [ (lib.getExe cfg.package) "--external-base" cfg.externalBase "--listen" cfg.listen "--mail-from" cfg.mail.from "--smtp-credentials-file" (toString cfg.mail.smtpCredentialsFile) "--data-dir" "/var/lib/${stateDirectory}" "--s3-endpoint" cfg.s3.endpoint "--s3-bucket" cfg.s3.bucket "--s3-region" cfg.s3.region "--s3-credentials-file" (toString cfg.s3.credentialsFile) "--signup-pds-url" cfg.signup.pdsUrl "--signup-handle-domain" cfg.signup.handleDomain "--pds-admin-credentials-file" (toString cfg.signup.pdsAdminCredentialsFile) "--turnstile-credentials-file" (toString cfg.signup.turnstileCredentialsFile) ] ++ lib.optional cfg.s3.virtualHost "--s3-virtual-host" ++ lib.optionals (cfg.staffDid != null) [ "--staff-did" cfg.staffDid ] ++ lib.optionals (cfg.signup.pdsInternalUrl != null) [ "--signup-pds-internal-url" cfg.signup.pdsInternalUrl ] ++ lib.optionals cfg.tap.enable [ "--tap-hostname" cfg.tap.bind ] ); DynamicUser = true; StateDirectory = stateDirectory; Restart = "on-failure"; }; };
systemd.services.starhaven-tap = lib.mkIf cfg.tap.enable { description = "tap indexer for the starhaven.dev server"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ];
serviceConfig = { ExecStart = "${lib.getExe cfg.tap.package} run"; Environment = [ "TAP_BIND=${cfg.tap.bind}" "TAP_DATABASE_URL=sqlite:///var/lib/starhaven-tap/tap.db" "TAP_SIGNAL_COLLECTION=${tapSignalCollection}" "TAP_COLLECTION_FILTERS=${tapCollections}" ]; EnvironmentFile = lib.mkIf (cfg.tap.adminPasswordFile != null) cfg.tap.adminPasswordFile; DynamicUser = true; StateDirectory = "starhaven-tap"; Restart = "on-failure"; }; }; }; }; };}