From e025b8c8e1a9d25c3c740407c42d8dec1381a37d Mon Sep 17 00:00:00 2001 From: Shreyas Date: Mon, 15 Dec 2025 11:54:37 +0530 Subject: [PATCH] perf(webapp-server): opt for build over run time (#5644) Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com> --- devenv.lock | 28 +- devenv.nix | 2 +- packages/hoppscotch-selfhost-web/.gitignore | 6 +- .../webapp-server/.gitignore | 24 +- .../webapp-server/Cargo.lock | 1925 ----------------- .../webapp-server/Cargo.toml | 26 - .../webapp-server/README.md | 104 +- .../webapp-server/go.mod | 15 + .../webapp-server/go.sum | 12 + .../webapp-server/internal/bundle/builder.go | 189 ++ .../webapp-server/internal/bundle/manager.go | 73 + .../webapp-server/internal/bundle/types.go | 36 + .../webapp-server/internal/config/config.go | 50 + .../webapp-server/internal/crypto/keys.go | 227 ++ .../webapp-server/internal/server/server.go | 146 ++ .../webapp-server/main.go | 98 + .../webapp-server/src/api/error.rs | 46 - .../webapp-server/src/api/handler.rs | 83 - .../webapp-server/src/api/mod.rs | 46 - .../webapp-server/src/api/model.rs | 46 - .../webapp-server/src/bundle/builder.rs | 109 - .../webapp-server/src/bundle/error.rs | 36 - .../webapp-server/src/bundle/manager.rs | 68 - .../webapp-server/src/bundle/mod.rs | 8 - .../webapp-server/src/bundle/model.rs | 58 - .../webapp-server/src/config.rs | 88 - .../webapp-server/src/error.rs | 125 -- .../webapp-server/src/main.rs | 69 - .../webapp-server/src/model.rs | 42 - .../webapp-server/src/signing/error.rs | 51 - .../webapp-server/src/signing/key.rs | 37 - .../webapp-server/src/signing/mod.rs | 5 - prod.Dockerfile | 31 +- 33 files changed, 977 insertions(+), 2932 deletions(-) delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/Cargo.lock delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/Cargo.toml create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/go.mod create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/go.sum create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/builder.go create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/manager.go create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/types.go create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/internal/config/config.go create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/internal/crypto/keys.go create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/internal/server/server.go create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/main.go delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/error.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/handler.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/mod.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/model.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/builder.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/error.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/manager.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/mod.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/model.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/config.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/error.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/main.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/model.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/signing/error.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/signing/key.rs delete mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/signing/mod.rs diff --git a/devenv.lock b/devenv.lock index 1307a977..ba163cb5 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1761922975, + "lastModified": 1764669403, "owner": "cachix", "repo": "devenv", - "rev": "c9f0b47815a4895fadac87812de8a4de27e0ace1", + "rev": "3f2d25e7af748127da0571266054575dd8fec5ab", "type": "github" }, "original": { @@ -24,10 +24,10 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1762238689, + "lastModified": 1764658058, "owner": "nix-community", "repo": "fenix", - "rev": "0f94d1e67ea9ef983a9b5caf9c14bc52ae2eac44", + "rev": "12bd9c7bcbeb949741b3ad0ca2b3506d0718cf4d", "type": "github" }, "original": { @@ -60,10 +60,10 @@ ] }, "locked": { - "lastModified": 1760663237, + "lastModified": 1763988335, "owner": "cachix", "repo": "git-hooks.nix", - "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", + "rev": "50b9238891e388c9fdc6a5c49e49c42533a1b5ce", "type": "github" }, "original": { @@ -80,10 +80,10 @@ ] }, "locked": { - "lastModified": 1709087332, + "lastModified": 1762808025, "owner": "hercules-ci", "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", "type": "github" }, "original": { @@ -94,10 +94,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1762156382, + "lastModified": 1764611609, "owner": "NixOS", "repo": "nixpkgs", - "rev": "7241bcbb4f099a66aafca120d37c65e8dda32717", + "rev": "8c29968b3a942f2903f90797f9623737c215737c", "type": "github" }, "original": { @@ -122,10 +122,10 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1762201112, + "lastModified": 1764603480, "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "132d3338f4526b5c71046e5dc7ddf800e279daf4", + "rev": "f25db5500baa047106d74962fe361ea59ce6f91e", "type": "github" }, "original": { @@ -142,10 +142,10 @@ ] }, "locked": { - "lastModified": 1762223900, + "lastModified": 1764643237, "owner": "oxalica", "repo": "rust-overlay", - "rev": "cfe1598d69a42a5edb204770e71b8df77efef2c3", + "rev": "e66d6b924ac59e6c722f69332f6540ea57c69233", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index f9ff690b..cbf44fe7 100644 --- a/devenv.nix +++ b/devenv.nix @@ -64,7 +64,7 @@ in { e.exec = "emacs"; lima-setup.exec = "limactl start template://docker"; lima-clean.exec = "limactl rm -f $(limactl ls -q)"; - colima-start.exec = "colima start --cpu 4 --memory 50"; + colima-start.exec = "colima start --cpu 8 --memory 50"; docker-prune.exec = '' echo "Cleaning up unused Docker resources..." diff --git a/packages/hoppscotch-selfhost-web/.gitignore b/packages/hoppscotch-selfhost-web/.gitignore index 2203746a..4f088bd3 100644 --- a/packages/hoppscotch-selfhost-web/.gitignore +++ b/packages/hoppscotch-selfhost-web/.gitignore @@ -27,4 +27,8 @@ dist-ssr .sitemap-gen # Backend Code generation -src/api/generated \ No newline at end of file +src/api/generated + +# webapp-server +webapp-server/webapp-server +webapp-server/.webapp-server diff --git a/packages/hoppscotch-selfhost-web/webapp-server/.gitignore b/packages/hoppscotch-selfhost-web/webapp-server/.gitignore index 6cf4d681..9a18c980 100644 --- a/packages/hoppscotch-selfhost-web/webapp-server/.gitignore +++ b/packages/hoppscotch-selfhost-web/webapp-server/.gitignore @@ -1,21 +1,5 @@ -# Devenv -.devenv* -devenv.local.nix +# compiled binary +webapp-server -# direnv -.direnv - -# pre-commit -.pre-commit-config.yaml - -/target/ - -/gen/schemas - -.env - -bundles - -trust/ - -site/ +# dev mode key directory +.webapp-server/ diff --git a/packages/hoppscotch-selfhost-web/webapp-server/Cargo.lock b/packages/hoppscotch-selfhost-web/webapp-server/Cargo.lock deleted file mode 100644 index e591515e..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/Cargo.lock +++ /dev/null @@ -1,1925 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-compression" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" -dependencies = [ - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "zstd", - "zstd-safe", -] - -[[package]] -name = "async-trait" -version = "0.1.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "axum" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 1.0.1", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.1", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "blake3" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "serde", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" - -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "cc" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-targets", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "serde", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "flate2" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "hashbrown" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" - -[[package]] -name = "httparse" -version = "1.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.162" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "libc", - "wasi", - "windows-sys", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" - -[[package]] -name = "serde" -version = "1.0.215" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.215" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.133" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "rand_core", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" -dependencies = [ - "thiserror-impl 2.0.3", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "tokio" -version = "1.41.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-util" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 0.1.2", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" -dependencies = [ - "async-compression", - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicase" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" - -[[package]] -name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "webapp-server" -version = "0.1.0" -dependencies = [ - "axum", - "base64", - "blake3", - "bytes", - "chrono", - "ed25519-dalek", - "mime_guess", - "rand", - "rayon", - "serde", - "serde_json", - "thiserror 2.0.3", - "tokio", - "tower-http", - "tracing", - "tracing-subscriber", - "walkdir", - "zip", - "zstd", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zip" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "hmac", - "indexmap", - "lzma-rs", - "memchr", - "pbkdf2", - "rand", - "sha1", - "thiserror 1.0.69", - "time", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zopfli" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" -dependencies = [ - "bumpalo", - "crc32fast", - "lockfree-object-pool", - "log", - "once_cell", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/packages/hoppscotch-selfhost-web/webapp-server/Cargo.toml b/packages/hoppscotch-selfhost-web/webapp-server/Cargo.toml deleted file mode 100644 index 09c38793..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "webapp-server" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -axum = { version = "0.7" } -base64 = "0.22.1" -blake3 = { version = "1.5.4", features = ["serde"] } -bytes = "1.8.0" -chrono = { version = "0.4.38", features = ["serde"] } -ed25519-dalek = { version = "2.1.1", features = ["rand_core", "serde"] } -mime_guess = "2.0.5" -rand = "0.8.5" -rayon = "1.10.0" -serde = { version = "1.0.215", features = ["derive"] } -serde_json = "1.0.133" -thiserror = "2.0.3" -tokio = { version = "1.0", features = ["full"] } -tower-http = { version = "0.6", features = ["compression-zstd", "fs", "trace"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -walkdir = "2.5.0" -zip = "2.2.0" -zstd = "0.13.2" diff --git a/packages/hoppscotch-selfhost-web/webapp-server/README.md b/packages/hoppscotch-selfhost-web/webapp-server/README.md index 277e0034..06a2c614 100644 --- a/packages/hoppscotch-selfhost-web/webapp-server/README.md +++ b/packages/hoppscotch-selfhost-web/webapp-server/README.md @@ -1,33 +1,105 @@ -# Hoppscotch Webapp Server +# Hoppscotch Webapp Server (Go) -A secure static web server for Hoppscotch Webapp with content bundling (`zstd` + `zip`) and verification (`blake3` + `ed25519`). +Static web server for Hoppscotch Webapp with content bundling (zstd + zip) and verification (blake3 + ed25519). ## Quick Start ```bash -# Build from source -cargo build --release +go build -o webapp-server . +GO_ENV=development ./webapp-server +``` -# or use Docker +or with Docker +```bash docker build -t hoppscotch-webapp-server . ``` -> [!note] -> Configuration via environment variables: -> - `FRONTEND_PATH`: UI assets build location -> - `DEFAULT_PORT`: Server port (default: 3200) +## Configuration + +| Variable | Description | Default | +|----------------------------------|------------------------------------|------------------------------------------------| +| `WEBAPP_SERVER_PORT` | Server port | `3200` | +| `FRONTEND_PATH` | Path to frontend assets | `/site/selfhost-web` (prod) or `../dist` (dev) | +| `WEBAPP_SERVER_SIGNING_SECRET` | Secret string for key derivation | None | +| `WEBAPP_SERVER_SIGNING_SEED` | Base64 encoded 32-byte seed | None | +| `WEBAPP_SERVER_SIGNING_KEY` | Base64 encoded 64-byte private key | None | +| `WEBAPP_SERVER_SIGNING_KEY_FILE` | Custom path for key file | `/data/webapp-server/signing.key` | +| `GO_ENV` | Set to `development` for dev mode | None | + +## Signing Key Persistence + +The server needs a stable signing key. Without one, users get "Invalid signature" errors when they have cached bundles from a previous server instance. Keys are resolved in order: + +1. Environment variable: `WEBAPP_SERVER_SIGNING_KEY`, `WEBAPP_SERVER_SIGNING_SEED`, or `WEBAPP_SERVER_SIGNING_SECRET` +2. Key file on disk at `/data/webapp-server/signing.key` +3. Auto-generate and persist to disk +4. Ephemeral fallback (logs the key for manual config) + +For Kubernetes, either mount a persistent volume at `/data/webapp-server` or set `WEBAPP_SERVER_SIGNING_SECRET` to the same value across replicas. + +If the server can't persist to disk, it logs the generated key: + +``` +======================================== +SIGNING KEY PERSISTENCE FAILED +======================================== +Could not save signing key to: /data/webapp-server/signing.key + +To persist this key, set this environment variable: + + WEBAPP_SERVER_SIGNING_KEY= + +Or mount a persistent volume at: + /data/webapp-server +======================================== +``` + +Copy the logged key value and set it as an environment variable before the next restart. ## API Endpoints -- Health check: `GET /health` -- Bundle manifest: `GET /api/v1/manifest` -- Download bundle: `GET /api/v1/bundle` -- Public key info: `GET /api/v1/key` +| Endpoint | Description | +|------------------------|------------------------------------------------| +| `GET /health` | Health check | +| `GET /api/v1/manifest` | Bundle metadata with file hashes and signature | +| `GET /api/v1/bundle` | Download signed ZIP bundle | +| `GET /api/v1/key` | Public verification key | + +All endpoints also available under `/desktop-app-server/` prefix for desktop app compatibility. + +## Architecture + +``` +Frontend files → zstd ZIP → BLAKE3 per file → ED25519 sign → HTTP serve + ↓ + Manifest JSON + (paths, sizes, hashes, MIME types) +``` + +## Bundle Format + +| Component | Method | +|----------------|--------------------------| +| Compression | zstd (ZIP method 93) | +| File hashing | BLAKE3 (base64) | +| Bundle signing | ED25519 over ZIP content | + +## Troubleshooting + +"Invalid signature" after restart: Server generated a new key because persistence wasn't configured. Mount a volume at `/data/webapp-server` or set `WEBAPP_SERVER_SIGNING_SECRET`. + +"Invalid signature" with multiple replicas: Each replica has a different key. Use env var config with the same secret across all replicas. + +Key file permission errors: Container can't write to `/data/webapp-server`. Make it writable or use env var config. ## Development ```bash -cargo watch -x run # Dev build with hot reload -cargo test # Run tests -cargo build --release # Production build +GO_ENV=development go run . +go test ./... +CGO_ENABLED=0 GOOS=linux go build -o webapp-server . ``` + +## Migration from Rust Version + +Full API and bundle format compatibility with the Rust version. Same ZIP structure, same BLAKE3 hashing, same ED25519 signatures, identical API responses. New feature is automatic signing key persistence. diff --git a/packages/hoppscotch-selfhost-web/webapp-server/go.mod b/packages/hoppscotch-selfhost-web/webapp-server/go.mod new file mode 100644 index 00000000..25e5143c --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/go.mod @@ -0,0 +1,15 @@ +module hoppscotch-selfhost-web/webapp-server + +go 1.24.0 + +toolchain go1.24.1 + +require ( + github.com/klauspost/compress v1.18.0 + github.com/zeebo/blake3 v0.2.4 +) + +require ( + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + golang.org/x/sys v0.36.0 // indirect +) diff --git a/packages/hoppscotch-selfhost-web/webapp-server/go.sum b/packages/hoppscotch-selfhost-web/webapp-server/go.sum new file mode 100644 index 00000000..1835ff19 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/go.sum @@ -0,0 +1,12 @@ +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/builder.go b/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/builder.go new file mode 100644 index 00000000..dd1a16e1 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/builder.go @@ -0,0 +1,189 @@ +// Package bundle handles creating and managing frontend bundles. +// +// Bundles are zstd-compressed ZIP archives with blake3 hashes per file +// and an ed25519 signature over the whole thing. + +package bundle + +import ( + "archive/zip" + "bytes" + "encoding/base64" + "fmt" + "io" + "log" + "mime" + "os" + "path/filepath" + "strings" + + "github.com/klauspost/compress/zstd" + "github.com/zeebo/blake3" +) + +// Builder walks frontend files and packs them into a signed bundle +type Builder struct{} + +func NewBuilder() (*Builder, error) { + return &Builder{}, nil +} + +func init() { + // zstd is ZIP method 93 + // see: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + zip.RegisterCompressor(ZipMethodZstd, func(w io.Writer) (io.WriteCloser, error) { + return zstd.NewWriter(w) + }) + + // register decompressor for ZIP validation in manager.go + zip.RegisterDecompressor(ZipMethodZstd, func(r io.Reader) io.ReadCloser { + decoder, err := zstd.NewReader(r) + if err != nil { + // return a reader that errors on read + return errReadCloser{err} + } + return decoder.IOReadCloser() + }) +} + +// errReadCloser is a ReadCloser that always returns an error on Read. +type errReadCloser struct { + err error +} + +func (e errReadCloser) Read(p []byte) (int, error) { + return 0, e.err +} + +func (e errReadCloser) Close() error { + return nil +} + +// Build walks frontendPath and creates a zstd-compressed ZIP. +// Returns the raw bytes, file metadata, and any error. +// +// NOTE: compression happens at the ZIP level (each file is zstd'd individually), +// matching the Rust implementation's approach. This plays nice with partial +// downloads if we ever want to support range requests. +func (b *Builder) Build(frontendPath string) ([]byte, []FileEntry, error) { + if _, err := os.Stat(frontendPath); os.IsNotExist(err) { + return nil, nil, fmt.Errorf("frontend path does not exist: %s", frontendPath) + } + + var buf bytes.Buffer + zipWriter := zip.NewWriter(&buf) + + var files []FileEntry + var fileCount int + + err := filepath.Walk(frontendPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error accessing %s: %w", path, err) + } + if info.IsDir() { + return nil + } + + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", path, err) + } + + relPath, err := filepath.Rel(frontendPath, path) + if err != nil { + return fmt.Errorf("failed to compute relative path for %s: %w", path, err) + } + + // normalize to forward slashes for cross-platform compat + normalizedPath := filepath.ToSlash(relPath) + + header := &zip.FileHeader{ + Name: normalizedPath, + Method: ZipMethodZstd, + } + header.SetMode(0644) + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return fmt.Errorf("failed to create ZIP entry for %s: %w", relPath, err) + } + + if _, err := writer.Write(content); err != nil { + return fmt.Errorf("failed to write file %s to ZIP: %w", relPath, err) + } + + // blake3 for file integrity checks + hasher := blake3.New() + hasher.Write(content) + hash := hasher.Sum(nil) + + mimeType := detectMimeType(path) + + files = append(files, FileEntry{ + Path: normalizedPath, + Size: info.Size(), + Hash: base64.StdEncoding.EncodeToString(hash), + MimeType: mimeType, + }) + + fileCount++ + return nil + }) + + if err != nil { + return nil, nil, err + } + + if err := zipWriter.Close(); err != nil { + return nil, nil, fmt.Errorf("failed to finalize ZIP archive: %w", err) + } + + log.Printf("Built bundle with %d files (%d bytes)", fileCount, buf.Len()) + return buf.Bytes(), files, nil +} + +// detectMimeType guesses MIME type from extension. +// Returns nil if unknown (matches Rust's Option behavior). +func detectMimeType(path string) *string { + ext := filepath.Ext(path) + if ext == "" { + return nil + } + + // try Go's builtin mime registry first + mimeType := mime.TypeByExtension(ext) + if mimeType != "" { + // strip params like "; charset=utf-8" + if idx := strings.Index(mimeType, ";"); idx != -1 { + mimeType = strings.TrimSpace(mimeType[:idx]) + } + return &mimeType + } + + // handle web-specific types Go doesn't know about + switch strings.ToLower(ext) { + case ".wasm": + m := "application/wasm" + return &m + case ".mjs": + m := "application/javascript" + return &m + case ".tsx", ".ts": + m := "application/typescript" + return &m + case ".vue": + m := "application/vue" + return &m + case ".svelte": + m := "application/svelte" + return &m + case ".json5": + m := "application/json5" + return &m + case ".webmanifest": + m := "application/manifest+json" + return &m + } + + return nil +} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/manager.go b/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/manager.go new file mode 100644 index 00000000..11c39fd0 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/manager.go @@ -0,0 +1,73 @@ +package bundle + +import ( + "archive/zip" + "bytes" + "crypto/ed25519" + "encoding/base64" + "fmt" + "log" + "sync" + "time" +) + +// Manager holds the bundle in memory and handles signing. +// Thread-safe for concurrent reads (writes only happen at startup). +type Manager struct { + mu sync.RWMutex + bundle *Bundle + maxSize int + signingKey ed25519.PrivateKey + verifyingKey ed25519.PublicKey +} + +// NewManager creates a manager with a pre-built bundle. +// Signs the bundle content immediately so it's ready to serve. +func NewManager( + content []byte, + files []FileEntry, + signingKey ed25519.PrivateKey, + verifyingKey ed25519.PublicKey, + maxSize int, +) (*Manager, error) { + if len(content) > maxSize { + return nil, fmt.Errorf("bundle too large: %d bytes (max: %d)", len(content), maxSize) + } + + // sanity check that we actually have a valid zip + if _, err := zip.NewReader(bytes.NewReader(content), int64(len(content))); err != nil { + return nil, fmt.Errorf("invalid zip archive: %w", err) + } + + // sign the raw bytes, clients will verify against this + signature := ed25519.Sign(signingKey, content) + + bundle := &Bundle{ + Metadata: Metadata{ + Version: Version, + CreatedAt: time.Now().UTC(), + Signature: base64.StdEncoding.EncodeToString(signature), + Manifest: Manifest{Files: files}, + }, + Content: content, + } + + log.Println("Bundle signed and stored successfully") + + return &Manager{ + bundle: bundle, + maxSize: maxSize, + signingKey: signingKey, + verifyingKey: verifyingKey, + }, nil +} + +func (m *Manager) GetBundle() *Bundle { + m.mu.RLock() + defer m.mu.RUnlock() + return m.bundle +} + +func (m *Manager) GetVerifyingKey() ed25519.PublicKey { + return m.verifyingKey +} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/types.go b/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/types.go new file mode 100644 index 00000000..d18043fa --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/internal/bundle/types.go @@ -0,0 +1,36 @@ +package bundle + +import "time" + +const ( + Version = "2025.12.0" + + DefaultMaxSize = 50 * 1024 * 1024 + + // zstd compression method for ZIP + // see: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + ZipMethodZstd = 93 +) + +type FileEntry struct { + Path string `json:"path"` + Size int64 `json:"size"` + Hash string `json:"hash"` // blake3, base64 encoded + MimeType *string `json:"mime_type"` // nil if unknown +} + +type Manifest struct { + Files []FileEntry `json:"files"` +} + +type Metadata struct { + Version string `json:"version"` + CreatedAt time.Time `json:"created_at"` + Signature string `json:"signature"` // ed25519 over Content, base64 encoded + Manifest Manifest `json:"manifest"` +} + +type Bundle struct { + Metadata Metadata + Content []byte // raw ZIP bytes +} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/internal/config/config.go b/packages/hoppscotch-selfhost-web/webapp-server/internal/config/config.go new file mode 100644 index 00000000..8d4dafa9 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/internal/config/config.go @@ -0,0 +1,50 @@ +package config + +import ( + "log" + "os" + "strconv" +) + +const ( + DefaultPort = 3200 + DefaultFrontendPath = "/site/selfhost-web" + DevFrontendPath = "../dist" +) + +type Config struct { + Port int + FrontendPath string +} + +// Load reads config from env vars with sensible defaults +func Load() *Config { + cfg := &Config{ + Port: DefaultPort, + } + + if portStr := os.Getenv("WEBAPP_SERVER_PORT"); portStr != "" { + if port, err := strconv.Atoi(portStr); err == nil { + cfg.Port = port + log.Printf("Using WEBAPP_SERVER_PORT from environment: %d", port) + } else { + log.Printf("Warning: Invalid WEBAPP_SERVER_PORT value '%s', using default %d", portStr, DefaultPort) + } + } else { + log.Printf("Using default port: %d", DefaultPort) + } + + // NOTE: env var takes priority, then we check GO_ENV for dev mode + if frontendPath := os.Getenv("FRONTEND_PATH"); frontendPath != "" { + cfg.FrontendPath = frontendPath + log.Printf("Using FRONTEND_PATH from environment: %s", frontendPath) + } else if os.Getenv("GO_ENV") == "development" { + cfg.FrontendPath = DevFrontendPath + log.Println("Running in development mode, using frontend path: ../dist") + } else { + cfg.FrontendPath = DefaultFrontendPath + log.Println("Running in production mode, using frontend path: /site/selfhost-web") + } + + return cfg +} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/internal/crypto/keys.go b/packages/hoppscotch-selfhost-web/webapp-server/internal/crypto/keys.go new file mode 100644 index 00000000..f696ec5f --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/internal/crypto/keys.go @@ -0,0 +1,227 @@ +// Package crypto handles ed25519 key generation and persistence. +// +// Key sources (in priority order): +// 1. WEBAPP_SERVER_SIGNING_KEY: full 64-byte private key, base64 +// 2. WEBAPP_SERVER_SIGNING_SEED: 32-byte seed, base64 +// 3. WEBAPP_SERVER_SIGNING_SECRET: any string (SHA-256 derived) +// 4. Key file on disk +// 5. Generate new and try to persist +// +// For production, either mount a volume at /data/webapp-server +// or set one of the WEBAPP_SERVER_SIGNING_* env vars. + +package crypto + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "log" + "os" + "path/filepath" +) + +const ( + DefaultKeyFileName = "signing.key" + DefaultKeyDir = "/data/webapp-server" + DevKeyDir = ".webapp-server" +) + +type KeyPair struct { + SigningKey ed25519.PrivateKey + VerifyingKey ed25519.PublicKey +} + +// GenerateKeyPair gets or creates an ed25519 key pair. +// Tries env vars first, then disk, then generates new. +func GenerateKeyPair() (*KeyPair, error) { + // try env vars first (explicit config always wins) + if keyB64 := os.Getenv("WEBAPP_SERVER_SIGNING_KEY"); keyB64 != "" { + return loadFromBase64Key(keyB64) + } + + if seedB64 := os.Getenv("WEBAPP_SERVER_SIGNING_SEED"); seedB64 != "" { + return loadFromBase64Seed(seedB64) + } + + if secret := os.Getenv("WEBAPP_SERVER_SIGNING_SECRET"); secret != "" { + return deriveFromSecret(secret) + } + + // try loading from disk + keyPath := getKeyFilePath() + if kp, err := loadFromFile(keyPath); err == nil { + return kp, nil + } + + // nothing found, generate fresh and try to persist + return generateAndPersist(keyPath) +} + +func getKeyFilePath() string { + if path := os.Getenv("WEBAPP_SERVER_SIGNING_KEY_FILE"); path != "" { + return path + } + + var keyDir string + if isDevMode() { + keyDir = DevKeyDir + } else { + keyDir = DefaultKeyDir + } + + return filepath.Join(keyDir, DefaultKeyFileName) +} + +func loadFromFile(path string) (*KeyPair, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + keyBytes, err := base64.StdEncoding.DecodeString(string(data)) + if err != nil { + return nil, fmt.Errorf("invalid key file format: %w", err) + } + + if len(keyBytes) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("invalid key length in file: expected %d, got %d", ed25519.PrivateKeySize, len(keyBytes)) + } + + priv := ed25519.PrivateKey(keyBytes) + pub := priv.Public().(ed25519.PublicKey) + + log.Printf("Loaded signing key from file: %s", path) + log.Printf("Verifying key: %s", base64.StdEncoding.EncodeToString(pub)) + + return &KeyPair{ + SigningKey: priv, + VerifyingKey: pub, + }, nil +} + +func saveToFile(path string, priv ed25519.PrivateKey) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create key directory: %w", err) + } + + encoded := base64.StdEncoding.EncodeToString(priv) + if err := os.WriteFile(path, []byte(encoded), 0600); err != nil { + return fmt.Errorf("failed to write key file: %w", err) + } + + return nil +} + +// generateAndPersist creates a new key and tries to save it. +// If we can't persist, we log the key so operators can set it manually. +func generateAndPersist(keyPath string) (*KeyPair, error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate key pair: %w", err) + } + + kp := &KeyPair{ + SigningKey: priv, + VerifyingKey: pub, + } + + if err := saveToFile(keyPath, priv); err == nil { + log.Printf("Generated and saved signing key to: %s", keyPath) + log.Printf("Verifying key: %s", base64.StdEncoding.EncodeToString(pub)) + return kp, nil + } + + // couldn't persist, log the key so it can be set via env var + // this is annoying but better than silent failures + keyB64 := base64.StdEncoding.EncodeToString(priv) + + log.Println("========================================") + log.Println("SIGNING KEY PERSISTENCE FAILED") + log.Println("========================================") + log.Printf("Could not save signing key to: %s", keyPath) + log.Println("") + log.Println("This key will be lost on restart, causing") + log.Println("'Invalid signature' errors for users with") + log.Println("cached bundles.") + log.Println("") + log.Println("To persist this key, set this environment variable:") + log.Println("") + log.Printf(" WEBAPP_SERVER_SIGNING_KEY=%s", keyB64) + log.Println("") + log.Println("Or mount a persistent volume at:") + log.Printf(" %s", filepath.Dir(keyPath)) + log.Println("========================================") + + log.Printf("Verifying key: %s", base64.StdEncoding.EncodeToString(pub)) + + return kp, nil +} + +func loadFromBase64Key(keyB64 string) (*KeyPair, error) { + keyBytes, err := base64.StdEncoding.DecodeString(keyB64) + if err != nil { + return nil, fmt.Errorf("invalid WEBAPP_SERVER_SIGNING_KEY: %w", err) + } + + if len(keyBytes) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("WEBAPP_SERVER_SIGNING_KEY must be %d bytes, got %d", ed25519.PrivateKeySize, len(keyBytes)) + } + + priv := ed25519.PrivateKey(keyBytes) + pub := priv.Public().(ed25519.PublicKey) + + log.Printf("Loaded signing key from WEBAPP_SERVER_SIGNING_KEY") + log.Printf("Verifying key: %s", base64.StdEncoding.EncodeToString(pub)) + + return &KeyPair{ + SigningKey: priv, + VerifyingKey: pub, + }, nil +} + +func loadFromBase64Seed(seedB64 string) (*KeyPair, error) { + seedBytes, err := base64.StdEncoding.DecodeString(seedB64) + if err != nil { + return nil, fmt.Errorf("invalid WEBAPP_SERVER_SIGNING_SEED: %w", err) + } + + if len(seedBytes) != ed25519.SeedSize { + return nil, fmt.Errorf("WEBAPP_SERVER_SIGNING_SEED must be %d bytes, got %d", ed25519.SeedSize, len(seedBytes)) + } + + priv := ed25519.NewKeyFromSeed(seedBytes) + pub := priv.Public().(ed25519.PublicKey) + + log.Printf("Derived signing key from WEBAPP_SERVER_SIGNING_SEED") + log.Printf("Verifying key: %s", base64.StdEncoding.EncodeToString(pub)) + + return &KeyPair{ + SigningKey: priv, + VerifyingKey: pub, + }, nil +} + +// deriveFromSecret hashes an arbitrary string to get a seed. +// Simple but works for shared secrets across replicas. +func deriveFromSecret(secret string) (*KeyPair, error) { + hash := sha256.Sum256([]byte(secret)) + priv := ed25519.NewKeyFromSeed(hash[:]) + pub := priv.Public().(ed25519.PublicKey) + + log.Printf("Derived signing key from WEBAPP_SERVER_SIGNING_SECRET") + log.Printf("Verifying key: %s", base64.StdEncoding.EncodeToString(pub)) + + return &KeyPair{ + SigningKey: priv, + VerifyingKey: pub, + }, nil +} + +// isDevMode returns true if GO_ENV=development. +func isDevMode() bool { + return os.Getenv("GO_ENV") == "development" +} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/internal/server/server.go b/packages/hoppscotch-selfhost-web/webapp-server/internal/server/server.go new file mode 100644 index 00000000..ae0fa241 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/internal/server/server.go @@ -0,0 +1,146 @@ +// Package server handles HTTP endpoints for bundle distribution. +// +// Endpoints: +// GET /health - health check +// GET /api/v1/manifest - bundle metadata (files, hashes, signature) +// GET /api/v1/bundle - download the actual ZIP +// GET /api/v1/key - public key for signature verification +// +// All endpoints also available under /desktop-app-server/ for backwards compat. + +package server + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net/http" + + "hoppscotch-selfhost-web/webapp-server/internal/bundle" +) + +type Response struct { + Success bool `json:"success"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` + Code int `json:"code"` +} + +type Server struct { + bundleManager *bundle.Manager +} + +func New(bundleManager *bundle.Manager) *Server { + return &Server{ + bundleManager: bundleManager, + } +} + +func (s *Server) HandleHealth(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + w.WriteHeader(http.StatusOK) +} + +func (s *Server) HandleManifest(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + s.writeErrorResponse(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + log.Println("Fetching bundle manifest") + + b := s.bundleManager.GetBundle() + + response := Response{ + Success: true, + Data: b.Metadata, + Code: http.StatusOK, + } + + s.writeJSONResponse(w, response, http.StatusOK) +} + +func (s *Server) HandleDownloadBundle(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + log.Println("Starting bundle download") + + b := s.bundleManager.GetBundle() + + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b.Content))) + w.Header().Set("Content-Disposition", "attachment; filename=\"bundle.zip\"") + + if _, err := w.Write(b.Content); err != nil { + log.Printf("Error writing bundle response: %v", err) + return + } + + log.Printf("Successfully sent bundle for download (size: %d bytes)", len(b.Content)) +} + +func (s *Server) HandleKey(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + s.writeErrorResponse(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + log.Println("Listing public key") + + keyInfo := map[string]string{ + "key": base64.StdEncoding.EncodeToString(s.bundleManager.GetVerifyingKey()), + } + + response := Response{ + Success: true, + Data: keyInfo, + Code: http.StatusOK, + } + + s.writeJSONResponse(w, response, http.StatusOK) +} + +// writeJSONResponse buffers the response first to avoid partial writes on error +func (s *Server) writeJSONResponse(w http.ResponseWriter, response Response, statusCode int) { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + log.Printf("Error encoding JSON response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if _, err := w.Write(buf.Bytes()); err != nil { + log.Printf("Error writing response: %v", err) + } +} + +func (s *Server) writeErrorResponse(w http.ResponseWriter, message string, statusCode int) { + response := Response{ + Success: false, + Error: message, + Code: statusCode, + } + s.writeJSONResponse(w, response, statusCode) +} + +func (s *Server) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("/health", s.HandleHealth) + mux.HandleFunc("/api/v1/manifest", s.HandleManifest) + mux.HandleFunc("/api/v1/bundle", s.HandleDownloadBundle) + mux.HandleFunc("/api/v1/key", s.HandleKey) + + // desktop app backwards compat + mux.HandleFunc("/desktop-app-server/api/v1/manifest", s.HandleManifest) + mux.HandleFunc("/desktop-app-server/api/v1/bundle", s.HandleDownloadBundle) + mux.HandleFunc("/desktop-app-server/api/v1/key", s.HandleKey) +} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/main.go b/packages/hoppscotch-selfhost-web/webapp-server/main.go new file mode 100644 index 00000000..9a9d6a64 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/webapp-server/main.go @@ -0,0 +1,98 @@ +// Hoppscotch Webapp Server +// +// Builds a signed bundle from frontend assets and serves it over HTTP. +// The bundle is zstd-compressed and signed with ed25519 so clients can verify integrity. + +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "hoppscotch-selfhost-web/webapp-server/internal/bundle" + "hoppscotch-selfhost-web/webapp-server/internal/config" + "hoppscotch-selfhost-web/webapp-server/internal/crypto" + "hoppscotch-selfhost-web/webapp-server/internal/server" +) + +func main() { + log.Println("Initializing Hoppscotch Web Static Server") + + cfg := config.Load() + + // NOTE: key generation handles persistence internally + // it'll try env vars first, then disk, then generate new + keyPair, err := crypto.GenerateKeyPair() + if err != nil { + log.Fatalf("Failed to generate key pair: %v", err) + } + + builder, err := bundle.NewBuilder() + if err != nil { + log.Fatalf("Failed to create bundle builder: %v", err) + } + + // this walks the frontend dir and creates a zstd-compressed zip + content, files, err := builder.Build(cfg.FrontendPath) + if err != nil { + log.Fatalf("Failed to build bundle: %v", err) + } + + // manager holds the bundle in memory and handles signing + bundleManager, err := bundle.NewManager( + content, + files, + keyPair.SigningKey, + keyPair.VerifyingKey, + bundle.DefaultMaxSize, + ) + if err != nil { + log.Fatalf("Failed to create bundle manager: %v", err) + } + + srv := server.New(bundleManager) + mux := http.NewServeMux() + srv.RegisterRoutes(mux) + + addr := fmt.Sprintf(":%d", cfg.Port) + + // NOTE: these timeouts are pretty conservative + // bump them if you're serving huge bundles over slow connections + httpServer := &http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + go func() { + log.Printf("Server starting on %s", addr) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + // wait for shutdown signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Server shutting down...") + + // give in-flight requests 30s to finish + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := httpServer.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown: %v", err) + } + + log.Println("Server stopped") +} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/api/error.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/api/error.rs deleted file mode 100644 index 681890bd..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/api/error.rs +++ /dev/null @@ -1,46 +0,0 @@ -use axum::http::StatusCode; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ApiError { - #[error("Failed to download bundle: {0}")] - DownloadFailed(String), - - #[error("Invalid request: {0}")] - InvalidRequest(String), - - #[error(transparent)] - Bundle(#[from] crate::bundle::BundleError), - - #[error(transparent)] - Json(#[from] serde_json::Error), - - #[error(transparent)] - Axum(#[from] axum::Error), -} - -pub type Result = std::result::Result; - -impl axum::response::IntoResponse for ApiError { - fn into_response(self) -> axum::response::Response { - let (status, error_message) = match &self { - ApiError::DownloadFailed(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::InvalidRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()), - _ => { - tracing::error!(error = ?self, "Internal server error"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - "Internal server error".to_string(), - ) - } - }; - - let body = serde_json::json!({ - "success": false, - "error": error_message, - "code": status.as_u16() - }); - - (status, axum::Json(body)).into_response() - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/api/handler.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/api/handler.rs deleted file mode 100644 index 16d5cf2d..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/api/handler.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::sync::Arc; - -use axum::{ - body::Body, - response::{IntoResponse, Response}, - Json, -}; -use base64::{engine::general_purpose::STANDARD, Engine}; - -use super::error::{ApiError, Result}; -use super::model::{ApiResponse, BundleManifest, PublicKeyInfo}; -use crate::bundle::BundleManager; - -pub struct ApiHandler { - pub bundle_manager: Arc, -} - -impl ApiHandler { - pub fn new(bundle_manager: Arc) -> Self { - Self { bundle_manager } - } - - pub async fn get_manifest(&self) -> Result { - tracing::info!("Fetching bundle manifest"); - let bundle = self.bundle_manager.bundle().await; - - let version = bundle.metadata.version; - let created_at = bundle.metadata.created_at; - let signature = bundle.metadata.signature; - let manifest = bundle.metadata.manifest; - - let manifest = BundleManifest { - version, - created_at, - signature, - manifest, - }; - - tracing::info!("Successfully retrieved bundle manifest"); - - Ok(Json(ApiResponse::ok(manifest))) - } - - pub async fn download_bundle(&self) -> Result { - tracing::info!("Starting bundle download"); - let bundle = self.bundle_manager.bundle().await; - - let response = Response::builder() - .header("content-type", "application/zip") - .header("content-length", bundle.content.len().to_string()) - .header("content-disposition", "attachment; filename=\"bundle.zip\"") - .body(Body::from(bundle.content.clone())) - .map_err(|e| { - tracing::error!(error = ?e, "Failed to create download response"); - ApiError::DownloadFailed(e.to_string()) - })?; - - tracing::info!( - content_length = bundle.content.len(), - "Successfully prepared bundle for download" - ); - Ok(response) - } - - pub async fn key(&self) -> Result { - tracing::info!("Listing public key"); - let server_config = self.bundle_manager.server_config(); - let verifying_key = server_config.verifying_key; - - let verifying_key = verifying_key.as_ref().ok_or_else(|| { - tracing::error!("No signing key configured"); - ApiError::InvalidRequest("No signing key configured".into()) - })?; - - let verifying_key = STANDARD.encode(verifying_key.to_bytes()); - tracing::debug!(verifying_key = ?verifying_key); - - let key_info = PublicKeyInfo { key: verifying_key }; - - tracing::info!("Successfully retrieved public key info"); - Ok(Json(ApiResponse::ok(key_info))) - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/api/mod.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/api/mod.rs deleted file mode 100644 index f021f846..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/api/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::sync::Arc; - -use axum::{extract::State, routing::get, Router}; - -mod error; -mod handler; -mod model; - -use error::Result; -use handler::ApiHandler; - -pub fn routes(bundle_manager: Arc) -> Router { - tracing::info!("Setting up API routes"); - let handler = Arc::new(ApiHandler::new(bundle_manager)); - - let api_routes = Router::new() - .route("/api/v1/manifest", get(get_manifest)) - .route("/api/v1/bundle", get(download_bundle)) - .route("/api/v1/key", get(key)) - .with_state(handler.clone()); - - // NOTE: A hack to allow subpath access override - let desktop_app_routes = Router::new() - .nest("/desktop-app-server", api_routes.clone()); - - api_routes.merge(desktop_app_routes) -} - -async fn get_manifest( - State(handler): State>, -) -> Result { - tracing::debug!("Received request for bundle manifest"); - handler.get_manifest().await -} - -async fn download_bundle( - State(handler): State>, -) -> Result { - tracing::debug!("Received request to download bundle"); - handler.download_bundle().await -} - -async fn key(State(handler): State>) -> Result { - tracing::debug!("Received request to list public keys"); - handler.key().await -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/api/model.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/api/model.rs deleted file mode 100644 index 78fcf5b3..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/api/model.rs +++ /dev/null @@ -1,46 +0,0 @@ -use chrono::{DateTime, Utc}; -use ed25519_dalek::Signature; -use serde::Serialize; - -use crate::model::Manifest; - -#[derive(Debug, Serialize)] -pub struct ApiResponse { - pub success: bool, - pub data: T, -} - -impl ApiResponse { - pub fn ok(data: T) -> Self { - Self { - success: true, - data, - } - } -} - -#[derive(Debug, Serialize)] -pub struct BundleManifest { - pub version: String, - pub created_at: DateTime, - #[serde(with = "signature_serde")] - pub signature: Signature, - pub manifest: Manifest, -} - -#[derive(Debug, Serialize)] -pub struct PublicKeyInfo { - pub key: String, -} - -mod signature_serde { - use super::*; - use base64::{engine::general_purpose::STANDARD, Engine}; - - pub fn serialize(sig: &Signature, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&STANDARD.encode(sig.to_bytes())) - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/builder.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/builder.rs deleted file mode 100644 index 8cb613f7..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/builder.rs +++ /dev/null @@ -1,109 +0,0 @@ -use rayon::prelude::*; -use std::io::{Cursor, Write}; -use std::path::Path; -use walkdir::WalkDir; -use zip::{write::SimpleFileOptions, CompressionMethod, ZipWriter}; - -use super::error::{BundleError, Result}; -use crate::model::FileEntry; - -pub struct BundleBuilder { - writer: ZipWriter>>, - files: Vec, -} - -impl BundleBuilder { - pub fn new>(frontend_path: P) -> Result { - let frontend_path = frontend_path.as_ref(); - - if !frontend_path.exists() { - return Err(BundleError::Config(format!( - "Frontend path {} does not exist", - frontend_path.display() - ))); - } - - struct FileInfo { - relative_path: String, - content: Vec, - hash: blake3::Hash, - size: u64, - mime_type: Option, - } - - let file_infos: Vec = WalkDir::new(frontend_path) - .into_iter() - .filter_map(|entry| entry.ok()) - .filter(|entry| entry.file_type().is_file()) - .par_bridge() - .map(|entry| { - let path = entry.path(); - let relative_path = path - .strip_prefix(frontend_path) - .unwrap() - .components() - .map(|comp| comp.as_os_str().to_string_lossy()) - .collect::>() - .join("/"); - - let content = std::fs::read(path).map_err(|e| { - BundleError::Config(format!("Failed to read file {}: {}", path.display(), e)) - })?; - - let hash = blake3::hash(&content); - let size = content.len() as u64; - - let mime_type = mime_guess::from_path(path) - .first() - .map(|mime| mime.to_string()); - - Ok(FileInfo { - relative_path, - content, - hash, - size, - mime_type, - }) - }) - .collect::>>()?; - - let mut builder = Self { - writer: ZipWriter::new(Cursor::new(Vec::new())), - files: Vec::with_capacity(file_infos.len()), - }; - - for file_info in file_infos { - let options = SimpleFileOptions::default() - .compression_method(CompressionMethod::Zstd) - .unix_permissions(0o644); - - builder - .writer - .start_file(&file_info.relative_path, options) - .map_err(|e| BundleError::Config(format!("Failed to start file in zip: {}", e)))?; - - builder - .writer - .write_all(&file_info.content) - .map_err(|e| BundleError::Config(format!("Failed to write file to zip: {}", e)))?; - - builder.files.push(FileEntry { - path: file_info.relative_path, - size: file_info.size, - hash: file_info.hash, - mime_type: file_info.mime_type, - }); - } - - Ok(builder) - } - - pub fn finish(self) -> Result<(Vec, Vec)> { - let writer = self - .writer - .finish() - .map_err(|e| BundleError::Config(format!("Failed to finish zip archive: {}", e)))?; - - Ok((writer.into_inner(), self.files)) - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/error.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/error.rs deleted file mode 100644 index 50087553..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum BundleError { - #[error("Bundle not found: {0}")] - NotFound(String), - - #[error("Bundle validation failed: {0}")] - ValidationFailed(String), - - #[error("Path access error: {0}")] - PathAccess(String), - - #[error("Unknown public key: {0}")] - UnknownKey(String), - - #[error("Bundle too large: {size} bytes (max: {max} bytes)")] - TooLarge { size: usize, max: usize }, - - #[error("Configuration error: {0}")] - Config(String), - - #[error(transparent)] - Io(#[from] std::io::Error), - - #[error(transparent)] - Zip(#[from] zip::result::ZipError), - - #[error(transparent)] - Serialization(#[from] serde_json::Error), - - #[error(transparent)] - Signing(#[from] crate::signing::SigningError), -} - -pub type Result = std::result::Result; diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/manager.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/manager.rs deleted file mode 100644 index d8fe0f12..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/manager.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::io::Cursor; -use std::sync::Arc; - -use ed25519_dalek::Signer; -use tokio::sync::RwLock; -use zip::ZipArchive; - -use super::error::{BundleError, Result}; -use super::model::Bundle; -use crate::config::ServerConfig; -use crate::model::FileEntry; - -#[derive(Clone)] -pub struct BundleManager { - current_bundle: Arc>, - config: Arc, -} - -impl BundleManager { - pub fn new(config: Arc, content: Vec, files: Vec) -> Result { - tracing::info!("Initializing BundleManager with a new bundle"); - - { content.len() <= config.max_bundle_size } - .then_some(()) - .ok_or_else(|| { - let err = BundleError::TooLarge { - size: content.len(), - max: config.max_bundle_size, - }; - tracing::error!( - size = content.len(), - max_size = config.max_bundle_size, - "Bundle exceeds size limit" - ); - err - })?; - - ZipArchive::new(Cursor::new(&content)).map_err(|e| { - tracing::error!(error = ?e, "Invalid ZIP archive"); - BundleError::Zip(e) - })?; - - let signing_key = config.signing_key.as_ref().ok_or_else(|| { - tracing::error!("No signing key configured"); - BundleError::Config("No signing key configured".into()) - })?; - let signature = signing_key.sign(&content); - - let bundle_version = &config.bundle_version; - let bundle = Bundle::new(bundle_version.clone(), content, signature, files); - tracing::info!("Successfully created initial bundle"); - - Ok(Self { - current_bundle: Arc::new(RwLock::new(bundle)), - config, - }) - } - - pub fn server_config(&self) -> &ServerConfig { - tracing::info!("Retrieving the current config"); - &self.config - } - - pub async fn bundle(&self) -> Bundle { - tracing::info!("Retrieving the current bundle"); - self.current_bundle.read().await.clone() - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/mod.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/mod.rs deleted file mode 100644 index 9c2c4f0f..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod builder; -mod error; -mod manager; -mod model; - -pub use builder::BundleBuilder; -pub use error::BundleError; -pub use manager::BundleManager; diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/model.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/model.rs deleted file mode 100644 index 04250f85..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/bundle/model.rs +++ /dev/null @@ -1,58 +0,0 @@ -use chrono::{DateTime, Utc}; -use ed25519_dalek::Signature; -use serde::{Deserialize, Serialize}; - -use crate::model::{FileEntry, Manifest}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BundleMetadata { - pub version: String, - pub created_at: DateTime, - #[serde(with = "signature_serde")] - pub signature: Signature, - pub manifest: Manifest, -} - -mod signature_serde { - use super::*; - use base64::{engine::general_purpose::STANDARD, Engine}; - - pub fn serialize(sig: &Signature, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&STANDARD.encode(sig.to_bytes())) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - let s = String::deserialize(deserializer)?; - let bytes = STANDARD.decode(&s).map_err(D::Error::custom)?; - let bytes: [u8; 64] = bytes - .try_into() - .map_err(|_| D::Error::custom("invalid signature length"))?; - Ok(Signature::from_bytes(&bytes)) - } -} - -#[derive(Debug, Clone)] -pub struct Bundle { - pub metadata: BundleMetadata, - pub content: Vec, -} - -impl Bundle { - pub fn new(bundle_version: Option, content: Vec, signature: Signature, files: Vec) -> Self { - let metadata = BundleMetadata { - version: "2025.11.2".to_string(), - created_at: Utc::now(), - signature, - manifest: Manifest { files }, - }; - - Self { metadata, content } - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/config.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/config.rs deleted file mode 100644 index 25a0ec13..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/config.rs +++ /dev/null @@ -1,88 +0,0 @@ -use ed25519_dalek::{SigningKey, VerifyingKey}; -use serde::{Deserialize, Serialize}; - -use crate::signing::SigningKeyPair; - -pub const DEFAULT_MAX_BUNDLE_SIZE: usize = 50 * 1024 * 1024; // 50MB -pub const DEFAULT_PORT: u16 = 3200; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ServerConfig { - #[serde(default = "default_port")] - pub port: u16, - - #[serde(default = "default_max_bundle_size")] - pub max_bundle_size: usize, - - #[serde(default)] - pub bundle_version: Option, - - #[serde(default)] - pub csp_directives: Option, - - #[serde(skip)] - pub signing_key: Option, - - #[serde(skip)] - pub verifying_key: Option, - - #[serde(default = "default_frontend_path")] - pub frontend_path: String, - - #[serde(default = "default_is_dev")] - pub is_dev: bool, -} - -fn default_port() -> u16 { - DEFAULT_PORT -} - -fn default_max_bundle_size() -> usize { - DEFAULT_MAX_BUNDLE_SIZE -} - -fn default_frontend_path() -> String { - "/site/selfhost-web".to_string() -} - -fn default_is_dev() -> bool { - false -} - -impl Default for ServerConfig { - fn default() -> Self { - Self { - port: default_port(), - max_bundle_size: default_max_bundle_size(), - bundle_version: Some("2025.11.2".to_string()), - csp_directives: None, - signing_key: None, - verifying_key: None, - frontend_path: default_frontend_path(), - is_dev: default_is_dev(), - } - } -} - -impl ServerConfig { - pub fn load(key_pair: SigningKeyPair) -> Self { - let frontend_path = if cfg!(debug_assertions) { - "../dist".to_string() - } else { - "/site/selfhost-web".to_string() - }; - - Self { - signing_key: Some(key_pair.signing_key), - verifying_key: Some(key_pair.verifying_key), - bundle_version: Some("2025.11.2".to_string()), - frontend_path, - is_dev: cfg!(debug_assertions), - ..Default::default() - } - } - - pub fn frontend_path(&self) -> &str { - &self.frontend_path - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/error.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/error.rs deleted file mode 100644 index c92ab456..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/error.rs +++ /dev/null @@ -1,125 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use serde::Serialize; - -pub type Result = std::result::Result; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - - #[error("Bundle not found: {0}")] - BundleNotFound(String), - - #[error("Validation error: {0}")] - ValidationError(String), - - #[error("Configuration error: {0}")] - Config(String), - - #[error(transparent)] - Bundle(#[from] crate::bundle::BundleError), - - #[error(transparent)] - Signing(#[from] crate::signing::SigningError), - - #[error(transparent)] - SerdeJson(#[from] serde_json::Error), - - #[error(transparent)] - Axum(#[from] axum::Error), - - #[error(transparent)] - Http(#[from] axum::http::Error), -} - -#[derive(Serialize)] -struct ErrorResponse { - success: bool, - error: String, - code: u16, -} - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let (status, error_message) = match &self { - Error::BundleNotFound(name) => { - tracing::warn!(bundle_name = %name, "Bundle not found"); - (StatusCode::NOT_FOUND, self.to_string()) - } - Error::ValidationError(msg) => { - tracing::warn!(message = %msg, "Validation error"); - (StatusCode::BAD_REQUEST, self.to_string()) - } - Error::Bundle(crate::bundle::BundleError::NotFound(msg)) => { - tracing::warn!(message = %msg, "Bundle not found"); - (StatusCode::NOT_FOUND, msg.clone()) - } - Error::Bundle(crate::bundle::BundleError::ValidationFailed(msg)) => { - tracing::warn!(message = %msg, "Bundle validation failed"); - (StatusCode::BAD_REQUEST, msg.clone()) - } - Error::Config(msg) => { - tracing::error!(error = %msg, "Configuration error"); - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) - } - _ => { - tracing::error!(error = ?self, "Internal server error"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - "Internal server error".to_string(), - ) - } - }; - - let body = ErrorResponse { - success: false, - error: error_message, - code: status.as_u16(), - }; - - tracing::debug!( - status = %status, - error = %body.error, - code = body.code, - "Generating error response" - ); - - (status, Json(body)).into_response() - } -} - -impl From for StatusCode { - fn from(error: Error) -> Self { - match error { - Error::BundleNotFound(_) => StatusCode::NOT_FOUND, - Error::ValidationError(_) => StatusCode::BAD_REQUEST, - Error::Bundle(crate::bundle::BundleError::NotFound(_)) => StatusCode::NOT_FOUND, - Error::Bundle(crate::bundle::BundleError::ValidationFailed(_)) => { - StatusCode::BAD_REQUEST - } - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl Error { - pub fn is_not_found(&self) -> bool { - matches!( - self, - Error::BundleNotFound(_) | Error::Bundle(crate::bundle::BundleError::NotFound(_)) - ) - } - - pub fn is_validation_error(&self) -> bool { - matches!( - self, - Error::ValidationError(_) - | Error::Bundle(crate::bundle::BundleError::ValidationFailed(_)) - ) - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/main.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/main.rs deleted file mode 100644 index 89306215..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/main.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::net::SocketAddr; -use std::path::Path; - -use axum::{http::StatusCode, routing::get, Router}; -use tower_http::trace::TraceLayer; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -mod api; -mod bundle; -mod config; -mod error; -mod model; -mod signing; - -use bundle::{BundleBuilder, BundleManager}; -use config::{ServerConfig, DEFAULT_PORT}; -use signing::SigningKeyPair; - -#[tokio::main] -async fn main() -> error::Result<()> { - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "info,tower_http=debug".into()), - ) - .with(tracing_subscriber::fmt::layer().without_time()) - .init(); - - tracing::info!("Initializing Hoppscotch Web Static Server"); - - let key_pair = SigningKeyPair::new(); - tracing::debug!("Generated new signing key pair"); - - let config = ServerConfig::load(key_pair); - tracing::debug!(?config, "Configuration loaded successfully"); - - let frontend_path = Path::new(config.frontend_path()).canonicalize()?; - if !frontend_path.exists() { - tracing::error!(?frontend_path, "Frontend path does not exist"); - panic!("Frontend path does not exist"); - } - tracing::info!(?frontend_path, "Frontend path verified"); - - let builder = BundleBuilder::new(&frontend_path)?; - tracing::info!(?frontend_path, "Initialized bundle builder from path",); - - let (content, files) = builder.finish()?; - tracing::info!("Bundle built successfully with {} files", files.len()); - - tracing::debug!("Initialized bundle manager"); - let bundle_manager = BundleManager::new(config.into(), content, files)?; - tracing::info!("Bundle signed and stored successfully in the bundle manager"); - - let app = Router::new() - .route("/health", get(|| async { StatusCode::OK })) - .merge(api::routes(bundle_manager.into())) - .layer(TraceLayer::new_for_http()); - - let addr = SocketAddr::from(([0, 0, 0, 0], DEFAULT_PORT)); - tracing::info!("Attempting to bind to address: {}", addr); - - let listener = tokio::net::TcpListener::bind(addr).await?; - tracing::info!("Server successfully bound to {}", listener.local_addr()?); - - tracing::info!("Starting server"); - axum::serve(listener, app).await?; - - Ok(()) -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/model.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/model.rs deleted file mode 100644 index fb15fb03..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/model.rs +++ /dev/null @@ -1,42 +0,0 @@ -use blake3::Hash; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileEntry { - pub path: String, - pub size: u64, - #[serde(with = "hash_serde")] - pub hash: Hash, - pub mime_type: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Manifest { - pub files: Vec, -} - -mod hash_serde { - use super::*; - use base64::{engine::general_purpose::STANDARD, Engine}; - use blake3::Hash; - - pub fn serialize(hash: &Hash, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&STANDARD.encode(hash.as_bytes())) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - let s = String::deserialize(deserializer)?; - let bytes = STANDARD.decode(&s).map_err(D::Error::custom)?; - let bytes: [u8; 32] = bytes - .try_into() - .map_err(|_| D::Error::custom("invalid hash length"))?; - Ok(Hash::from_bytes(bytes)) - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/signing/error.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/signing/error.rs deleted file mode 100644 index 2e959f29..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/signing/error.rs +++ /dev/null @@ -1,51 +0,0 @@ -use base64::DecodeError; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum SigningError { - #[error("Environment variable not found")] - EnvVarMissing, - - #[error("Invalid base64 encoding: {0}")] - InvalidBase64(#[from] DecodeError), - - #[error("Invalid key length: expected 32 bytes")] - InvalidKeyLength, - - #[error("Invalid signature")] - InvalidSignature, - - #[error("Invalid key format: {0}")] - InvalidKeyFormat(String), - - #[error("Signature verification failed: {0}")] - VerificationFailed(String), - - #[error("Key generation failed: {0}")] - GenerationFailed(String), -} - -impl SigningError { - pub fn is_invalid_signature(&self) -> bool { - matches!( - self, - SigningError::InvalidSignature | SigningError::VerificationFailed(_) - ) - } - - pub fn is_configuration_error(&self) -> bool { - matches!( - self, - SigningError::EnvVarMissing - | SigningError::InvalidKeyFormat(_) - | SigningError::InvalidKeyLength - ) - } -} - -impl From for SigningError { - fn from(err: ed25519_dalek::SignatureError) -> Self { - tracing::error!(error = ?err, "Signature verification failed"); - SigningError::InvalidSignature - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/signing/key.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/signing/key.rs deleted file mode 100644 index a3a293db..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/signing/key.rs +++ /dev/null @@ -1,37 +0,0 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; -use chrono::Utc; -use ed25519_dalek::{SigningKey, VerifyingKey}; - -#[derive(Debug, Clone)] -pub struct SigningKeyPair { - pub signing_key: SigningKey, - pub verifying_key: VerifyingKey, - pub key_id: String, -} - -impl SigningKeyPair { - pub fn new() -> Self { - tracing::info!("Generating new signing key pair"); - - let signing_key = SigningKey::generate(&mut rand::rngs::OsRng); - let verifying_key = VerifyingKey::from(&signing_key); - - let key_id = format!("key_{}", Utc::now().format("%Y%m%d_%H%M%S")); - - tracing::info!(key_id = %key_id, "Generated new signing key pair"); - tracing::info!(signing_key_bytes_encoded = ?STANDARD.encode(signing_key.to_bytes())); - tracing::info!(verifying_key_bytes_encoded = ?STANDARD.encode(verifying_key.to_bytes())); - - Self { - signing_key, - verifying_key, - key_id, - } - } -} - -impl std::fmt::Display for SigningKeyPair { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SigningKeyPair(id: {})", self.key_id) - } -} diff --git a/packages/hoppscotch-selfhost-web/webapp-server/src/signing/mod.rs b/packages/hoppscotch-selfhost-web/webapp-server/src/signing/mod.rs deleted file mode 100644 index 1190add7..00000000 --- a/packages/hoppscotch-selfhost-web/webapp-server/src/signing/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod error; -mod key; - -pub use error::SigningError; -pub use key::SigningKeyPair; diff --git a/prod.Dockerfile b/prod.Dockerfile index 11d17eb6..ddd83d88 100644 --- a/prod.Dockerfile +++ b/prod.Dockerfile @@ -1,6 +1,8 @@ -# This step is used to build a custom build of Caddy to prevent -# vulnerable packages on the dependency chain -FROM alpine:3.23.0 AS caddy_builder +# Base Go builder with Go lang installation +# This stage is used to build both Caddy and the webapp server, +# preventing vulnerable packages on the dependency chain +FROM alpine:3.23.0 AS go_builder + RUN apk add --no-cache curl git && \ mkdir -p /tmp/caddy-build && \ curl -L -o /tmp/caddy-build/src.tar.gz https://github.com/caddyserver/caddy/releases/download/v2.10.2/caddy_2.10.2_src.tar.gz @@ -40,10 +42,22 @@ RUN tar xvf /tmp/caddy-build/src.tar.gz && \ go mod tidy && \ go mod vendor +# Build Caddy from the Go base +FROM go_builder AS caddy_builder WORKDIR /tmp/caddy-build/cmd/caddy # Build using the updated vendored dependencies RUN go build +# Build webapp server from the Go base +# This reuses the Go installation from go_builder, avoiding a separate image pull +# and significantly reducing build time (especially on ARM64 in CI) +FROM go_builder AS webapp_server_builder +WORKDIR /usr/src/app +COPY . . +WORKDIR /usr/src/app/packages/hoppscotch-selfhost-web/webapp-server +RUN go mod download +RUN CGO_ENABLED=0 GOOS=linux go build -o webapp-server . + # Shared Node.js base with optimized NPM installation @@ -123,13 +137,6 @@ FROM base_builder AS fe_builder WORKDIR /usr/src/app/packages/hoppscotch-selfhost-web RUN pnpm run generate -FROM rust:1-alpine AS webapp_server_builder -WORKDIR /usr/src/app -RUN apk add --no-cache musl-dev -COPY . . -WORKDIR /usr/src/app/packages/hoppscotch-selfhost-web/webapp-server -RUN cargo build --release - FROM node_base AS app @@ -137,7 +144,7 @@ FROM node_base AS app COPY --from=caddy_builder /tmp/caddy-build/cmd/caddy/caddy /usr/bin/caddy # Copy over webapp server bin -COPY --from=webapp_server_builder /usr/src/app/packages/hoppscotch-selfhost-web/webapp-server/target/release/webapp-server /usr/local/bin/ +COPY --from=webapp_server_builder /usr/src/app/packages/hoppscotch-selfhost-web/webapp-server/webapp-server /usr/local/bin/ COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/prod_run.mjs /site/prod_run.mjs COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/selfhost-web.Caddyfile /etc/caddy/selfhost-web.Caddyfile @@ -198,7 +205,7 @@ COPY --from=backend_builder /dist/backend /dist/backend COPY --from=base_builder /usr/src/app/packages/hoppscotch-backend/prod_run.mjs /dist/backend # Static Server -COPY --from=webapp_server_builder /usr/src/app/packages/hoppscotch-selfhost-web/webapp-server/target/release/webapp-server /usr/local/bin/ +COPY --from=webapp_server_builder /usr/src/app/packages/hoppscotch-selfhost-web/webapp-server/webapp-server /usr/local/bin/ RUN mkdir -p /site/selfhost-web COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/dist /site/selfhost-web