commit b9fbed9a543be6b0b54054757d396d38c9743520
Author: Hadi <112569860+anotherhadi@users.noreply.github.com>
Date: Wed Sep 24 17:20:03 2025 +0200
init
diff --git a/.github/assets/banner.png b/.github/assets/banner.png
new file mode 100644
index 0000000..e0eb561
Binary files /dev/null and b/.github/assets/banner.png differ
diff --git a/.github/assets/logo.png b/.github/assets/logo.png
new file mode 100644
index 0000000..a8da970
Binary files /dev/null and b/.github/assets/logo.png differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fff8bb5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+result/
+testdata/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0f066ed
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,13 @@
+# Contributing
+
+Everybody is invited and welcome to contribute. There is a lot to do... Check
+the issues!
+
+The process is straight-forward.
+
+- Read
+ [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)
+ by Kubernetes (but skip step 0 and 1)
+- Fork this git repository
+- Write your changes (bug fixes, new features, ...).
+- Create a Pull Request against the main branch.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5615ec7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Hadi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..891affa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+# Eleakxir β Self-hosted search engine for leaked data.
+
+[Eleakxir](https://eleakxir.hadi.diy) is a **self-hosted search engine** that
+lets you connect to your own **private and secure server**, **explore data
+wells** (parquet files) from multiple sources, and visualize results in a clean,
+modern web interface.
+
+> β¨ 100% open-source β you control your data, you control your server.
+
+## π Features
+
+- π **Private by design** β connect to your own Eleakxir server with a custom
+ URL + password.
+- π¨ **Beautiful UI** β built with Svelte, TailwindCSS, and DaisyUI.
+- π **Open source & extensible** β hack it, self-host it, extend it.
+- **π Efficient File Format**: Uses the columnar **Parquet** format for high
+ compression and rapid query performance.
+- **π€ Automatic Discovery**: Automatically detects new `.parquet` files in your
+ folders and updates its metadata cache on a configurable schedule.
+- **π Standardized Schema**: Includes a detailed guide on how to normalize your
+ data leaks for consistent and effective searching across different breaches.
+ (See [here](./leak-utils/DATALEAKS-NORMALIZATION.md))
+- **π§° Data Utility Toolkit**: Includes a dedicated command-line tool
+ [leak-utils](./leak-utils/README.md) for managing, cleaning, and converting
+ data files to the standardized Parquet format.
+- **π OSINT Tools**: Integration of various OSINT tools:
+ - [github-recon](https://github.com/anotherhadi/github-recon)
+ - [gravatar-recon](https://github.com/anotherhadi/gravatar-recon) (To-do)
+ - sherlock (To-do)
+ - holehe (To-do)
+ - ghunt (To-do)
+
+## βοΈ How it works
+
+1. You run an **Elixir server** that manages parquet files from various leaked
+ data sources and multiple OSINT tools.
+2. Eleakxir (the web client) connects to your server via HTTPS and authenticated
+ headers.
+3. You can:
+ - Search across indexed leaks and OSINT tools
+ - Browse results interactively
+ - Review history and stats
+
+## π¨ Disclaimer
+
+Eleakxir is provided **for educational and research purposes only**. You are
+solely responsible for how you use this software. Accessing, storing, or
+distributing leaked data may be illegal in your jurisdiction. The authors and
+contributors **do not condone or promote illegal activity**. Use responsibly and
+only with data you are legally permitted to process.
+
+## π§βπ» Tech stack
+
+- **Frontend**: [Svelte](https://svelte.dev/),
+ [sv-router](https://github.com/colinlienard/sv-router),
+ [TailwindCSS](https://tailwindcss.com/), [DaisyUI](https://daisyui.com/)
+- **Backend**: [Golang](https://go.dev), [Gin](https://gin-gonic.com),
+ [duckdb](https://duckdb.org)
+
+## π¦ Getting started
+
+### Install with NixOS
+
+1. In the `flake.nix` file, add `eleakxir` in the `inputs` section and import
+ the `eleakxir.nixosModules.default` module:
+
+```nix
+{
+ inputs = {
+ eleakxir.url = "github:anotherhadi/eleakxir";
+ };
+ outputs = {
+ # ...
+ modules = [
+ inputs.eleakxir.nixosModules.eleakxir
+ ];
+ # ...
+ }
+}
+```
+
+2. Enable the backend service:
+
+```nix
+services.eleakxir = {
+ enable = true;
+ # port = 9198;
+ folders = ["/var/lib/eleakxir/leaks/"] # Folders with parquet files
+};
+```
+
+## π Configuration
+
+### Backend
+
+Check the [back.nix](./nix/back.nix) file to see configuration options.
+
+### Client
+
+Before searching, configure your server in the client:
+
+1. Open [https://eleakxir.hadi.diy](https://eleakxir.hadi.diy) in your browser
+ and add your server.
+2. Click **βConnect your serverβ** in the UI.
+3. Enter your **server URL** and **password**.
+4. Start searching π
+
+## π€ Contributing
+
+[Contributions are welcome](./CONTRIBUTING.md)! Feel free to open issues or
+submit PRs.
diff --git a/back/.air.toml b/back/.air.toml
new file mode 100644
index 0000000..197045c
--- /dev/null
+++ b/back/.air.toml
@@ -0,0 +1,21 @@
+root = "."
+tmp_dir = "tmp"
+
+[build]
+cmd = "go build -o ./tmp/main ./cmd/main.go"
+full_bin = "DEBUG=true DATALEAKS_FOLDERS=../testdata ./tmp/main"
+args = []
+exclude_dir = ["tmp", "vendor"]
+exclude_file = []
+exclude_regex = ["_test.go"]
+exclude_unchanged = false
+follow_symlink = false
+send_interrupt = false
+delay = 1000 # ms
+stop_on_error = true
+
+[log]
+time = true
+
+[misc]
+clean_on_exit = true
diff --git a/back/.gitignore b/back/.gitignore
new file mode 100644
index 0000000..3fec32c
--- /dev/null
+++ b/back/.gitignore
@@ -0,0 +1 @@
+tmp/
diff --git a/back/api/api.go b/back/api/api.go
new file mode 100644
index 0000000..5cc0f62
--- /dev/null
+++ b/back/api/api.go
@@ -0,0 +1,174 @@
+package api
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/anotherhadi/eleakxir/backend/search"
+ "github.com/anotherhadi/eleakxir/backend/server"
+ "github.com/gin-gonic/gin"
+)
+
+// TODO: We need to know when we hit the LIMIT
+
+func routes(s *server.Server, cache *map[string]*search.Result) {
+ s.Router.Use(
+ func(c *gin.Context) {
+ if s.Settings.Password != "" {
+ password := c.GetHeader("X-Password")
+ if password != s.Settings.Password {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
+ return
+ }
+ }
+ c.Next()
+ },
+ )
+
+ s.Router.GET("/", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "Settings": s.Settings,
+ "Dataleaks": s.Dataleaks,
+ "TotalDataleaks": s.TotalDataleaks,
+ "TotalRows": s.TotalRows,
+ "TotalSize": s.TotalSize,
+ })
+ })
+
+ s.Router.GET("/history", func(c *gin.Context) {
+ type historyItem struct {
+ Id string
+ Status string
+ Date time.Time
+ Query search.Query
+ Results int
+ }
+ var history []historyItem
+ s.Mu.RLock()
+ for _, r := range *cache {
+ history = append(history, historyItem{
+ Id: r.Id,
+ Status: r.Status,
+ Date: r.Date,
+ Query: r.Query,
+ Results: len(r.LeakResult.Rows),
+ })
+ }
+ s.Mu.RUnlock()
+ for i := 0; i < len(history)-1; i++ {
+ for j := 0; j < len(history)-i-1; j++ {
+ if history[j].Date.Before(history[j+1].Date) {
+ history[j], history[j+1] = history[j+1], history[j]
+ }
+ }
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "History": history,
+ })
+ })
+
+ s.Router.POST("/search", func(c *gin.Context) {
+ var query search.Query
+ if err := c.BindJSON(&query); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"Error": "invalid JSON"})
+ return
+ }
+ query = cleanQuery(query)
+ if len(query.Text) <= s.Settings.MinimumQueryLength {
+ c.JSON(http.StatusBadRequest, gin.H{"Error": "query too short"})
+ return
+ }
+ id := search.EncodeQueryID(query, *s.TotalDataleaks)
+ s.Mu.RLock()
+ _, exists := (*cache)[id]
+ s.Mu.RUnlock()
+ if exists {
+ c.JSON(http.StatusOK, gin.H{"Id": id})
+ return
+ }
+ r := search.Result{
+ Id: id,
+ }
+ go search.Search(s, query, &r, s.Mu)
+ s.Mu.Lock()
+ (*cache)[id] = &r
+ s.Mu.Unlock()
+ c.JSON(http.StatusOK, gin.H{"Id": id})
+ })
+
+ s.Router.GET("/search/:id", func(c *gin.Context) {
+ id := c.Param("id")
+ s.Mu.RLock()
+ r, exists := (*cache)[id]
+ s.Mu.RUnlock()
+ if !exists {
+ c.JSON(http.StatusNotFound, gin.H{"Error": "not found"})
+ return
+ }
+ c.JSON(http.StatusOK, r)
+ })
+}
+
+func Init(s *server.Server) {
+ if !s.Settings.Debug {
+ gin.SetMode(gin.ReleaseMode)
+ }
+
+ s.Router = gin.Default()
+ s.Router.Use(CORSMiddleware())
+
+ cache := make(map[string]*search.Result)
+
+ go func() {
+ for {
+ time.Sleep(time.Minute)
+ deleteOldCache(s, &cache)
+ }
+ }()
+
+ routes(s, &cache)
+}
+
+func deleteOldCache(s *server.Server, cache *map[string]*search.Result) {
+ s.Mu.Lock()
+ defer s.Mu.Unlock()
+ now := time.Now()
+ for id, r := range *cache {
+ if now.Sub(r.Date) > s.Settings.MaxCacheDuration {
+ delete(*cache, id)
+ }
+ }
+}
+
+func cleanQuery(q search.Query) search.Query {
+ q.Column = strings.ToLower(strings.TrimSpace(q.Column))
+ q.Column = strings.Join(strings.Fields(q.Column), " ")
+ q.Column = strings.ReplaceAll(q.Column, "`", "")
+ q.Column = strings.ReplaceAll(q.Column, "'", "")
+ q.Column = strings.ReplaceAll(q.Column, "-", "_")
+ q.Column = strings.ReplaceAll(q.Column, " ", "_")
+ q.Column = strings.ReplaceAll(q.Column, "\"", "")
+
+ q.Text = strings.TrimSpace(q.Text)
+ q.Text = strings.Join(strings.Fields(q.Text), " ")
+
+ return q
+}
+
+func CORSMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+ c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
+ c.Writer.Header().
+ Set("Access-Control-Allow-Headers", "X-Password, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
+ c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
+
+ if c.Request.Method == "OPTIONS" {
+ c.AbortWithStatus(204)
+ return
+ }
+
+ c.Next()
+ }
+}
diff --git a/back/cmd/main.go b/back/cmd/main.go
new file mode 100644
index 0000000..db444f3
--- /dev/null
+++ b/back/cmd/main.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/anotherhadi/eleakxir/backend/api"
+ "github.com/anotherhadi/eleakxir/backend/server"
+)
+
+func main() {
+ server := server.NewServer()
+ fmt.Println("Starting the server.")
+
+ api.Init(server)
+
+ err := server.Router.Run(":" + strconv.Itoa(server.Settings.Port))
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/back/go.mod b/back/go.mod
new file mode 100644
index 0000000..6b45ca6
--- /dev/null
+++ b/back/go.mod
@@ -0,0 +1,70 @@
+module github.com/anotherhadi/eleakxir/backend
+
+go 1.25.0
+
+require (
+ github.com/anotherhadi/github-recon v1.5.6
+ github.com/charmbracelet/log v0.4.2
+ github.com/gin-gonic/gin v1.10.1
+ github.com/marcboeker/go-duckdb v1.8.5
+)
+
+require (
+ github.com/apache/arrow-go/v18 v18.1.0 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
+ github.com/charmbracelet/x/ansi v0.8.0 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/google/flatbuffers v25.1.24+incompatible // indirect
+ github.com/google/go-github/v72 v72.0.0 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.17.11 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/pierrec/lz4/v4 v4.1.22 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51 // indirect
+ github.com/spf13/pflag v1.0.7 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/zeebo/xxh3 v1.0.2 // indirect
+ golang.org/x/arch v0.8.0 // indirect
+ golang.org/x/crypto v0.38.0 // indirect
+ golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
+ golang.org/x/image v0.28.0 // indirect
+ golang.org/x/mod v0.25.0 // indirect
+ golang.org/x/net v0.40.0 // indirect
+ golang.org/x/sync v0.15.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.26.0 // indirect
+ golang.org/x/tools v0.33.0 // indirect
+ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
+ google.golang.org/protobuf v1.36.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/back/go.sum b/back/go.sum
new file mode 100644
index 0000000..edf6922
--- /dev/null
+++ b/back/go.sum
@@ -0,0 +1,179 @@
+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/anotherhadi/github-recon v1.5.6 h1:IN3lQZRqqNbPpSyP5fvNoJrYODbM2tNwS5tiRgD+i1s=
+github.com/anotherhadi/github-recon v1.5.6/go.mod h1:E2tmCmjEZdJeBx8u1J8sSMtnmU8aDQ6IjCoq3ykoHtY=
+github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y=
+github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0=
+github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
+github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
+github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o=
+github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
+github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
+github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0=
+github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
+github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51 h1:H/XUfYcLxI3CBmDlgBpnOeTntRgqWvIoUXnqhCF5a0s=
+github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51/go.mod h1:sqhdZVLvqzTEBtmZBuTnFDUW0Lsryw2X2/wrLgqLEYg=
+github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
+golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
+golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
+golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
+golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
+golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
+golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
+gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
+google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
+google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/back/search/dataleak/dataleak.go b/back/search/dataleak/dataleak.go
new file mode 100644
index 0000000..4632474
--- /dev/null
+++ b/back/search/dataleak/dataleak.go
@@ -0,0 +1,191 @@
+package dataleak
+
+import (
+ "fmt"
+ "slices"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/anotherhadi/eleakxir/backend/server"
+ "github.com/charmbracelet/log"
+)
+
+type LeakResult struct {
+ Duration time.Duration
+ Rows []map[string]string
+ Error string
+}
+
+func Search(s *server.Server, queryText, column string, exactMatch bool) LeakResult {
+ if len(*(s.Dataleaks)) == 0 {
+ return LeakResult{
+ Error: "No dataleak configured",
+ }
+ }
+ now := time.Now()
+ result := LeakResult{}
+
+ sqlQuery := buildSqlQuery(s, queryText, column, exactMatch)
+
+ if s.Settings.Debug {
+ log.Info("New query:", "query", sqlQuery)
+ }
+ rows, err := s.Duckdb.Query(sqlQuery)
+ if err != nil {
+ result.Error = err.Error()
+ return result
+ }
+ defer rows.Close()
+
+ cols, err := rows.Columns()
+ if err != nil {
+ result.Error = err.Error()
+ return result
+ }
+
+ rawResult := make([][]byte, len(cols))
+ dest := make([]any, len(cols))
+ for i := range rawResult {
+ dest[i] = &rawResult[i]
+ }
+
+ for rows.Next() {
+ err := rows.Scan(dest...)
+ if err != nil {
+ result.Error = err.Error()
+ return result
+ }
+
+ rowMap := make(map[string]string)
+ for i, colName := range cols {
+ if rawResult[i] == nil || colName == "" {
+ continue
+ }
+ if colName == "filename" {
+ rowMap["source"] = server.FormatParquetName(string(rawResult[i]))
+ continue
+ }
+ rowMap[colName] = string(rawResult[i])
+ }
+ result.Rows = append(result.Rows, rowMap)
+ }
+
+ if err = rows.Err(); err != nil {
+ result.Error = err.Error()
+ return result
+ }
+
+ result.Rows = removeDuplicateMaps(result.Rows)
+
+ result.Duration = time.Since(now)
+ return result
+}
+
+func removeDuplicateMaps(maps []map[string]string) []map[string]string {
+ seen := make(map[string]struct{})
+ result := []map[string]string{}
+
+ for _, m := range maps {
+ // Create a unique key for the map by concatenating its key-value pairs
+ var sb strings.Builder
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ slices.Sort(keys) // Sort keys to ensure consistent order
+ for _, k := range keys {
+ sb.WriteString(k)
+ sb.WriteString("=")
+ sb.WriteString(m[k])
+ sb.WriteString(";")
+ }
+ key := sb.String()
+
+ if _, exists := seen[key]; !exists {
+ seen[key] = struct{}{}
+ result = append(result, m)
+ }
+ }
+
+ return result
+}
+
+func buildSqlQuery(s *server.Server, queryText, column string, exactMatch bool) string {
+ limit := strconv.Itoa(s.Settings.Limit)
+ from := getFromClause(s)
+ if column == "name" {
+ column = "full_name"
+ }
+ columns := []string{column}
+ if column == "all" || column == "" {
+ columns = s.Settings.BaseColumns
+ }
+ columnsFiltered := []string{}
+ allColumns := []string{}
+ // TODO: Add columns that ends with _col aswell
+ for _, dataleak := range *s.Dataleaks {
+ for _, col := range dataleak.Columns {
+ if !slices.Contains(allColumns, col) {
+ allColumns = append(allColumns, col)
+ }
+ }
+ }
+ if column == "full_text" {
+ columnsFiltered = allColumns
+ } else {
+ for _, col := range columns {
+ if slices.Contains(allColumns, col) {
+ columnsFiltered = append(columnsFiltered, col)
+ }
+ }
+ }
+
+ if len(columnsFiltered) == 0 {
+ return fmt.Sprintf("SELECT * FROM %s LIMIT %s", from, limit)
+ }
+
+ where := getWhereClause(queryText, columnsFiltered, exactMatch)
+ return fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT %s", from, where, limit)
+}
+
+func getWhereClause(queryText string, columns []string, exactMatch bool) string {
+ terms := strings.Fields(queryText)
+ var andClauses []string
+
+ for _, term := range terms {
+ var orClausesForTerm []string
+ termEscaped := strings.ReplaceAll(term, "'", "''")
+
+ for _, col := range columns {
+ if exactMatch {
+ termEscapedILike := strings.ReplaceAll(termEscaped, "_", "\\_")
+ termEscapedILike = strings.ReplaceAll(termEscapedILike, "%", "\\%")
+ orClausesForTerm = append(orClausesForTerm, fmt.Sprintf("\"%s\" ILIKE '%s' ESCAPE '\\'", col, strings.ToLower(termEscapedILike)))
+ } else {
+ // Escape characters for ILIKE
+ termEscapedILike := strings.ReplaceAll(termEscaped, "_", "\\_")
+ termEscapedILike = strings.ReplaceAll(termEscapedILike, "%", "\\%")
+ orClausesForTerm = append(orClausesForTerm, fmt.Sprintf("\"%s\" ILIKE '%%%s%%' ESCAPE '\\'", col, strings.ToLower(termEscapedILike)))
+ }
+ }
+ andClauses = append(andClauses, "("+strings.Join(orClausesForTerm, " OR ")+")")
+ }
+ return strings.Join(andClauses, " AND ")
+}
+
+func getFromClause(s *server.Server) string {
+ parquets := []string{}
+ for _, dataleak := range *s.Dataleaks {
+ parquets = append(parquets, "'"+dataleak.Path+"'")
+ }
+ return fmt.Sprintf("read_parquet([%s], union_by_name=true, filename=true)", strings.Join(parquets, ", "))
+}
+
+func castAllColumns(cols []string) []string {
+ casted := make([]string, len(cols))
+ for i, col := range cols {
+ casted[i] = fmt.Sprintf("cast(%s as text)", col)
+ }
+ return casted
+}
diff --git a/back/search/osint/github.go b/back/search/osint/github.go
new file mode 100644
index 0000000..3ec1fbc
--- /dev/null
+++ b/back/search/osint/github.go
@@ -0,0 +1,90 @@
+package osint
+
+import (
+ "strings"
+ "time"
+
+ "github.com/anotherhadi/eleakxir/backend/server"
+
+ recon_email "github.com/anotherhadi/github-recon/github-recon/email"
+ recon_username "github.com/anotherhadi/github-recon/github-recon/username"
+ github_recon_settings "github.com/anotherhadi/github-recon/settings"
+)
+
+type GithubResult struct {
+ Duration time.Duration
+ Error string
+
+ UsernameResult *recon_username.UsernameResult
+ EmailResult *recon_email.EmailResult
+}
+
+func Search(s *server.Server, queryText, column string) *GithubResult {
+ if !s.Settings.GithubRecon {
+ return nil
+ }
+ gr := GithubResult{}
+ now := time.Now()
+ settings := github_recon_settings.GetDefaultSettings()
+ settings.Token = s.Settings.GithubToken
+ settings.DeepScan = s.Settings.GithubDeepMode
+ if settings.Token != "null" && strings.TrimSpace(settings.Token) != "" {
+ settings.Client = settings.Client.WithAuthToken(settings.Token)
+ }
+ settings.Silent = true
+
+ queryText = strings.TrimSpace(queryText)
+
+ if column == "email" || strings.HasSuffix(column, "_email") ||
+ column == "username" || strings.HasSuffix(column, "_username") ||
+ column == "" || column == "all" {
+ if isValidEmail(queryText) {
+ settings.Target = queryText
+ settings.TargetType = github_recon_settings.TargetEmail
+ result := recon_email.Email(settings)
+ gr.EmailResult = &result
+ } else if isValidUsername(queryText) {
+ settings.Target = queryText
+ settings.TargetType = github_recon_settings.TargetUsername
+ result, err := recon_username.Username(settings)
+ if err != nil {
+ gr.Error = err.Error()
+ }
+ if result.User.Username == "" {
+ gr.UsernameResult = nil
+ } else {
+ gr.UsernameResult = &result
+ }
+ } else {
+ return nil
+ }
+ } else {
+ return nil
+ }
+
+ gr.Duration = time.Since(now)
+ return &gr
+}
+
+func isValidEmail(email string) bool {
+ if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
+ return false
+ }
+ if strings.HasPrefix(email, "@") || strings.HasSuffix(email, "@") {
+ return false
+ }
+ if strings.Contains(email, " ") {
+ return false
+ }
+ return true
+}
+
+func isValidUsername(username string) bool {
+ if len(username) < 1 || len(username) > 39 {
+ return false
+ }
+ if strings.Contains(username, " ") {
+ return false
+ }
+ return true
+}
diff --git a/back/search/search.go b/back/search/search.go
new file mode 100644
index 0000000..7677096
--- /dev/null
+++ b/back/search/search.go
@@ -0,0 +1,68 @@
+package search
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/anotherhadi/eleakxir/backend/search/dataleak"
+ "github.com/anotherhadi/eleakxir/backend/search/osint"
+ "github.com/anotherhadi/eleakxir/backend/server"
+)
+
+type Query struct {
+ Text string
+ Column string // The column to search in (e.g., "email", "password", etc.
+ ExactMatch bool // Whether to search for an exact match
+}
+
+type Result struct {
+ Id string
+ Date time.Time
+ Status string // "pending", "completed"
+ Query Query
+
+ LeakResult dataleak.LeakResult
+ GithubResult osint.GithubResult
+}
+
+func Search(s *server.Server, q Query, r *Result, mu *sync.RWMutex) {
+ var wg sync.WaitGroup
+
+ mu.Lock()
+ r.Date = time.Now()
+ r.Status = "pending"
+ r.Query = q
+ mu.Unlock()
+
+ wg.Add(2)
+
+ go func() {
+ leakResult := dataleak.Search(s, q.Text, q.Column, q.ExactMatch)
+ mu.Lock()
+ r.LeakResult = leakResult
+ mu.Unlock()
+ wg.Done()
+ }()
+
+ go func() {
+ githubResult := osint.Search(s, q.Text, q.Column)
+ mu.Lock()
+ r.GithubResult = *githubResult
+ mu.Unlock()
+ wg.Done()
+ }()
+
+ wg.Wait()
+
+ mu.Lock()
+ r.Status = "completed"
+ mu.Unlock()
+}
+
+func EncodeQueryID(q Query, dataleaksCount uint64) string {
+ raw, _ := json.Marshal(q)
+ return fmt.Sprintf("%d:%s", dataleaksCount, base64.URLEncoding.EncodeToString(raw))
+}
diff --git a/back/server/dataleak.go b/back/server/dataleak.go
new file mode 100644
index 0000000..a2903ac
--- /dev/null
+++ b/back/server/dataleak.go
@@ -0,0 +1,111 @@
+package server
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/charmbracelet/log"
+)
+
+type Dataleak struct {
+ Path string
+ Name string
+ Columns []string
+ Length uint64
+ Size uint64
+}
+
+const CACHE_FILENAME = "dataleaks_cache.json"
+
+// TODO: check os.FileInfo.ModTime() to see if the file has changed since last cache update
+
+func Cache(s *Server) error {
+ if len(s.Settings.Folders) == 0 {
+ return nil
+ }
+ if s.Settings.CacheFolder == "" {
+ s.Settings.CacheFolder = s.Settings.Folders[0]
+ }
+ if err := createDirectoryIfNotExists(s.Settings.CacheFolder); err != nil {
+ return err
+ }
+
+ cacheFile := filepath.Join(s.Settings.CacheFolder, CACHE_FILENAME)
+ dataleaks := []Dataleak{}
+
+ data, err := os.ReadFile(cacheFile)
+ if err == nil {
+ if err := json.Unmarshal(data, &dataleaks); err != nil {
+ log.Warn("Failed to unmarshal dataleaks cache", "error", err)
+ }
+ } else {
+ log.Warn("Failed to read dataleaks cache file", "error", err)
+ }
+
+ // Filter out non-existent files
+ filteredDataleaks := []Dataleak{}
+ writeOutput := false
+ for _, d := range dataleaks {
+ if _, err := os.Stat(d.Path); err == nil {
+ filteredDataleaks = append(filteredDataleaks, d)
+ } else if os.IsNotExist(err) {
+ log.Info("Removing non-existent file from cache", "path", d.Path)
+ writeOutput = true
+ } else {
+ log.Error("Error checking file existence", "path", d.Path, "error", err)
+ }
+ }
+ dataleaks = filteredDataleaks
+
+ // Create a map for quick lookups
+ dataleakMap := make(map[string]struct{}, len(dataleaks))
+ for _, d := range dataleaks {
+ dataleakMap[d.Path] = struct{}{}
+ }
+
+ // Add new files
+ parquetFiles := getAllParquetFiles(s.Settings.Folders)
+ for _, p := range parquetFiles {
+ if _, found := dataleakMap[p]; found {
+ continue
+ }
+
+ writeOutput = true
+ dataleaks = append(dataleaks, getDataleak(*s, p))
+ }
+
+ if writeOutput {
+ data, err := json.MarshalIndent(dataleaks, "", " ")
+ if err != nil {
+ return fmt.Errorf("error marshalling cache: %w", err)
+ }
+ if err := os.WriteFile(cacheFile, data, 0644); err != nil {
+ return fmt.Errorf("error writing cache: %w", err)
+ }
+ }
+
+ s.Dataleaks = &dataleaks
+ totalDataleaks := uint64(len(dataleaks))
+ totalRows := uint64(0)
+ totalSize := uint64(0)
+ for _, d := range dataleaks {
+ totalRows += d.Length
+ totalSize += d.Size
+ }
+ s.TotalDataleaks = &totalDataleaks
+ s.TotalSize = &totalSize
+ s.TotalRows = &totalRows
+ return nil
+}
+
+func getDataleak(s Server, path string) Dataleak {
+ return Dataleak{
+ Path: path,
+ Name: FormatParquetName(path),
+ Columns: getParquetColumns(s, path),
+ Length: getParquetLength(s, path),
+ Size: getFileSize(path),
+ }
+}
diff --git a/back/server/server.go b/back/server/server.go
new file mode 100644
index 0000000..e16fdd6
--- /dev/null
+++ b/back/server/server.go
@@ -0,0 +1,61 @@
+package server
+
+import (
+ "database/sql"
+ "sync"
+ "time"
+
+ "github.com/charmbracelet/log"
+ "github.com/gin-gonic/gin"
+ _ "github.com/marcboeker/go-duckdb"
+)
+
+type Server struct {
+ Settings ServerSettings
+
+ Dataleaks *[]Dataleak
+
+ TotalRows *uint64
+ TotalDataleaks *uint64
+ TotalSize *uint64 // MB
+
+ Router *gin.Engine
+ Duckdb *sql.DB
+ Mu *sync.RWMutex
+}
+
+func NewServer() *Server {
+ zero := uint64(0)
+ emptyDataleak := []Dataleak{}
+ s := &Server{
+ Settings: LoadServerSettings(),
+ Mu: &sync.RWMutex{},
+ TotalDataleaks: &zero,
+ TotalRows: &zero,
+ TotalSize: &zero,
+ Dataleaks: &emptyDataleak,
+ }
+
+ var err error
+
+ s.Duckdb, err = sql.Open("duckdb", "")
+ if err != nil {
+ panic(err)
+ }
+
+ err = Cache(s)
+ if err != nil {
+ panic(err)
+ }
+ go func() {
+ for {
+ time.Sleep(s.Settings.ReloadDataleaksInterval)
+ err := Cache(s)
+ if err != nil {
+ log.Error(err)
+ }
+ }
+ }()
+
+ return s
+}
diff --git a/back/server/settings.go b/back/server/settings.go
new file mode 100644
index 0000000..8a3c04f
--- /dev/null
+++ b/back/server/settings.go
@@ -0,0 +1,129 @@
+package server
+
+import (
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ github_recon_settings "github.com/anotherhadi/github-recon/settings"
+)
+
+type ServerSettings struct {
+ Port int `json:"-"` // Port to run the server on
+ Debug bool
+ Password string `json:"-"` // Do not expose the password in JSON
+ MinimumQueryLength int
+ MaxCacheDuration time.Duration // Delete a search from the cache after this duration
+
+ // Dataleaks
+ Folders []string // Folders to search in for parquets, recursive
+ CacheFolder string
+ BaseColumns []string // Use these columns when column="all"
+ Limit int // Limit number of rows returned
+ ReloadDataleaksInterval time.Duration // Reload dataleaks files from disk every X
+
+ // OSINT Tools
+ GithubRecon bool // Activate github-recon OSINT tool
+ GithubToken string `json:"-"` // Github token for github-recon
+ GithubTokenLoaded bool
+ GithubDeepMode bool // Deep mode for github-recon
+}
+
+func LoadServerSettings() ServerSettings {
+ ss := ServerSettings{
+ Port: getEnvPortOrDefault("PORT", 9198),
+ Debug: getEnvBoolOrDefault("DEBUG", false),
+ Password: getEnvStringOrDefault("PASSWORD", ""),
+ MinimumQueryLength: getEnvIntOrDefault("MINIMUM_QUERY_LENGTH", 3),
+ MaxCacheDuration: getEnvDurationOrDefault("MAX_CACHE_DURATION", 24*time.Hour),
+
+ // Dataleaks
+ Folders: getEnvStringListOrDefault("DATALEAKS_FOLDERS", []string{}),
+ CacheFolder: getEnvStringOrDefault("DATALEAKS_CACHE_FOLDER", ""),
+ BaseColumns: getEnvStringListOrDefault("BASE_COLUMNS", []string{"email", "username", "password", "full_name", "phone", "url"}),
+ Limit: getEnvIntOrDefault("LIMIT", 100),
+ ReloadDataleaksInterval: getEnvDurationOrDefault("RELOAD_DATALEAKS_INTERVAL", 20*time.Minute),
+
+ // OSINT Tools
+ GithubRecon: getEnvBoolOrDefault("GITHUB_RECON", true),
+ GithubToken: getEnvStringOrDefault("GITHUB_TOKEN", "null"),
+ GithubDeepMode: getEnvBoolOrDefault("GITHUB_DEEP_MODE", false),
+ }
+
+ if ss.GithubToken == "null" || strings.TrimSpace(ss.GithubToken) == "" {
+ ss.GithubToken = github_recon_settings.GetToken()
+ }
+
+ if ss.GithubToken != "null" && strings.TrimSpace(ss.GithubToken) != "" {
+ ss.GithubTokenLoaded = true
+ }
+
+ return ss
+}
+
+func getEnvStringOrDefault(envKey, defaultValue string) string {
+ value := strings.TrimSpace(os.Getenv(envKey))
+ if value == "" {
+ return defaultValue
+ }
+ return value
+}
+
+func getEnvBoolOrDefault(envKey string, defaultValue bool) bool {
+ value := strings.TrimSpace(os.Getenv(envKey))
+ if value == "" {
+ return defaultValue
+ }
+ value = strings.ToLower(value)
+ if value == "true" || value == "1" {
+ return true
+ } else if value == "false" || value == "0" {
+ return false
+ }
+ return defaultValue
+}
+
+func getEnvDurationOrDefault(envKey string, defaultValue time.Duration) time.Duration {
+ v := getEnvStringOrDefault(envKey, "")
+ if v == "" {
+ return defaultValue
+ }
+ t, err := time.ParseDuration(v)
+ if err != nil {
+ return defaultValue
+ }
+ return t
+}
+
+func getEnvStringListOrDefault(envKey string, defaultValue []string) []string {
+ value := strings.TrimSpace(os.Getenv(envKey))
+ if value == "" {
+ return defaultValue
+ }
+ l := strings.Split(value, ",")
+ for i := range l {
+ l[i] = strings.TrimSpace(l[i])
+ }
+ return l
+}
+
+func getEnvIntOrDefault(envKey string, defaultValue int) int {
+ value := strings.TrimSpace(os.Getenv(envKey))
+ if value == "" {
+ return defaultValue
+ }
+ i, err := strconv.Atoi(value)
+ if err != nil {
+ return defaultValue
+ }
+ return i
+}
+
+func getEnvPortOrDefault(envKey string, defaultValue int) int {
+ p := getEnvIntOrDefault(envKey, defaultValue)
+ if p <= 0 || p >= 65534 {
+ return defaultValue
+ }
+ return p
+}
diff --git a/back/server/utils.go b/back/server/utils.go
new file mode 100644
index 0000000..13678dc
--- /dev/null
+++ b/back/server/utils.go
@@ -0,0 +1,131 @@
+package server
+
+import (
+ "database/sql"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func getParquetColumns(s Server, path string) []string {
+ query := fmt.Sprintf("DESCRIBE SELECT * FROM '%s';", path)
+
+ rows, err := s.Duckdb.Query(query)
+ if err != nil {
+ return []string{}
+ }
+ defer rows.Close()
+
+ var columns []string
+
+ for rows.Next() {
+ var columnName string
+ var columnType string
+ var nullable string
+
+ var key sql.NullString
+ var defaultValue sql.NullString
+ var extra sql.NullString
+
+ if err := rows.Scan(&columnName, &columnType, &nullable, &key, &defaultValue, &extra); err != nil {
+ return []string{}
+ }
+ columns = append(columns, columnName)
+ }
+
+ if err = rows.Err(); err != nil {
+ return []string{}
+ }
+
+ if len(columns) == 0 {
+ return []string{}
+ }
+
+ return columns
+}
+
+func getParquetLength(s Server, path string) uint64 {
+ query := fmt.Sprintf("SELECT COUNT(*) FROM '%s';", path)
+
+ row := s.Duckdb.QueryRow(query)
+
+ var count uint64
+ if err := row.Scan(&count); err != nil {
+ return 0
+ }
+
+ return count
+}
+
+// Walk through the given folder and its subfolders to find all parquet files
+// Return a list of path
+func getAllParquetFiles(folders []string) []string {
+ var paths []string
+ for _, baseDir := range folders {
+ _ = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".parquet") {
+ return err
+ }
+ paths = append(paths, path)
+ return nil
+ })
+ }
+ return paths
+}
+
+func getFileSize(path string) uint64 {
+ info, err := os.Stat(path)
+ if err != nil {
+ return 0
+ }
+ return uint64(info.Size() / (1024 * 1024)) // MB
+}
+
+func FormatParquetName(path string) string {
+ _, file := filepath.Split(path)
+ fileName := strings.TrimSuffix(file, ".parquet")
+
+ parts := strings.Split(fileName, "-")
+ sourceName := parts[0]
+ var blocks []string
+
+ for _, part := range parts[1:] {
+ if strings.HasPrefix(part, "date_") {
+ dateStr := strings.TrimPrefix(part, "date_")
+ dateStr = strings.ReplaceAll(dateStr, "_", "/")
+ blocks = append(blocks, fmt.Sprintf("date: %s", dateStr))
+ } else if strings.HasPrefix(part, "source_") {
+ sourceStr := strings.TrimPrefix(part, "source_")
+ blocks = append(blocks, fmt.Sprintf("source: %s", sourceStr))
+ } else if strings.HasPrefix(part, "notes_") {
+ noteStr := strings.TrimPrefix(part, "notes_")
+ noteStr = strings.ReplaceAll(noteStr, "_", " ")
+ blocks = append(blocks, noteStr)
+ }
+ }
+
+ sourceName = strings.ReplaceAll(sourceName, "_", " ")
+ sourceWords := strings.Fields(sourceName)
+ for i, word := range sourceWords {
+ if len(word) > 0 {
+ sourceWords[i] = strings.ToUpper(string(word[0])) + word[1:]
+ }
+ }
+ formattedSourceName := strings.Join(sourceWords, " ")
+
+ if len(blocks) > 0 {
+ return fmt.Sprintf("%s (%s)", formattedSourceName, strings.Join(blocks, ", "))
+ }
+
+ return formattedSourceName
+}
+
+func createDirectoryIfNotExists(path string) error {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ if err := os.MkdirAll(path, 0755); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..553b50a
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,43 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1757487488,
+ "narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "systems": "systems"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..d4dca0f
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,58 @@
+{
+ description = "Flake for eleakxir";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ systems.url = "github:nix-systems/default";
+ };
+
+ outputs = {
+ systems,
+ self,
+ nixpkgs,
+ ...
+ }: let
+ eachSystem = nixpkgs.lib.genAttrs (import systems);
+
+ importBackend = system:
+ import ./nix/back.nix {
+ pkgs = nixpkgs.legacyPackages.${system};
+ lib = nixpkgs.lib;
+ inherit self;
+ };
+
+ importUtils = system:
+ import ./nix/leak-utils.nix {
+ pkgs = nixpkgs.legacyPackages.${system};
+ lib = nixpkgs.lib;
+ inherit self;
+ };
+
+ importDevShell = system:
+ import ./nix/devshell.nix {
+ pkgs = nixpkgs.legacyPackages.${system};
+ lib = nixpkgs.lib;
+ inherit self;
+ };
+ in {
+ packages = eachSystem (system: {
+ backend = (importBackend system).package;
+ leak-utils = (importUtils system).package;
+ });
+
+ devShells = eachSystem (system: {
+ default = (importDevShell system).devShell;
+ });
+
+ nixosModules.eleakxir = {
+ config,
+ lib,
+ pkgs,
+ ...
+ }:
+ (importBackend pkgs.system).nixosModule {
+ inherit config lib pkgs;
+ inherit self;
+ };
+ };
+}
diff --git a/front/.gitignore b/front/.gitignore
new file mode 100644
index 0000000..3aee7df
--- /dev/null
+++ b/front/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.router
diff --git a/front/bun.lock b/front/bun.lock
new file mode 100644
index 0000000..0d205dd
--- /dev/null
+++ b/front/bun.lock
@@ -0,0 +1,377 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "svelte-app",
+ "dependencies": {
+ "@lucide/svelte": "^0.542.0",
+ "@tailwindcss/vite": "^4.1.12",
+ "axios": "^1.12.1",
+ "clsx": "^2.1.1",
+ "marked": "^16.3.0",
+ "path": "^0.12.7",
+ "sv-router": "latest",
+ "svelte-sonner": "^1.0.5",
+ "tailwind-merge": "^3.3.1",
+ "tailwindcss": "^4.1.12",
+ "theme-change": "^2.5.0",
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
+ "@tailwindcss/typography": "^0.5.18",
+ "daisyui": "^5.1.6",
+ "svelte": "^5.20.2",
+ "svelte-check": "^4.1.4",
+ "typescript": "~5.7.2",
+ "vite": "^6.2.0",
+ },
+ },
+ },
+ "packages": {
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
+
+ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
+
+ "@lucide/svelte": ["@lucide/svelte@0.542.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-NuWttxTVfMSURpOxcKiKvoCtma3JtEpcJWzF/0cO69saZfXlv6G8NYAvEEGLmk75YPl+I+ROe+F97WhddM8r2A=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="],
+
+ "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="],
+
+ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
+
+ "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="],
+
+ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.12", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.12", "@tailwindcss/oxide-darwin-arm64": "4.1.12", "@tailwindcss/oxide-darwin-x64": "4.1.12", "@tailwindcss/oxide-freebsd-x64": "4.1.12", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", "@tailwindcss/oxide-linux-x64-musl": "4.1.12", "@tailwindcss/oxide-wasm32-wasi": "4.1.12", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.12", "", { "os": "android", "cpu": "arm64" }, "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12", "", { "os": "linux", "cpu": "arm" }, "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.12", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.12", "", { "os": "win32", "cpu": "x64" }, "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA=="],
+
+ "@tailwindcss/typography": ["@tailwindcss/typography@0.5.18", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-dDIgwZOlf+tVkZ7A029VvQ1+ngKATENDjMEx2N35s2yPjfTS05RWSM8ilhEWSa5DMJ6ci2Ha9WNZEd2GQjrdQg=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.12", "", { "dependencies": { "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "axios": ["axios@1.12.1", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ=="],
+
+ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
+ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
+ "daisyui": ["daisyui@5.1.6", "", {}, "sha512-KCzv25f+3lwWbfnPZZG9Xo0kSGO1NSysyIiS5AoCtDotIrvvArggHklCey1Fg6U2gZuqxsi2rptT1q3khoYCMw=="],
+
+ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
+
+ "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
+ "esrap": ["esrap@2.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
+ "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
+
+ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
+
+ "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
+
+ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+ "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
+
+ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
+
+ "magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
+
+ "marked": ["marked@16.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+
+ "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
+
+ "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
+
+ "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
+
+ "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
+
+ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
+
+ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+
+ "rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="],
+
+ "runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
+
+ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "sv-router": ["sv-router@0.8.1", "", { "dependencies": { "esm-env": "^1.2.2" }, "peerDependencies": { "svelte": "^5" }, "bin": { "sv-router": "src/cli/index.js" } }, "sha512-evUoE6TFEB89u8FzgEXiqj8aCTTCbCrWLBbfPe8qZxbNUT5I0sARoTLIZJtBNuumXe+FxpCCDrb8v/d6va+ZVQ=="],
+
+ "svelte": ["svelte@5.38.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ltBPlkvqk3bgCK7/N323atUpP3O3Y+DrGV4dcULrsSn4fZaaNnOmdplNznwfdWclAgvSr5rxjtzn/zJhRm6TKg=="],
+
+ "svelte-check": ["svelte-check@4.3.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg=="],
+
+ "svelte-sonner": ["svelte-sonner@1.0.5", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg=="],
+
+ "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
+
+ "tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="],
+
+ "tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
+
+ "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
+
+ "theme-change": ["theme-change@2.5.0", "", {}, "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ=="],
+
+ "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
+
+ "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
+
+ "util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="],
+
+ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+ "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
+
+ "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
+
+ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
+
+ "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ }
+}
diff --git a/front/index.html b/front/index.html
new file mode 100644
index 0000000..9c04752
--- /dev/null
+++ b/front/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Eleakxir
+
+
+
+
+
+
+
diff --git a/front/package.json b/front/package.json
new file mode 100644
index 0000000..05077cc
--- /dev/null
+++ b/front/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "svelte-app",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-check",
+ "postinstall": "sv-router"
+ },
+ "dependencies": {
+ "@lucide/svelte": "^0.542.0",
+ "@tailwindcss/vite": "^4.1.12",
+ "axios": "^1.12.1",
+ "clsx": "^2.1.1",
+ "marked": "^16.3.0",
+ "path": "^0.12.7",
+ "sv-router": "latest",
+ "svelte-sonner": "^1.0.5",
+ "tailwind-merge": "^3.3.1",
+ "tailwindcss": "^4.1.12",
+ "theme-change": "^2.5.0"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
+ "@tailwindcss/typography": "^0.5.18",
+ "daisyui": "^5.1.6",
+ "svelte": "^5.20.2",
+ "svelte-check": "^4.1.4",
+ "typescript": "~5.7.2",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/front/public/favicon.svg b/front/public/favicon.svg
new file mode 100644
index 0000000..449b4ce
--- /dev/null
+++ b/front/public/favicon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/front/public/l.png b/front/public/l.png
new file mode 100644
index 0000000..14f3ca3
Binary files /dev/null and b/front/public/l.png differ
diff --git a/front/src/App.svelte b/front/src/App.svelte
new file mode 100644
index 0000000..6eabc04
--- /dev/null
+++ b/front/src/App.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/front/src/app.css b/front/src/app.css
new file mode 100644
index 0000000..f096603
--- /dev/null
+++ b/front/src/app.css
@@ -0,0 +1,94 @@
+@import "tailwindcss";
+@plugin "@tailwindcss/typography";
+
+@plugin "daisyui" {
+ themes:
+ light,
+ dark --prefersdark --default;
+}
+
+@plugin "daisyui/theme" {
+ name: "dark";
+ default: true; /* set as default */
+ prefersdark: true; /* set as default dark mode (prefers-color-scheme:dark) */
+ color-scheme: dark; /* color of browser-provided UI */
+
+ --color-base-100: oklch(0.2096 0.0275 290.36);
+ --color-base-200: oklch(0.1896 0.0242 287.67);
+ --color-base-300: oklch(0.1674 0.0229 292.08);
+ --color-base-content: oklch(0.841 0.0056 297.71);
+
+ --color-primary: oklch(0.5454 0.2756 292.04);
+ --color-primary-content: oklch(0.9074 0.049167 293.0386);
+
+ --color-secondary: oklch(0.5103 0.2756 292.04);
+ --color-secondary-content: oklch(0.9074 0.049167 293.0386);
+
+ --color-accent: oklch(0.6241 0.1575 277.95);
+ --color-accent-content: oklch(0.1248 0.031 280.93);
+
+ --color-neutral: oklch(0.2813 0.0153 269.13);
+ --color-neutral-content: oklch(0.8574 0.003 264.54);
+
+ --radius-selector: 1rem;
+ --radius-field: 0.5rem;
+ --radius-box: 1rem;
+ --size-selector: 0.25rem;
+ --size-field: 0.25rem;
+ --border: 1px;
+ --depth: 0;
+ --noise: 0;
+}
+
+@plugin "daisyui/theme" {
+ name: "light";
+ default: false; /* set as default */
+ prefersdark: false; /* set as default dark mode (prefers-color-scheme:dark) */
+ color-scheme: light; /* color of browser-provided UI */
+
+ --color-primary: oklch(0.5454 0.2756 292.04);
+ --color-primary-content: oklch(0.9074 0.049167 293.0386);
+
+ --color-secondary: oklch(0.5103 0.2756 292.04);
+ --color-secondary-content: oklch(0.9074 0.049167 293.0386);
+
+ --color-accent: oklch(0.6241 0.1575 277.95);
+ --color-accent-content: oklch(0.1248 0.031 280.93);
+
+ --radius-selector: 1rem;
+ --radius-field: 0.5rem;
+ --radius-box: 1rem;
+ --size-selector: 0.25rem;
+ --size-field: 0.25rem;
+ --border: 1px;
+ --depth: 0;
+ --noise: 0;
+}
+
+main {
+ @apply mx-auto w-full px-6;
+}
+
+.h1 {
+ @apply scroll-m-20 text-4xl sm:text-5xl font-extrabold tracking-tight lg:text-5xl;
+}
+
+.h2 {
+ @apply scroll-m-20 text-3xl sm:text-4xl font-semibold tracking-tight transition-colors;
+}
+
+.h3 {
+ @apply scroll-m-20 text-2xl font-semibold tracking-tight transition-colors;
+}
+
+.h4 {
+ @apply scroll-m-20 text-xl font-semibold tracking-tight transition-colors;
+}
+
+.h5 {
+ @apply scroll-m-20 text-lg font-semibold tracking-tight transition-colors;
+}
+
+.h6 {
+ @apply scroll-m-20 text-base font-semibold tracking-tight transition-colors;
+}
diff --git a/front/src/lib/components/accordion.svelte b/front/src/lib/components/accordion.svelte
new file mode 100644
index 0000000..22c952c
--- /dev/null
+++ b/front/src/lib/components/accordion.svelte
@@ -0,0 +1,79 @@
+
+
+ {
+ if (children != null) {
+ isOpen = !isOpen;
+ }
+ }}
+>
+
+ {#if imageUrl && imageUrl.length > 0}
+
+ {:else}
+ {@const Icon = icon}
+
+
+
+ {/if}
+
+
+
{title}
+ {#if subtitle != null && subtitle.length !== 0}
+
+ {subtitle}
+
+ {/if}
+
+ {#if children != null}
+
+ {#if isOpen}
+
+ {:else}
+
+ {/if}
+
+ {/if}
+
+{#if children != null}
+ {#if isOpen}
+
+ {@render children()}
+
+ {/if}
+{/if}
diff --git a/front/src/lib/components/dark-mode-toggle.svelte b/front/src/lib/components/dark-mode-toggle.svelte
new file mode 100644
index 0000000..7b9f688
--- /dev/null
+++ b/front/src/lib/components/dark-mode-toggle.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ Light
+
+
+
+ Dark
+
+
+
diff --git a/front/src/lib/components/index/AnimatedBeam.svelte b/front/src/lib/components/index/AnimatedBeam.svelte
new file mode 100644
index 0000000..10cbb78
--- /dev/null
+++ b/front/src/lib/components/index/AnimatedBeam.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/lib/components/index/AnimatedBeamMultiple.svelte b/front/src/lib/components/index/AnimatedBeamMultiple.svelte
new file mode 100644
index 0000000..cd991d4
--- /dev/null
+++ b/front/src/lib/components/index/AnimatedBeamMultiple.svelte
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/lib/components/index/Circle.svelte b/front/src/lib/components/index/Circle.svelte
new file mode 100644
index 0000000..c341878
--- /dev/null
+++ b/front/src/lib/components/index/Circle.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/front/src/lib/components/index/search/datawells.svelte b/front/src/lib/components/index/search/datawells.svelte
new file mode 100644
index 0000000..f04bdc6
--- /dev/null
+++ b/front/src/lib/components/index/search/datawells.svelte
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ Number of rows
+ {#if showColumns}
+ Columns
+ {/if}
+
+
+
+ {#if paginatedDataleaks.length > 0}
+ {#each paginatedDataleaks as item}
+
+
+ {item.Name}
+
+ {item.Length.toLocaleString("fr")}
+ {#if showColumns}
+
+ {item.Columns.map((col) => col.replace(/_/g, " ")).join(", ")}
+
+ {/if}
+
+ {/each}
+ {:else}
+
+ (Ξ.Ξ) No data wells found
+
+ {/if}
+
+
+
+
+ {#if totalPages > 1}
+
+ Β«
+ Page {page} / {totalPages}
+ Β»
+
+ {/if}
+
diff --git a/front/src/lib/components/index/search/history.svelte b/front/src/lib/components/index/search/history.svelte
new file mode 100644
index 0000000..3ea5b2c
--- /dev/null
+++ b/front/src/lib/components/index/search/history.svelte
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Query
+ Results
+ Status
+ Date
+
+
+
+
+ {#if paginatedHistory.length > 0}
+ {#each paginatedHistory as item}
+
+
+ {
+ navigate(`/search/:id`, { params: { id: item.Id } });
+ }}
+ class="btn btn-link p-0 no-underline text-base-content"
+ >
+ {item.Query.Text}
+
+
+ {item.Results}
+
+ {item.Status}
+
+ {formatDate(item.Date)}
+ {
+ navigate(`/search/:id`, { params: { id: item.Id } });
+ }}
+ >
+
+ {/each}
+ {:else}
+
+ (Ξ.Ξ) No history found
+
+ {/if}
+
+
+
+
+ {#if totalPages > 1}
+
+ Β«
+ Page {page} / {totalPages}
+ Β»
+
+ {/if}
+
diff --git a/front/src/lib/components/index/search/howToSearch.svelte b/front/src/lib/components/index/search/howToSearch.svelte
new file mode 100644
index 0000000..5c77e35
--- /dev/null
+++ b/front/src/lib/components/index/search/howToSearch.svelte
@@ -0,0 +1,46 @@
+
+
+
+
+ Eleakxir's search engine is designed to be both fast and flexible, letting
+ you find what you need in multiple ways.
+
+
+
Search Modes
+
+
+ All: This is the default mode.
+ It searches for your query across a set of standard columns like email, username,
+ and phone number. This is the fastest and most efficient way to find a specific
+ user or account.
+
+
+
+ Specific column:
+ This mode lets you choose a specific column to search within, such as email or
+ username. It's useful when you know exactly where the data you're looking for
+ is stored.
+
+
+
+ Full Text: This mode combines
+ all available columns into a single, large text field and searches within it.
+ It's great for finding data that might be in an unexpected column, but it's way
+ slower.
+
+
+
Query Matching
+
+ Standard Search: By default,
+ Eleakxir uses a "fuzzy" search. This means it will find results where your search
+ terms are part of a larger string. For example, searching for 1234 would find
+ john.doe@1234.com.
+
+
+
+ Exact Match: When you enable
+ "Exact Match," the search will only return results where the data in a column
+ is an exact match for your search term. This is useful for finding specific,
+ unique values.
+
+
diff --git a/front/src/lib/components/index/search/id/githubResult.svelte b/front/src/lib/components/index/search/id/githubResult.svelte
new file mode 100644
index 0000000..ba24ad7
--- /dev/null
+++ b/front/src/lib/components/index/search/id/githubResult.svelte
@@ -0,0 +1,313 @@
+
+
+{#if githubResult.UsernameResult}
+
+
+
+
+
+
+
+
+
+
{githubResult.UsernameResult.User.Name}
+
+ @{githubResult.UsernameResult.User.Username}
+
+
+
{githubResult.UsernameResult.User.Bio}
+
+
+
+ {#if githubResult.UsernameResult.Socials && githubResult.UsernameResult.Socials.length > 0}
+
+ {/if}
+ {#if githubResult.UsernameResult.CloseFriends && githubResult.UsernameResult.CloseFriends.length > 0}
+
+ {/if}
+ {#if githubResult.UsernameResult.Orgs && githubResult.UsernameResult.Orgs.length > 0}
+
+ {/if}
+ {#if githubResult.UsernameResult.Commits && githubResult.UsernameResult.Commits.length > 0}
+
+
Commits
+
+ {#each githubResult.UsernameResult.Commits as commit}
+ "}
+ subtitle={"Occurrences: " + commit.Occurrences}
+ >
+
+
+ {/each}
+
+
+ {/if}
+ {#if githubResult.UsernameResult.SshKeys && githubResult.UsernameResult.SshKeys.length > 0}
+
+
SSH Keys
+
+ {#each githubResult.UsernameResult.SshKeys as key}
+
+ {key.Key}
+
+ {/each}
+
+
+ {/if}
+ {#if githubResult.UsernameResult.SshSigningKeys && githubResult.UsernameResult.SshSigningKeys.length > 0}
+
+
SSH Signing Keys
+
+ {#each githubResult.UsernameResult.SshSigningKeys as key}
+
+ {key.Key}
+
+ {/each}
+
+
+ {/if}
+ {#if githubResult.UsernameResult.GpgKeys && githubResult.UsernameResult.GpgKeys.length > 0}
+
+
GPG Keys
+
+ {#each githubResult.UsernameResult.GpgKeys as key}
+ 0 ? key.Emails[0].Email : key.KeyID}
+ subtitle={"Created At: " + key.CreatedAt}
+ >
+
+
+ {/each}
+
+
+ {/if}
+ {#if githubResult.UsernameResult.DeepScan}
+ {#if githubResult.UsernameResult.DeepScan.Authors && githubResult.UsernameResult.DeepScan.Authors.length > 0}
+
+ {/if}
+ {#if githubResult.UsernameResult.DeepScan.Emails && githubResult.UsernameResult.DeepScan.Emails.length > 0}
+
+ {/if}
+ {#if githubResult.UsernameResult.DeepScan.Secrets && githubResult.UsernameResult.DeepScan.Secrets.length > 0}
+ {@const flattenedSecrets = githubResult.UsernameResult.DeepScan.Secrets.map(FlattenObject)}
+
+ {/if}
+ {/if}
+
+{:else if githubResult.EmailResult}
+
+ {#if githubResult.EmailResult.Spoofing}
+
From spoofing
+
+
+
+
+
+
+
+
+
@{githubResult.EmailResult.Spoofing.Username}
+ {#if githubResult.EmailResult.Spoofing.Name}
+
+ Name:
+ {githubResult.EmailResult.Spoofing.Name}
+
+ {/if}
+ {#if githubResult.EmailResult.Spoofing.Email}
+
+ Public email:
+ {githubResult.EmailResult.Spoofing.Email}
+
+ {/if}
+ {#if githubResult.EmailResult.Target}
+
+ Primary email:
+ {githubResult.EmailResult.Target}
+
+ {/if}
+
+ {githubResult.EmailResult.Spoofing.Url}
+
+
+
+
+
+ {/if}
+ {#if githubResult.EmailResult.Commits}
+
+
Commits
+
+ {#each githubResult.EmailResult.Commits as commit}
+
+
+
+ {/each}
+
+
+ {/if}
+
+{/if}
diff --git a/front/src/lib/components/index/search/id/row.svelte b/front/src/lib/components/index/search/id/row.svelte
new file mode 100644
index 0000000..726765b
--- /dev/null
+++ b/front/src/lib/components/index/search/id/row.svelte
@@ -0,0 +1,100 @@
+
+
+ {
+ isOpen = !isOpen;
+ }}
+>
+
+ {#if getDomain(row["source"])}
+
+ {:else if row["password"] !== null}
+
+
+
+ {:else if row["email"] !== null}
+
+
+
+ {:else}
+
+
+
+ {/if}
+
+
+
{getHighlightedContent(row)}
+
+ {row["source"]}
+
+
+
+ {#if isOpen}
+
+ {:else}
+
+ {/if}
+
+
+{#if isOpen}
+
+
+
+{/if}
diff --git a/front/src/lib/components/index/search/id/rows.svelte b/front/src/lib/components/index/search/id/rows.svelte
new file mode 100644
index 0000000..623693f
--- /dev/null
+++ b/front/src/lib/components/index/search/id/rows.svelte
@@ -0,0 +1,72 @@
+
+
+
+{#if result}
+
+ {#each paginated as row (row)}
+
+ {/each}
+
+
+ {#if totalPages > 1}
+
+ Β«
+ Page {page} / {totalPages}
+ Β»
+
+ {/if}
+{:else}
+ No result
+{/if}
diff --git a/front/src/lib/components/index/search/id/stats.svelte b/front/src/lib/components/index/search/id/stats.svelte
new file mode 100644
index 0000000..c96deae
--- /dev/null
+++ b/front/src/lib/components/index/search/id/stats.svelte
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
Results
+
+ {nresult.toLocaleString("fr")}
+ {#if result.Status === "pending"}
+
+ {/if}
+
+
+
+
+
+
+
Date
+
+ {formatDate(result.Date)}
+
+
+
+
+
+
+
Status
+
+ {result.Status}
+ {#if result.Status === "pending"}
+
+ {/if}
+
+
+
diff --git a/front/src/lib/components/index/search/searchbar.svelte b/front/src/lib/components/index/search/searchbar.svelte
new file mode 100644
index 0000000..a07f05f
--- /dev/null
+++ b/front/src/lib/components/index/search/searchbar.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+ {#each filters as filter}
+ (activeFilter = filter)}>{filter.replace("_", " ")}
+ {/each}
+
+
+
+
diff --git a/front/src/lib/components/index/search/services.svelte b/front/src/lib/components/index/search/services.svelte
new file mode 100644
index 0000000..6cf258f
--- /dev/null
+++ b/front/src/lib/components/index/search/services.svelte
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+ Service
+ Status
+
+
+
+
+ Data wells lookup
+
+ {#if serverInfo.Dataleaks.length !== 0}
+
+ Active
+ {:else}
+
+ Inactive
+ {/if}
+
+
+
+
+ Github recon
+ {#if serverInfo.Settings.GithubTokenLoaded === true}
+ Token
+ {/if}
+ {#if serverInfo.Settings.GithubDeepMode === true}
+ Deep Mode
+ {/if}
+
+
+ {#if serverInfo.Settings.GithubRecon === true}
+
+ Active
+ {:else}
+
+ Inactive
+ {/if}
+
+
+
+ Google hunt
+
+ {#if serverInfo.Settings.GithubRecon === true}
+
+ Active
+ {:else}
+
+ Inactive
+ {/if}
+
+
+
+
+
+
diff --git a/front/src/lib/components/index/search/stats.svelte b/front/src/lib/components/index/search/stats.svelte
new file mode 100644
index 0000000..ee7dc0c
--- /dev/null
+++ b/front/src/lib/components/index/search/stats.svelte
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
Rows available
+
+ {serverInfo?.TotalRows
+ ? serverInfo.TotalRows.toLocaleString("fr")
+ : "-- --- --- ---"}
+
+
+
+
+
+
+
Data wells available
+
+ {serverInfo?.TotalDataleaks
+ ? serverInfo.TotalDataleaks.toLocaleString("fr")
+ : "---"}
+
+
+
+
+
+
+
Storage used
+
+ {serverInfo?.TotalSize
+ ? mbToGb(serverInfo.TotalSize).toLocaleString("fr") + " Gb"
+ : "--- Gb"}
+
+
+
diff --git a/front/src/lib/components/logo.svelte b/front/src/lib/components/logo.svelte
new file mode 100644
index 0000000..665ae9b
--- /dev/null
+++ b/front/src/lib/components/logo.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/front/src/lib/components/server-dialog.svelte b/front/src/lib/components/server-dialog.svelte
new file mode 100644
index 0000000..f3b8ba1
--- /dev/null
+++ b/front/src/lib/components/server-dialog.svelte
@@ -0,0 +1,144 @@
+
+
+
+
+
+ {#if $serverUrl !== ""}
+
+ {:else}
+
+
+ {/if}
+
+
+
{
+ isModalOpen = !isModalOpen;
+ }}
+ class={cn(className, "btn btn-ghost btn-primary")}
+ >
+
+ {text}
+
+
+
+
+
+
+
+
+
+
Connect to your server
+
+ You can connect to your own Eleakxir server by providing the server
+ URL and an optional password.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Optional
+
+
+
+ Test
+ Save
+
+
+
+
+
diff --git a/front/src/lib/components/table.svelte b/front/src/lib/components/table.svelte
new file mode 100644
index 0000000..b5b6378
--- /dev/null
+++ b/front/src/lib/components/table.svelte
@@ -0,0 +1,79 @@
+
+
+
+
+ {#if Array.isArray(row) && row.length !== 0}
+ {@const head = Object.entries(row[0])}
+
+
+
+ {#each head as [key, _]}
+
+ {key}
+
+ {/each}
+
+
+
+ {#each row as item}
+
+ {#each Object.entries(item) as [key, value]}
+
+ {#if key.toLowerCase() === "url" && value !== "" && value !== null}
+
+ {value}
+
+
+ {:else}
+ {value}
+ {/if}
+
+ {/each}
+
+ {/each}
+
+ {:else}
+
+ {#each Object.entries(row) as [key, value]}
+ {#if key !== "source" && value !== "" && value !== null}
+
+ {key.replace(/_/g, " ")}
+
+
+ {#if key.toLowerCase() === "url"}
+
+ {value}
+
+
+ {:else}
+ {value}
+ {/if}
+
+
+ {/if}
+ {/each}
+
+ {/if}
+
+
diff --git a/front/src/lib/navigation/sidebar-menu-item.svelte b/front/src/lib/navigation/sidebar-menu-item.svelte
new file mode 100644
index 0000000..597f303
--- /dev/null
+++ b/front/src/lib/navigation/sidebar-menu-item.svelte
@@ -0,0 +1,30 @@
+
+
+
+ {#if item.items}
+
+
+ {#if item.icon}
+
+ {/if}
+ {item.title}
+
+
+ {#each item.items as subitem}
+
+ {/each}
+
+
+ {:else}
+
+ {#if item.icon}
+
+ {/if}
+ {item.title}
+
+ {/if}
+
diff --git a/front/src/lib/navigation/sidebar.svelte b/front/src/lib/navigation/sidebar.svelte
new file mode 100644
index 0000000..97c0639
--- /dev/null
+++ b/front/src/lib/navigation/sidebar.svelte
@@ -0,0 +1,57 @@
+
+
+
+
+
+
diff --git a/front/src/lib/navigation/topbar.svelte b/front/src/lib/navigation/topbar.svelte
new file mode 100644
index 0000000..6448cb0
--- /dev/null
+++ b/front/src/lib/navigation/topbar.svelte
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/front/src/lib/stores/server.ts b/front/src/lib/stores/server.ts
new file mode 100644
index 0000000..750c0f8
--- /dev/null
+++ b/front/src/lib/stores/server.ts
@@ -0,0 +1,15 @@
+import { writable } from "svelte/store";
+
+function persistent(key: string, initial: any) {
+ const stored = localStorage.getItem(key);
+ const data = writable(stored ? stored : initial);
+
+ data.subscribe((value) => {
+ localStorage.setItem(key, value);
+ });
+
+ return data;
+}
+
+export const serverUrl = persistent("serverUrl", "");
+export const serverPassword = persistent("serverPassword", "");
diff --git a/front/src/lib/types.ts b/front/src/lib/types.ts
new file mode 100644
index 0000000..7344dfd
--- /dev/null
+++ b/front/src/lib/types.ts
@@ -0,0 +1,78 @@
+type Query = {
+ Text: string;
+ Column: string;
+ ExactMatch: boolean;
+};
+
+type LeakResult = {
+ Duration: number;
+ Error: string;
+ Rows: Array>;
+};
+
+type GithubResult = {
+ Duration: number;
+ Error: string;
+
+ EmailResult: any;
+ UsernameResult: any;
+};
+
+type Result = {
+ Id: string;
+ Status: "pending" | "completed";
+ Date: string;
+ Query: Query;
+ LeakResult: LeakResult;
+ GithubResult: GithubResult;
+};
+
+type HistoryItem = {
+ Id: string;
+ Status: "pending" | "completed";
+ Date: string;
+ Query: Query;
+ Results: number;
+};
+
+type History = HistoryItem[];
+
+type ServerSettings = {
+ Folders: string[];
+ CacheFolder: string;
+ Limit: number;
+ MinimumQueryLength: number;
+
+ GithubRecon: boolean;
+ GithubTokenLoaded: boolean;
+ GithubDeepMode: boolean;
+};
+
+type Server = {
+ Settings: ServerSettings;
+
+ Dataleaks: Dataleak[];
+
+ TotalRows: number;
+ TotalDataleaks: number;
+ TotalSize: number;
+};
+
+type Dataleak = {
+ Name: string;
+ Columns: string[];
+ Length: number;
+ Size: number;
+};
+
+export type {
+ Query,
+ LeakResult,
+ History,
+ HistoryItem,
+ GithubResult,
+ Result,
+ ServerSettings,
+ Server,
+ Dataleak,
+};
diff --git a/front/src/lib/utils.ts b/front/src/lib/utils.ts
new file mode 100644
index 0000000..fb8be92
--- /dev/null
+++ b/front/src/lib/utils.ts
@@ -0,0 +1,80 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+// take "2025-09-13T21:14:46.13030464+02:00"
+// return "13/09/2025 21:14"
+export function formatDate(date: string) {
+ const d = new Date(date);
+ const day = String(d.getDate()).padStart(2, "0");
+ const month = String(d.getMonth() + 1).padStart(2, "0");
+ const year = d.getFullYear();
+ const hours = String(d.getHours()).padStart(2, "0");
+ const minutes = String(d.getMinutes()).padStart(2, "0");
+ return `${day}/${month}/${year} ${hours}:${minutes}`;
+}
+
+export function convertNanoSeconds(nanoseconds: number): string {
+ const ONE_MS_IN_NS = 1e6;
+ const ONE_S_IN_NS = 1e9;
+ const ONE_MIN_IN_NS = 6e10;
+
+ if (nanoseconds < ONE_MS_IN_NS) {
+ return `${nanoseconds} ns`; // Garde la sortie en ns pour les très petites valeurs
+ } else if (nanoseconds < ONE_S_IN_NS) {
+ const ms = Math.round(nanoseconds / ONE_MS_IN_NS);
+ return `${ms} ms`;
+ } else if (nanoseconds < ONE_MIN_IN_NS) {
+ const s = Math.round(nanoseconds / ONE_S_IN_NS);
+ return `${s} s`;
+ } else {
+ const totalSeconds = Math.round(nanoseconds / ONE_S_IN_NS);
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${minutes}m${seconds}s`;
+ }
+}
+
+type FlatObject = { [key: string]: any };
+
+export function FlattenObject(obj: object): FlatObject {
+ const flattened: FlatObject = {};
+
+ function recurse(currentObj: any, prefix: string = ""): void {
+ for (const key in currentObj) {
+ if (Object.prototype.hasOwnProperty.call(currentObj, key)) {
+ const value = currentObj[key];
+ const newKey = prefix ? `${prefix}.${key}` : key;
+
+ if (
+ typeof value === "object" &&
+ value !== null &&
+ !Array.isArray(value)
+ ) {
+ // Si la valeur est un objet, on continue la rΓ©cursion
+ recurse(value, newKey);
+ } else if (Array.isArray(value)) {
+ // Si la valeur est un tableau, on itère sur ses éléments
+ value.forEach((item, index) => {
+ // On continue la rΓ©cursion pour les objets dans le tableau
+ if (typeof item === "object" && item !== null) {
+ recurse(item, `${newKey}.${index}`);
+ } else {
+ // On ajoute les valeurs primitives
+ flattened[`${newKey}.${index}`] = item;
+ }
+ });
+ } else {
+ // Si la valeur est une primitive, on l'ajoute Γ l'objet aplati
+ flattened[newKey] = value;
+ }
+ }
+ }
+ }
+
+ recurse(obj);
+ return flattened;
+}
diff --git a/front/src/main.ts b/front/src/main.ts
new file mode 100644
index 0000000..dd9a3e8
--- /dev/null
+++ b/front/src/main.ts
@@ -0,0 +1,5 @@
+import { mount } from "svelte";
+import App from "./App.svelte";
+import "sv-router/generated";
+
+mount(App, { target: document.querySelector("#app")! });
diff --git a/front/src/routes/dataleaks/index.svelte b/front/src/routes/dataleaks/index.svelte
new file mode 100644
index 0000000..6d5d59a
--- /dev/null
+++ b/front/src/routes/dataleaks/index.svelte
@@ -0,0 +1,57 @@
+
+
+
+
+ {#if serverInfo}
+
+
+
+
+ {:else}
+ Loading...
+ {/if}
+
diff --git a/front/src/routes/index.svelte b/front/src/routes/index.svelte
new file mode 100644
index 0000000..0c0d44e
--- /dev/null
+++ b/front/src/routes/index.svelte
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
βοΈ How Eleakxir works?
+
+ You run an Elixir server that manages parquet files from various
+ leaked data sources and multiple OSINT tools. The web client connects
+ to your server via HTTPS and authenticated headers then you can search
+ across indexed leaks and OSINT tools, browse results interactively and
+ review history and stats
+
+
+ And it's open source!
+
+
+
+
+
+
+
+
+
+
π Features
+
+ {#each [{ title: "π Private by design", content: "connect to your own Eleakxir server with a custom URL + password." }, { title: "π Open source & extensible", content: "hack it, self-host it, extend it." }, { title: "π Efficient File Format", content: "Uses the columnar Parquet format for high compression and rapid query performance." }, { title: "π OSINT Tools", content: "Includes Github-recon, GHunt, sherlock and more." }, { title: "π Standardized Schema", content: "Includes a detailed guide on how to normalize your data leaks for consistent and effective searching across different breaches." }] as value}
+
+
+
{value.title}
+
+ {value.content}
+
+
+
+ {/each}
+
+
+
+
+
π’ Speed
+
+ While Eleakxir is designed to be storage-efficient rather than
+ lightning-fast, searches will naturally take longer compared to an indexed
+ engine like Elasticsearch. Indexing systems can provide near-instant
+ results, but at the cost of massive disk usage β often requiring multiple
+ terabytes even for relatively modest datasets. In contrast, Eleakxir
+ trades some speed for compactness: for example, Iβm able to store 25
+ billion rows in just over 600 GB on entry-level hardware. A query might
+ take around an hour to complete, but the key point is that itβs actually
+ possible to run such searches at home β something that would be completely
+ out of reach if I had to maintain Elasticsearchβs much larger index
+ footprint.
+
+
+
+
+
π¨ Disclaimer
+
+ Eleakxir is provided for educational and research purposes only. You are
+ solely responsible for how you use this software. Accessing, storing, or
+ distributing leaked data may be illegal in your jurisdiction. The authors
+ and contributors do not condone or promote illegal activity. Use
+ responsibly and only with data you are legally permitted to process.
+
+
+
+
+
diff --git a/front/src/routes/layout.svelte b/front/src/routes/layout.svelte
new file mode 100644
index 0000000..366ce8a
--- /dev/null
+++ b/front/src/routes/layout.svelte
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+ {@render children()}
+
+
+
diff --git a/front/src/routes/leak-utils/index.svelte b/front/src/routes/leak-utils/index.svelte
new file mode 100644
index 0000000..c2f8f08
--- /dev/null
+++ b/front/src/routes/leak-utils/index.svelte
@@ -0,0 +1,18 @@
+
+
+
+ {@html marked(text)}
+
diff --git a/front/src/routes/parquet/index.svelte b/front/src/routes/parquet/index.svelte
new file mode 100644
index 0000000..3acb2ba
--- /dev/null
+++ b/front/src/routes/parquet/index.svelte
@@ -0,0 +1,18 @@
+
+
+
+ {@html marked(text)}
+
diff --git a/front/src/routes/search/[id]/index.svelte b/front/src/routes/search/[id]/index.svelte
new file mode 100644
index 0000000..6ec01a1
--- /dev/null
+++ b/front/src/routes/search/[id]/index.svelte
@@ -0,0 +1,226 @@
+
+
+
+ {#if result}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Data wells lookup
+
+ {#if result.LeakResult.Error !== ""}
+
+ {:else if result.LeakResult.Duration === 0}
+
+ {:else if result.LeakResult.Rows.length > 0}
+
+ {:else}
+
+ {/if}
+
+
+ {#if result.LeakResult.Error !== ""}
+
+
+ Error! {result.LeakResult.Error}
+
+ {:else if result.LeakResult.Duration === 0}
+
+ {#each Array(5) as _}
+
+ {/each}
+
+ {:else}
+
+ {result.LeakResult.Rows.length} results in {convertNanoSeconds(
+ result.LeakResult.Duration,
+ )}
+
+
+ {/if}
+
+
+
+
+
+
+
+ Github Recon
+
+ {#if result.GithubResult.Error !== ""}
+
+ {:else if result.GithubResult.Duration === 0}
+
+ {:else if !result.GithubResult.EmailResult?.Commits && !result.GithubResult.EmailResult?.Spoofing && !result.GithubResult.UsernameResult?.User}
+
+ {:else if result.GithubResult.UsernameResult || result.GithubResult.EmailResult}
+
+ {/if}
+
+
+ {#if result.GithubResult.Error !== ""}
+
+
+ Error! {result.GithubResult.Error}
+
+ {:else if result.GithubResult.Duration === 0}
+
+
+ Loading...
+
+ {:else if !result.GithubResult.EmailResult?.Commits && !result.GithubResult.EmailResult?.Spoofing && !result.GithubResult.UsernameResult?.User}
+
+
+ No result
+
+ {:else}
+
+ Found a result in {convertNanoSeconds(
+ result.GithubResult.Duration,
+ )}
+
+
+ {/if}
+
+
+
+ {/if}
+
+
+
diff --git a/front/src/routes/search/index.svelte b/front/src/routes/search/index.svelte
new file mode 100644
index 0000000..f14dc05
--- /dev/null
+++ b/front/src/routes/search/index.svelte
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
History
+
+
+
+
Active services
+
+ {#if !serverInfo}
+
Loading...
+ {:else}
+
+ {/if}
+
+
+
+
Last data wells added
+
+
+
+
+
+
How to search
+
+
+
+
+
+
diff --git a/front/src/vite-env.d.ts b/front/src/vite-env.d.ts
new file mode 100644
index 0000000..4078e74
--- /dev/null
+++ b/front/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/front/svelte.config.js b/front/svelte.config.js
new file mode 100644
index 0000000..e5353cd
--- /dev/null
+++ b/front/svelte.config.js
@@ -0,0 +1,6 @@
+/** @type {import('@sveltejs/vite-plugin-svelte').SvelteConfig} */
+export default {
+ compilerOptions: {
+ runes: true,
+ },
+};
diff --git a/front/tsconfig.json b/front/tsconfig.json
new file mode 100644
index 0000000..2dc2374
--- /dev/null
+++ b/front/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "extends": ["./.router/tsconfig.json"],
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "noEmit": true,
+ "verbatimModuleSyntax": true,
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "$src": ["./src"],
+ "$src/*": ["./src/*"],
+ "$lib": ["./src/lib"],
+ "$lib/*": ["./src/lib/*"],
+ "sv-router/generated": [".router/router.ts"],
+ }
+ }
+}
diff --git a/front/vite.config.ts b/front/vite.config.ts
new file mode 100644
index 0000000..b0a10ee
--- /dev/null
+++ b/front/vite.config.ts
@@ -0,0 +1,15 @@
+import { svelte } from "@sveltejs/vite-plugin-svelte";
+import { router } from "sv-router/vite-plugin";
+import { defineConfig } from "vite";
+import tailwindcss from "@tailwindcss/vite";
+import path from "path";
+
+export default defineConfig({
+ plugins: [tailwindcss(), svelte({}), router()],
+ resolve: {
+ alias: {
+ $lib: path.resolve("./src/lib"),
+ $src: path.resolve("./src"),
+ },
+ },
+});
diff --git a/leak-utils/DATALEAKS-NORMALIZATION.md b/leak-utils/DATALEAKS-NORMALIZATION.md
new file mode 100644
index 0000000..8fb4d9b
--- /dev/null
+++ b/leak-utils/DATALEAKS-NORMALIZATION.md
@@ -0,0 +1,138 @@
+# Rules for handling Data Leaks
+
+This normalization framework is designed to standardize data leaks for
+[Eleakxir](https://github.com/anotherhadi/eleakxir), the open-source search
+engine, using
+[leak-utils](https://github.com/anotherhadi/eleakxir-temp/blob/main/leak-utils/README.md),
+a dedicated CLI tool that converts and cleans files for efficient indexing and
+searching.
+
+## The Relevance of Parquet for Data Leaks
+
+Parquet is an efficient, open-source columnar storage file format designed to
+handle complex data in bulk. When dealing with data leaks, its choice is highly
+relevant for several reasons:
+
+- **Compression**: Parquet files offer superior compression compared to
+ row-based formats like CSV. By storing data column by column, it applies more
+ effective compression algorithms, which significantly reduces disk space. For
+ data leaks, where file sizes can range from gigabytes to terabytes, this is
+ crucial for minimizing storage costs.
+- **Query Performance**: As a columnar format, Parquet allows you to read only
+ the specific columns you need for a query. In a data leak, you might only be
+ interested in emails and passwords, not full addresses or phone numbers. This
+ selective reading drastically speeds up search operations, as the system
+ doesn't have to scan through entire rows of irrelevant data.
+- **Efficiency**: The format is optimized for analytics. It stores data with
+ metadata and statistics (min/max values) for each column, allowing for query
+ **pruning**. This means a query can skip entire blocks of data that don't
+ match the filtering criteria, boosting performance even further.
+
+## Disclaimer
+
+The information in this document is provided **for research and educational
+purposes only**. I am **not responsible** for how this data, methods, or
+guidelines are used. Any misuse, unlawful activity, or harm resulting from
+applying this content is the sole responsibility of the individual or
+organization using it.
+
+## File Naming Convention
+
+- **Lowercase only**, ASCII (no accents).
+- **Separators**:
+ - `_` inside blocks (`date_2023_10`)
+ - `-` between blocks (`instagram.com-date_2023_10`)
+- **Prefix**: always start with the **source name/url** (e.g., `instagram.com`,
+ `alien_txt`).
+- **Blocks**: each additional part must be prefixed by its block name:
+
+ - `date_YYYY[_MM[_DD]]` β use ISO format (year, or year-month, or full date).
+ - `source_*` β origin of the leak (e.g., `scrape`, `dump`, `combo`).
+ - `version_v*` β versioning if regenerated or transformed.
+ - `notes_*` β optional clarifications.
+- **Extension**: always `.parquet`.
+
+**Recommended pattern:**
+
+```txt
+{source}-date_{YYYY[_MM[_DD]]}-source_{origin}-version_{vN}-notes_{info}.parquet
+```
+
+**Examples:**
+
+```txt
+instagram.com-date_2023_10.parquet
+alien_txt-date_2022-source_dump.parquet
+combo_french-notes_crypto.parquet
+```
+
+## Column Naming Convention
+
+- **snake\_case only** (lowercase, `_` separator).
+
+- **No dots (`.`)** in column names (`husband.phone` β `husband_phone`).
+
+- **Allowed characters**: `[a-z0-9_]+` (no spaces, hyphens, or accents).
+
+- **Multiple variants of the same field**:
+
+ - Relations β prefix clearly: `husband_phone`, `mother_last_name`.
+ - Multiples of the same type β numbered prefix: `1_phone`, `2_phone`,
+ `3_phone`.
+ - Always end with the column "type" (e.g., `_phone`, `_last_name`).
+
+- **Rename if mislabeled**: If a `username` column actually contains only emails
+ rename it to `email`.
+
+- **Remove irrelevant columns**: Drop meaningless identifiers like `id` or
+ fields with no analytical value.
+
+- **Standard columns**: to enable schema alignment across leaks:
+
+ | Column |
+ | ------------- |
+ | email |
+ | username |
+ | password |
+ | password_hash |
+ | phone |
+ | date |
+ | birth_date |
+ | age |
+ | first_name |
+ | last_name |
+ | full_name |
+ | address |
+ | city |
+ | country |
+ | state |
+ | postal_code |
+ | ip |
+ | url |
+ | city |
+
+## Standard Column Formatting
+
+- **Email**: lowercase, trimmed, keep only `[^a-z0-9._@-]`.
+
+- **Phone**: keep only `[^0-9]`
+
+- **Names**:
+
+ - Keep `first_name` / `last_name` if present.
+ - Generate `full_name = CONCAT(first_name, ' ', last_name)`.
+ - If only `name` exists, rename it to `full_name`.
+
+- **Passwords**:
+
+ - Hashes β `password_hash`.
+ - Plaintext β `password`.
+ - Never mix hashes and plaintext in the same column.
+
+- **NULLs**: always use SQL `NULL` (never `""` or `"NULL"`).
+
+## Deduplication
+
+Deduplication is often **impractical at scale** (billions of rows). Do **not**
+attempt to deduplicate at ingestion time. Instead, handle deduplication **after
+running a search** to optimize performance and storage.
diff --git a/leak-utils/README.md b/leak-utils/README.md
new file mode 100644
index 0000000..b9c4058
--- /dev/null
+++ b/leak-utils/README.md
@@ -0,0 +1,107 @@
+# π leak-utils: The Eleakxir Data Utility Toolkit
+
+`leak-utils` is a powerful command-line tool built to help you manage, process,
+and optimize data leaks for use with the **Eleakxir** search engine. It provides
+a suite of utilities for data cleaning, format conversion, and file
+manipulation, all designed to ensure your data wells are efficient and
+standardized.
+
+`leak-utils` is written in **Go** and leverages **DuckDB** for its
+high-performance in-memory processing, ensuring fast and reliable operations on
+large datasets.
+
+## π Features
+
+- **Parquet File Management**: Clean and inspect existing `.parquet` files.
+- **Format Conversion**: Seamlessly convert `.csv`, `.txt`, `.json` files into
+ the optimized `.parquet` format.
+- **Schema Uniformity**: Tools designed to help you standardize and normalize
+ your data to align with the
+ [Eleakxir data leak normalization rules](./DATALEAKS-NORMALIZATION.md). This
+ ensures a consistent schema across all your files, which is crucial for
+ efficient searching and consistent results.
+- **High Performance**: Built with Go and DuckDB for fast and efficient data
+ processing.
+
+## βοΈ How to Use
+
+The tool operates via a single executable with different commands, each
+corresponding to a specific action. You can find the executable in the
+`leak-utils` directory of the Eleakxir project.
+
+### Install
+
+#### With go
+
+```bash
+go install "github.com/anotherhadi/eleakxir/leak-utils@latest"
+```
+
+#### With Nix/NixOS
+
+
+Click to expand
+
+**From anywhere (using the repo URL):**
+
+```bash
+nix run "github:anotherhadi/eleakxir#leak-utils" -- action [--flags value]
+```
+
+**Permanent Installation:**
+
+```bash
+# add the flake to your flake.nix
+{
+ inputs = {
+ eleakxir.url = "github:anotherhadi/eleakxir";
+ };
+}
+
+# then add it to your packages
+environment.systemPackages = with pkgs; [ # or home.packages
+ eleakxir.packages.${pkgs.system}.leak-utils
+];
+```
+
+
+
+### Available Actions
+
+#### `cleanParquet`
+
+Optimizes and cleans an existing Parquet file. This can be used to change
+columns, clean rows, ...
+
+See:
+
+```bash
+leak-utils cleanParquet --help
+```
+
+#### `infoParquet`
+
+Displays metadata and schema information for a given Parquet file. Useful for
+inspecting file structure and column types.
+
+#### `csvToParquet`
+
+Converts a `.csv` file into a highly compressed and efficient `.parquet` file.
+This is the recommended way to prepare your data for Eleakxir.
+
+#### `mergeFiles`
+
+Merges multiple files (of the same type) into a single, larger file. This is
+useful for combining smaller data leaks.
+
+#### `removeUrlSchemeFromUlp`
+
+This utility prevents the colon (`:`) in URL schemes like `https://` from being
+mistakenly parsed as a column separator when processing ULP data in flat files
+like CSV or TXT.
+
+## π€ Contributing
+
+[Contributions](../CONTRIBUTING.md) to `leak-utils` are welcome! Feel free to
+open issues or submit pull requests for new features, bug fixes, or performance
+improvements.
diff --git a/leak-utils/go.mod b/leak-utils/go.mod
new file mode 100644
index 0000000..4e7da71
--- /dev/null
+++ b/leak-utils/go.mod
@@ -0,0 +1,42 @@
+module github.com/anotherhadi/eleakxir/leak-utils
+
+go 1.25.0
+
+require (
+ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1
+ github.com/charmbracelet/log v0.4.2
+ github.com/marcboeker/go-duckdb v1.8.5
+ github.com/spf13/pflag v1.0.10
+)
+
+require (
+ github.com/apache/arrow-go/v18 v18.1.0 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/colorprofile v0.3.0 // indirect
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
+ github.com/charmbracelet/x/ansi v0.8.0 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/google/flatbuffers v25.1.24+incompatible // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/klauspost/compress v1.17.11 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/pierrec/lz4/v4 v4.1.22 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/zeebo/xxh3 v1.0.2 // indirect
+ golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
+ golang.org/x/mod v0.22.0 // indirect
+ golang.org/x/sync v0.10.0 // indirect
+ golang.org/x/sys v0.31.0 // indirect
+ golang.org/x/tools v0.29.0 // indirect
+ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
+)
diff --git a/leak-utils/go.sum b/leak-utils/go.sum
new file mode 100644
index 0000000..2c61a9f
--- /dev/null
+++ b/leak-utils/go.sum
@@ -0,0 +1,94 @@
+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y=
+github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0=
+github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
+github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ=
+github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
+github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
+github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o=
+github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
+github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0=
+github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
+github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
+golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
+golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
+golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
+gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/leak-utils/leak-utils/main.go b/leak-utils/leak-utils/main.go
new file mode 100644
index 0000000..8461207
--- /dev/null
+++ b/leak-utils/leak-utils/main.go
@@ -0,0 +1,145 @@
+package main
+
+import (
+ "database/sql"
+ "fmt"
+ "os"
+ "slices"
+ "strings"
+
+ "github.com/anotherhadi/eleakxir/leak-utils/misc"
+ "github.com/anotherhadi/eleakxir/leak-utils/parquet"
+ "github.com/anotherhadi/eleakxir/leak-utils/settings"
+ "github.com/charmbracelet/log"
+ _ "github.com/marcboeker/go-duckdb"
+ flag "github.com/spf13/pflag"
+)
+
+func main() {
+ db, err := sql.Open("duckdb", "")
+ if err != nil {
+ log.Fatal("Failed to open DuckDB", "error", err)
+ }
+ defer db.Close()
+ lu := settings.LeakUtils{
+ Db: db,
+ }
+ actions := []string{
+ "cleanParquet",
+ "infoParquet",
+ // Csv
+ "csvToParquet",
+ // Misc
+ "mergeFiles",
+ "removeUrlSchemeFromUlp",
+ }
+
+ if len(os.Args) < 2 {
+ fmt.Println(settings.Muted.Render("Usage: "), settings.Accent.Render(os.Args[0], ""))
+ fmt.Println(settings.Muted.Render("Actions: "), settings.Base.Render(strings.Join(actions, ", ")))
+ return
+ }
+ action := os.Args[1]
+ if !slices.Contains(actions, action) {
+ log.Fatal("Unknown action", "action", action)
+ }
+
+ switch action {
+ case "cleanParquet":
+ var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file")
+ var outputFile *string = flag.StringP("output", "o", "", "Output Parquet file")
+ var compression *string = flag.StringP("compression", "c", "ZSTD", "Compression codec (UNCOMPRESSED, SNAPPY, GZIP, BROTLI, LZ4, ZSTD)")
+ var skipLineFormating *bool = flag.BoolP("skip-line-formating", "s", false, "Skip line formating")
+ var deleteFirstRow *bool = flag.Bool("delete-first-row", false, "Delete first row")
+ var debug *bool = flag.Bool("debug", false, "Debug mode")
+ var noColors *bool = flag.Bool("no-colors", false, "Remove all colors")
+ var printQuery *bool = flag.BoolP("print-query", "p", false, "Print the query instead of executing it")
+ flag.Parse()
+ if *inputFile == "" || *outputFile == "" {
+ log.Fatal("Input and output files are required")
+ }
+ if *noColors {
+ settings.DisableColors()
+ }
+ lu.Compression = *compression
+ lu.Debug = *debug
+ err := parquet.CleanParquet(lu, *inputFile, *outputFile, *skipLineFormating, *deleteFirstRow, *printQuery)
+ if err != nil {
+ log.Fatal("Failed to clean Parquet file", "error", err)
+ }
+ return
+ case "infoParquet":
+ var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file")
+ var debug *bool = flag.Bool("debug", false, "Debug mode")
+ var noColors *bool = flag.Bool("no-colors", false, "Remove all colors")
+ flag.Parse()
+ if *inputFile == "" {
+ log.Fatal("Input files are required")
+ }
+ if *noColors {
+ settings.DisableColors()
+ }
+ lu.Debug = *debug
+ err := parquet.InfoParquet(lu, *inputFile)
+ if err != nil {
+ log.Fatal("Failed to read Parquet file", "error", err)
+ }
+ return
+ case "csvToParquet":
+ var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file")
+ var outputFile *string = flag.StringP("output", "o", "", "Output Parquet file")
+ var strict *bool = flag.Bool("strict", true, "Strict mode for Duckdb")
+ var compression *string = flag.StringP("compression", "c", "ZSTD", "Compression codec (UNCOMPRESSED, SNAPPY, GZIP, BROTLI, LZ4, ZSTD)")
+ var noColors *bool = flag.Bool("no-colors", false, "Remove all colors")
+ var debug *bool = flag.Bool("debug", false, "Debug mode")
+ flag.Parse()
+ if *inputFile == "" || *outputFile == "" {
+ log.Fatal("Input and output files are required")
+ }
+ if *noColors {
+ settings.DisableColors()
+ }
+ lu.Compression = *compression
+ lu.Debug = *debug
+ err := misc.CsvToParquet(lu, *inputFile, *outputFile, *strict)
+ if err != nil {
+ log.Fatal("Failed to transform Csv file", "error", err)
+ }
+ return
+ case "mergeFiles":
+ var inputFiles *[]string = flag.StringArrayP("inputs", "i", []string{}, "Input Parquet files")
+ var outputFile *string = flag.StringP("output", "o", "", "Output Parquet file")
+ var noColors *bool = flag.Bool("no-colors", false, "Remove all colors")
+ var debug *bool = flag.Bool("debug", false, "Debug mode")
+ flag.Parse()
+ if len(*inputFiles) == 0 || *outputFile == "" {
+ log.Fatal("Inputs and output files are required")
+ }
+ if *noColors {
+ settings.DisableColors()
+ }
+ lu.Debug = *debug
+ err := misc.MergeFiles(lu, *outputFile, *inputFiles...)
+ if err != nil {
+ log.Fatal("Failed to merge files", "error", err)
+ }
+ return
+ case "removeUrlSchemeFromUlp":
+ var inputFile *string = flag.StringP("input", "i", "", "Input Parquet file")
+ var noColors *bool = flag.Bool("no-colors", false, "Remove all colors")
+ var debug *bool = flag.Bool("debug", false, "Debug mode")
+ flag.Parse()
+ if *inputFile == "" {
+ log.Fatal("Input files are required")
+ }
+ if *noColors {
+ settings.DisableColors()
+ }
+ lu.Debug = *debug
+ err := misc.RemoveUrlSchemeFromUlp(lu, *inputFile)
+ if err != nil {
+ log.Fatal("Failed to remove ULP Url schemes", "error", err)
+ }
+ return
+ }
+}
diff --git a/leak-utils/misc/csv.go b/leak-utils/misc/csv.go
new file mode 100644
index 0000000..f5750ad
--- /dev/null
+++ b/leak-utils/misc/csv.go
@@ -0,0 +1,173 @@
+package misc
+
+import (
+ "bufio"
+ "encoding/csv"
+ "fmt"
+ "io"
+ "os"
+ "slices"
+ "strings"
+
+ "github.com/anotherhadi/eleakxir/leak-utils/settings"
+ "github.com/charmbracelet/log"
+)
+
+func CsvToParquet(lu settings.LeakUtils, inputFile string, outputFile string, strict bool) error {
+ hasHeader, err := csvHasHeader(inputFile)
+ if err != nil {
+ return err
+ }
+ header := "true"
+ if !hasHeader {
+ header = "false"
+ }
+ strictMode := "true"
+ if !strict {
+ strictMode = "false"
+ }
+
+ delimiter := getDelimiter(inputFile)
+
+ query := fmt.Sprintf(`CREATE TABLE my_table AS FROM read_csv_auto('%s', HEADER=%s, delim='%s', ignore_errors=true, all_varchar=true, null_padding=true, strict_mode=%s);
+ COPY my_table TO '%s' (FORMAT 'parquet', COMPRESSION '%s', ROW_GROUP_SIZE 200_000);`,
+ inputFile, header, delimiter, strictMode, outputFile, lu.Compression)
+
+ if lu.Debug {
+ log.Info("Detected delimiter", "delimiter", delimiter)
+ log.Info("CSV header detection", "hasHeader", hasHeader)
+ log.Info("Executing query", "query", query)
+ }
+
+ _, err = lu.Db.Exec(query)
+
+ if lu.Debug {
+ log.Info("Finished executing query")
+ }
+
+ return err
+}
+
+func getDelimiter(inputFile string) string {
+ lines, err := getNLine(inputFile, 10, 0)
+ if err != nil {
+ log.Warn("Failed to read CSV file to determine delimiter, defaulting to comma", "error", err)
+ return ","
+ }
+
+ delimiterCounts := map[string]int{
+ ",": 0,
+ ";": 0,
+ "\t": 0,
+ "|": 0,
+ ":": 0,
+ }
+
+ for _, line := range lines {
+ for d := range delimiterCounts {
+ delimiterCounts[d] += strings.Count(line, d)
+ }
+ }
+
+ maxCount := 0
+ delimiter := ","
+
+ for d, count := range delimiterCounts {
+ if count > maxCount {
+ maxCount = count
+ delimiter = d
+ }
+ }
+
+ return delimiter
+}
+
+func csvHasHeader(inputFile string) (hasHeader bool, err error) {
+ firstRow, err := getFirstRowCsv(inputFile)
+ if err != nil {
+ return false, err
+ }
+ for i, col := range firstRow {
+ col = strings.ReplaceAll(col, "\"", "")
+ col = strings.ReplaceAll(col, " ", "")
+ col = strings.ReplaceAll(col, "-", "")
+ col = strings.ReplaceAll(col, "_", "")
+ col = strings.ReplaceAll(col, ".", "")
+ firstRow[i] = strings.ToLower(strings.TrimSpace(col))
+ }
+ knownHeaders := []string{"email", "password", "username", "phone", "lastname", "firstname"}
+ for _, knownHeader := range knownHeaders {
+ if slices.Contains(firstRow, knownHeader) {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func getNLine(inputFile string, n, offset int) (lines []string, err error) {
+ if n <= 0 {
+ return nil, nil
+ }
+
+ if offset < 0 {
+ offset = 0
+ }
+
+ file, err := os.Open(inputFile)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ currentLine := 0
+
+ for scanner.Scan() {
+ currentLine++
+ if currentLine <= offset {
+ continue
+ }
+
+ lines = append(lines, scanner.Text())
+ if len(lines) >= n {
+ break
+ }
+ }
+
+ if err := scanner.Err(); err != nil && err != io.EOF {
+ return nil, err
+ }
+
+ return lines, nil
+}
+
+func getFirstRowCsv(inputFile string) (row []string, err error) {
+ rows, err := getFirstNRowsCsv(inputFile, 1)
+ if len(rows) == 0 {
+ return nil, fmt.Errorf("no rows found in CSV")
+ }
+ return rows[0], err
+}
+
+func getFirstNRowsCsv(inputFile string, n int) (rows [][]string, err error) {
+ f, err := os.Open(inputFile)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open file: %w", err)
+ }
+ defer f.Close()
+
+ reader := csv.NewReader(f)
+
+ for i := 0; i < n; i++ {
+ row, err := reader.Read()
+ if err != nil {
+ if err.Error() == "EOF" {
+ break
+ }
+ return nil, fmt.Errorf("failed to read CSV: %w", err)
+ }
+ rows = append(rows, row)
+ }
+
+ return rows, nil
+}
diff --git a/leak-utils/misc/misc.go b/leak-utils/misc/misc.go
new file mode 100644
index 0000000..983690f
--- /dev/null
+++ b/leak-utils/misc/misc.go
@@ -0,0 +1,31 @@
+package misc
+
+import (
+ "io"
+ "os"
+
+ "github.com/anotherhadi/eleakxir/leak-utils/settings"
+)
+
+func MergeFiles(lu settings.LeakUtils, outputFile string, inputFiles ...string) error {
+ out, err := os.Create(outputFile)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ for _, inputFile := range inputFiles {
+ file, err := os.Open(inputFile)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ _, err = io.Copy(out, file)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/leak-utils/misc/ulp.go b/leak-utils/misc/ulp.go
new file mode 100644
index 0000000..40d7b04
--- /dev/null
+++ b/leak-utils/misc/ulp.go
@@ -0,0 +1,67 @@
+package misc
+
+import (
+ "bufio"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/anotherhadi/eleakxir/leak-utils/settings"
+)
+
+func RemoveUrlSchemeFromUlp(lu settings.LeakUtils, inputFile string) error {
+ file, err := os.Open(inputFile)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ outputFile := inputFile + ".clean"
+ out, err := os.Create(outputFile)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ reader := bufio.NewReader(file)
+ writer := bufio.NewWriter(out)
+
+ for {
+ line, err := reader.ReadString('\n')
+ if err != nil && err != io.EOF {
+ return err
+ }
+
+ firstColumn := strings.Index(line, ":")
+ firstScheme := strings.Index(line, "://")
+ if firstScheme != -1 && firstColumn == firstScheme {
+ line = line[firstScheme+3:]
+ }
+
+ _, werr := writer.WriteString(line)
+ if werr != nil {
+ return err
+ }
+
+ if err == io.EOF {
+ break
+ }
+ }
+
+ err = writer.Flush()
+ if err != nil {
+ return err
+ }
+
+ err = os.Remove(inputFile)
+ if err != nil {
+ return err
+ }
+
+ err = os.Rename(outputFile, inputFile)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/leak-utils/parquet/format.go b/leak-utils/parquet/format.go
new file mode 100644
index 0000000..1579a03
--- /dev/null
+++ b/leak-utils/parquet/format.go
@@ -0,0 +1,107 @@
+package parquet
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/anotherhadi/eleakxir/leak-utils/settings"
+)
+
+// If there is no full_name but there is last_name and first_name, create full_name
+// If there is no full_name, no last_name or no first_name, but there is name, rename name to full_name
+func addFullname(operations []ColumnOperation) []ColumnOperation {
+ hasFullName := false
+ hasFirstName := false
+ hasLastName := false
+ hasName := false
+ for _, op := range operations {
+ if op.Action != "drop" {
+ if op.NewName == "full_name" {
+ hasFullName = true
+ } else if op.NewName == "first_name" {
+ hasFirstName = true
+ } else if op.NewName == "last_name" {
+ hasLastName = true
+ } else if op.NewName == "name" {
+ hasName = true
+ }
+ }
+ }
+ if hasFullName {
+ return operations
+ }
+ if hasFirstName && hasLastName {
+ operations = append(operations, ColumnOperation{
+ OriginalName: "first_name || ' ' || last_name",
+ NewName: "full_name",
+ Action: "rename",
+ })
+ fmt.Println(settings.Muted.Render("\nAdding new column 'full_name' as concatenation of 'first_name' and 'last_name'."))
+ return operations
+ }
+ if hasName {
+ for i, op := range operations {
+ if op.NewName == "name" && op.Action != "drop" {
+ operations[i].NewName = "full_name"
+ fmt.Println(settings.Muted.Render("\nRenaming column 'name' to 'full_name'."))
+ return operations
+ }
+ }
+ }
+ if hasFirstName {
+ operations = append(operations, ColumnOperation{
+ OriginalName: "first_name",
+ NewName: "full_name",
+ Action: "rename",
+ })
+ fmt.Println(settings.Muted.Render("\nAdding new column 'full_name' from 'first_name'."))
+ return operations
+ }
+ if hasLastName {
+ operations = append(operations, ColumnOperation{
+ OriginalName: "last_name",
+ NewName: "full_name",
+ Action: "rename",
+ })
+ fmt.Println(settings.Muted.Render("\nAdding new column 'full_name' from 'last_name'."))
+ return operations
+ }
+
+ return operations
+}
+
+// formatColumnName formats a column name to be SQL-compliant.
+func formatColumnName(columnName string) string {
+ columnName = strings.TrimSpace(columnName)
+ columnName = strings.ToLower(columnName)
+ columnName = strings.Join(strings.Fields(columnName), "_")
+ columnName = strings.ReplaceAll(columnName, "\"", "")
+ columnName = strings.ReplaceAll(columnName, "'", "")
+ columnName = strings.ReplaceAll(columnName, " ", "_")
+ columnName = strings.ReplaceAll(columnName, "-", "_")
+ // Only keep a-z, 0-9 and _
+ var formatted strings.Builder
+ for _, r := range columnName {
+ if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' {
+ formatted.WriteRune(r)
+ }
+ }
+ columnName = formatted.String()
+ columnName = strings.TrimPrefix(columnName, "_")
+ columnName = strings.TrimSuffix(columnName, "_")
+ return columnName
+}
+
+// formatColumns applies specific formatting rules to column operations.
+func formatColumns(operations []ColumnOperation) []ColumnOperation {
+ formatedOperations := []ColumnOperation{}
+ for _, op := range operations {
+ if op.NewName == "phone" || strings.HasSuffix(op.NewName, "_phone") {
+ op.OriginalName = "REGEXP_REPLACE(" + op.OriginalName + ", '[^0-9]', '')"
+ } else if op.NewName == "email" || strings.HasSuffix(op.NewName, "_email") {
+ op.OriginalName = "REGEXP_REPLACE(LOWER(TRIM(" + op.OriginalName + ")), '[^a-z0-9._@-]', '')"
+ }
+ formatedOperations = append(formatedOperations, op)
+ }
+ return formatedOperations
+}
diff --git a/leak-utils/parquet/parquet.go b/leak-utils/parquet/parquet.go
new file mode 100644
index 0000000..538a982
--- /dev/null
+++ b/leak-utils/parquet/parquet.go
@@ -0,0 +1,276 @@
+package parquet
+
+import (
+ "bufio"
+ "database/sql"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/anotherhadi/eleakxir/leak-utils/settings"
+ "github.com/charmbracelet/log"
+)
+
+type Parquet struct {
+ Filepath string
+ Filename string
+ Columns []string
+ Sample [][]string
+ NRows int64
+ Compression string // Compression of the output file (e.g., "SNAPPY", "ZSTD", "NONE" or "")
+}
+
+type ColumnOperation struct {
+ OriginalName string
+ NewName string
+ Action string // "keep", "rename", "drop"
+}
+
+func (parquet Parquet) PrintParquet() {
+ fmt.Println(settings.Header.Render(parquet.Filename) + "\n")
+ fmt.Println(settings.Accent.Render("File path:"), settings.Base.Render(parquet.Filepath))
+ fmt.Println(settings.Accent.Render("Number of columns:"), settings.Base.Render(fmt.Sprintf("%d", len(parquet.Columns))))
+ fmt.Println(settings.Accent.Render("Number of rows:"), settings.Base.Render(formatWithSpaces(parquet.NRows)))
+ fmt.Println()
+ fmt.Println(settings.Accent.Render(strings.Join(parquet.Columns, " | ")))
+ for _, row := range parquet.Sample {
+ fmt.Println(settings.Base.Render(strings.Join(row, " | ")))
+ }
+}
+
+func InfoParquet(lu settings.LeakUtils, inputFile string) error {
+ parquet, err := GetParquet(lu.Db, inputFile)
+ if err != nil {
+ return err
+ }
+
+ parquet.PrintParquet()
+ return nil
+}
+
+func CleanParquet(lu settings.LeakUtils, inputFile, outputFile string, skipLineFormating, deleteFirstRow, printQuery bool) error {
+ input, err := GetParquet(lu.Db, inputFile)
+ if err != nil {
+ return err
+ }
+ input.PrintParquet()
+ columnOps := configureColumns(*input, skipLineFormating)
+ output := Parquet{
+ Filepath: outputFile,
+ Compression: lu.Compression,
+ }
+ err = transformParquet(lu, *input, output, columnOps, deleteFirstRow, printQuery)
+ return err
+}
+
+func configureColumns(input Parquet, skipLineFormating bool) []ColumnOperation {
+ reader := bufio.NewReader(os.Stdin)
+ var operations []ColumnOperation
+
+ fmt.Println()
+ fmt.Println(settings.Base.Render("For each column, choose an action:"))
+ fmt.Println(settings.Base.Render(" [k] Keep"))
+ fmt.Println(settings.Base.Render(" [r] Rename"))
+ fmt.Println(settings.Base.Render(" [d] Drop/Delete"))
+ fmt.Println(settings.Base.Render(" [s] Suggested"))
+ fmt.Println(settings.Base.Render(" [b] Go back"))
+ fmt.Println()
+
+ for i := 0; i < len(input.Columns); i++ {
+ col := input.Columns[i]
+ suggestion := getSuggestion(col)
+
+ for {
+ fmt.Println(settings.Muted.Render("\nColumn:"), settings.Accent.Render(col))
+ if suggestion != "" {
+ fmt.Println(settings.Alert.Render("Suggested action: Rename to '" + suggestion + "'"))
+ }
+ fmt.Print(settings.Base.Render("[k/r/d/s/b]: "))
+
+ input, err := reader.ReadString('\n')
+ if err != nil {
+ log.Printf("Error reading input: %v", err)
+ continue
+ }
+ input = strings.TrimSpace(strings.ToLower(input))
+
+ op := ColumnOperation{
+ OriginalName: col,
+ NewName: col,
+ Action: "keep",
+ }
+
+ switch input {
+ case "b", "back":
+ if i > 0 {
+ i -= 2
+ if len(operations) > 0 {
+ operations = operations[:len(operations)-1]
+ }
+ fmt.Println(settings.Muted.Render("Going back to the previous column..."))
+ } else {
+ fmt.Println(settings.Muted.Render("Already at the first column, cannot go back further."))
+ continue
+ }
+ goto nextColumn
+
+ case "r", "rename":
+ fmt.Print(settings.Base.Render("Enter new name: "))
+ newName, err := reader.ReadString('\n')
+ if err != nil {
+ log.Printf("Error reading new name: %v", err)
+ continue
+ }
+ newName = strings.TrimSpace(newName)
+ if newName != "" {
+ op.OriginalName = "\"" + op.OriginalName + "\""
+ op.NewName = formatColumnName(newName)
+ op.Action = "rename"
+ operations = append(operations, op)
+ goto nextColumn
+ } else {
+ fmt.Println(settings.Muted.Render("Invalid name, please try again."))
+ continue
+ }
+
+ case "s", "suggested":
+ if suggestion != "" {
+ op.OriginalName = "\"" + op.OriginalName + "\""
+ op.NewName = formatColumnName(suggestion)
+ op.Action = "rename"
+ } else {
+ fmt.Println(settings.Muted.Render("No valid suggestion available"))
+ continue
+ }
+ operations = append(operations, op)
+ goto nextColumn
+
+ case "d", "drop", "delete":
+ op.Action = "drop"
+ operations = append(operations, op)
+ goto nextColumn
+
+ case "k", "keep", "":
+ op.OriginalName = "\"" + op.OriginalName + "\""
+ op.NewName = formatColumnName(op.NewName)
+ op.Action = "rename"
+ operations = append(operations, op)
+ goto nextColumn
+
+ default:
+ fmt.Println(settings.Muted.Render("Invalid choice, please enter [k/r/d/s/b]."))
+ continue
+ }
+ }
+ nextColumn:
+ lastOp := operations[len(operations)-1]
+ switch lastOp.Action {
+ case "rename":
+ if formatColumnName(lastOp.OriginalName) == lastOp.NewName {
+ fmt.Printf(settings.Muted.Render("Keeping column '%s' as is.\n"), lastOp.OriginalName)
+ } else {
+ fmt.Printf(settings.Muted.Render("Renaming column '%s' to '%s'.\n"), lastOp.OriginalName, lastOp.NewName)
+ }
+ case "drop":
+ fmt.Printf(settings.Muted.Render("Dropping column '%s'.\n"), lastOp.OriginalName)
+ }
+ }
+ if !skipLineFormating {
+ operations = formatColumns(operations)
+ }
+ operations = addFullname(operations)
+
+ return operations
+}
+
+func transformParquet(lu settings.LeakUtils, input, output Parquet, operations []ColumnOperation, deleteFirstRow, printQuery bool) error {
+ var selectClauses []string
+ hasColumns := false
+
+ for _, op := range operations {
+ if op.Action != "drop" {
+ hasColumns = true
+ if op.Action == "rename" {
+ selectClauses = append(selectClauses, fmt.Sprintf("%s AS \"%s\"", op.OriginalName, op.NewName))
+ } else {
+ selectClauses = append(selectClauses, op.OriginalName)
+ }
+ }
+ }
+
+ if !hasColumns {
+ return fmt.Errorf("no columns selected for output")
+ }
+
+ selectClause := strings.Join(selectClauses, ", ")
+ compression := ""
+ if output.Compression != "" {
+ compression = ", COMPRESSION '" + output.Compression + "'"
+ }
+
+ columnsLength := []string{}
+ for _, col := range input.Columns {
+ columnsLength = append(columnsLength, "COALESCE(LENGTH(\""+col+"\"),0)")
+ }
+ allowedRowSize := 30 * len(input.Columns)
+ offset := ""
+ if deleteFirstRow {
+ offset = "OFFSET 1"
+ }
+
+ query := fmt.Sprintf(`
+ COPY (
+ SELECT %s
+ FROM read_parquet('%s')
+ WHERE (%s) < %d
+ %s
+ ) TO '%s' (FORMAT PARQUET, ROW_GROUP_SIZE 200_000 %s)
+ `, selectClause, input.Filepath, strings.Join(columnsLength, "+"), allowedRowSize, offset, output.Filepath, compression)
+
+ if printQuery {
+ fmt.Println("Query:", query) // TODO: Remove tabs
+ return nil
+ }
+
+ fmt.Println(settings.Base.Render("\nTransforming and writing to output parquet..."))
+ _, err := lu.Db.Exec(query)
+ if err != nil {
+ return fmt.Errorf("failed to execute transformation: %w", err)
+ }
+ fmt.Println(settings.Base.Render("Transformation complete!\n"))
+
+ newParquet, err := GetParquet(lu.Db, output.Filepath)
+ if err != nil {
+ return err
+ }
+ newParquet.PrintParquet()
+
+ return nil
+}
+
+func GetParquet(db *sql.DB, inputFile string) (parquet *Parquet, err error) {
+ parquet = &Parquet{}
+ parquet.Filepath = inputFile
+
+ parquet.Columns, err = getColumns(db, inputFile)
+ if err != nil {
+ return
+ }
+ parquet.NRows, err = countRows(db, inputFile)
+ if err != nil {
+ return
+ }
+ parquet.Sample, err = getFirstNRows(db, inputFile, 6)
+ if err != nil {
+ return
+ }
+
+ n := strings.LastIndex(inputFile, "/")
+ if n == -1 {
+ parquet.Filename = inputFile
+ } else {
+ parquet.Filename = inputFile[n+1:]
+ }
+
+ return
+}
diff --git a/leak-utils/parquet/suggestions.go b/leak-utils/parquet/suggestions.go
new file mode 100644
index 0000000..2038c67
--- /dev/null
+++ b/leak-utils/parquet/suggestions.go
@@ -0,0 +1,81 @@
+package parquet
+
+import (
+ "slices"
+)
+
+func getSuggestion(col string) string {
+ col = formatColumnName(col)
+ knownNames := []string{
+ "date",
+ "phone",
+ "username",
+ "address",
+ "email",
+ "postal_code",
+ "city",
+ "country",
+ "state",
+ "age",
+ "gender",
+ "password",
+ "password_hash",
+ "full_name",
+ "last_name",
+ "name", // Will be renamed to full_name later
+ "first_name",
+ "birth_date",
+ "url",
+ "ip",
+ }
+ if slices.Contains(knownNames, col) {
+ return col
+ }
+ if col == "user" {
+ return "username"
+ }
+ if col == "login" {
+ return "username"
+ }
+ if col == "sex" {
+ return "gender"
+ }
+ if col == "ip_address" {
+ return "ip"
+ }
+ if col == "password_hashed" {
+ return "password_hash"
+ }
+ if col == "firstname" {
+ return "first_name"
+ }
+ if col == "lastname" {
+ return "last_name"
+ }
+ if col == "fullname" {
+ return "full_name"
+ }
+ if col == "mail" {
+ return "email"
+ }
+ if col == "zip" || col == "postalcode" || col == "zipcode" || col == "postal" || col == "zip_code" {
+ return "postal_code"
+ }
+ if col == "street_address" {
+ return "address"
+ }
+ if col == "hash" || col == "hashed_password" || col == "hash_password" {
+ return "password_hash"
+ }
+ if col == "birthdate" || col == "dob" || col == "date_of_birth" {
+ return "birth_date"
+ }
+
+ return ""
+}
+
+// HINTS:
+// date: _date
+// url: _url, link
+// address: _address
+//
diff --git a/leak-utils/parquet/utils.go b/leak-utils/parquet/utils.go
new file mode 100644
index 0000000..e227865
--- /dev/null
+++ b/leak-utils/parquet/utils.go
@@ -0,0 +1,105 @@
+package parquet
+
+import (
+ "database/sql"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// getColumns retrieves the column names from the Parquet file.
+func getColumns(db *sql.DB, filepath string) ([]string, error) {
+ // Create a view from the parquet file
+ query := fmt.Sprintf("CREATE OR REPLACE VIEW parquet_view AS SELECT * FROM read_parquet('%s')", filepath)
+ _, err := db.Exec(query)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create view: %w", err)
+ }
+
+ // Get column information
+ rows, err := db.Query("DESCRIBE parquet_view")
+ if err != nil {
+ return nil, fmt.Errorf("failed to describe view: %w", err)
+ }
+ defer rows.Close()
+
+ var columns []string
+ for rows.Next() {
+ var colName, colType, nullable, key, defaultVal, extra sql.NullString
+ err := rows.Scan(&colName, &colType, &nullable, &key, &defaultVal, &extra)
+ if err != nil {
+ return nil, fmt.Errorf("failed to scan row: %w", err)
+ }
+ if colName.Valid {
+ columns = append(columns, colName.String)
+ }
+ }
+
+ return columns, nil
+}
+
+// getFirstNRows retrieves the first N rows from the Parquet file.
+func getFirstNRows(db *sql.DB, inputFile string, n int) ([][]string, error) {
+ query := fmt.Sprintf("SELECT * FROM read_parquet('%s') LIMIT %d", inputFile, n)
+ rows, err := db.Query(query)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query parquet file: %w", err)
+ }
+ defer rows.Close()
+
+ cols, err := rows.Columns()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get columns: %w", err)
+ }
+
+ var results [][]string
+ for rows.Next() {
+ values := make([]sql.NullString, len(cols))
+ valuePtrs := make([]any, len(cols))
+ for i := range values {
+ valuePtrs[i] = &values[i]
+ }
+
+ err := rows.Scan(valuePtrs...)
+ if err != nil {
+ return nil, fmt.Errorf("failed to scan row: %w", err)
+ }
+
+ var row []string
+ for _, val := range values {
+ if val.Valid {
+ row = append(row, val.String)
+ } else {
+ row = append(row, "NULL")
+ }
+ }
+ results = append(results, row)
+ }
+
+ return results, nil
+}
+
+// countRows counts the number of rows in the Parquet file.
+func countRows(db *sql.DB, inputFile string) (int64, error) {
+ var count int64
+ err := db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM read_parquet('%s')", inputFile)).Scan(&count)
+ if err != nil {
+ return 0, fmt.Errorf("failed to count rows: %w", err)
+ }
+ return count, nil
+}
+
+// formatWithSpaces formats an integer with spaces as thousand separators.
+func formatWithSpaces(n int64) string {
+ s := strconv.FormatInt(n, 10)
+
+ var b strings.Builder
+ l := len(s)
+ for i, c := range s {
+ if i != 0 && (l-i)%3 == 0 {
+ b.WriteRune(' ')
+ }
+ b.WriteRune(c)
+ }
+ return b.String()
+}
diff --git a/leak-utils/settings/colors.go b/leak-utils/settings/colors.go
new file mode 100644
index 0000000..d3dbf41
--- /dev/null
+++ b/leak-utils/settings/colors.go
@@ -0,0 +1,27 @@
+package settings
+
+import (
+ "github.com/charmbracelet/lipgloss/v2"
+)
+
+var (
+ purple = lipgloss.Color("99")
+ lightPurple = lipgloss.Color("98")
+ yellow = lipgloss.Color("220")
+ gray = lipgloss.Color("245")
+ lightGray = lipgloss.Color("241")
+
+ Header = lipgloss.NewStyle().Foreground(purple).Bold(true)
+ Accent = lipgloss.NewStyle().Foreground(lightPurple)
+ Base = lipgloss.NewStyle().Foreground(lightGray)
+ Alert = lipgloss.NewStyle().Foreground(yellow).Bold(true)
+ Muted = lipgloss.NewStyle().Foreground(gray)
+)
+
+func DisableColors() {
+ Header = lipgloss.NewStyle()
+ Accent = lipgloss.NewStyle()
+ Base = lipgloss.NewStyle()
+ Alert = lipgloss.NewStyle()
+ Muted = lipgloss.NewStyle()
+}
diff --git a/leak-utils/settings/settings.go b/leak-utils/settings/settings.go
new file mode 100644
index 0000000..6f0ecd9
--- /dev/null
+++ b/leak-utils/settings/settings.go
@@ -0,0 +1,9 @@
+package settings
+
+import "database/sql"
+
+type LeakUtils struct {
+ Debug bool
+ Compression string // Compression of the output file (e.g., "SNAPPY", "ZSTD", "NONE" or "")
+ Db *sql.DB
+}
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000..5581a19
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,4 @@
+[build]
+ base = "front"
+ publish = "dist"
+ command = "bun run build"
diff --git a/nix/back.nix b/nix/back.nix
new file mode 100644
index 0000000..26d5a5e
--- /dev/null
+++ b/nix/back.nix
@@ -0,0 +1,155 @@
+{
+ pkgs,
+ lib,
+ self,
+}: let
+ name = "eleakxir";
+
+ package = pkgs.buildGoModule {
+ pname = name;
+ version = "0.1.0";
+ src = ../back;
+ vendorHash = "";
+
+ buildInputs = [
+ pkgs.duckdb
+ pkgs.arrow-cpp
+ ];
+ };
+in {
+ package = package;
+
+ nixosModule = {config, ...}: let
+ cfg = config.services."${name}";
+ in {
+ options.services."${name}" = {
+ enable = lib.mkEnableOption "Enable the ${name} service";
+ user = lib.mkOption {
+ type = lib.types.str;
+ default = name;
+ description = "User to run the ${name} service as";
+ };
+ group = lib.mkOption {
+ type = lib.types.str;
+ default = name;
+ description = "Group to run the ${name} service as";
+ };
+ port = lib.mkOption {
+ type = lib.types.port;
+ default = 9198;
+ description = "Port for the ${name} service";
+ };
+ folders = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [];
+ description = "Folders to monitor for parquet files";
+ };
+ cacheFolder = lib.mkOption {
+ type = lib.types.str;
+ default = "";
+ description = "Cache folder";
+ };
+ limit = lib.mkOption {
+ type = lib.types.int;
+ default = 200;
+ description = "Limit of results to return";
+ };
+ password = lib.mkOption {
+ type = lib.types.str;
+ default = "";
+ description = "Password for auth (empty means no auth)";
+ };
+ debug = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Debug mode";
+ };
+ maxCacheDuration = lib.mkOption {
+ type = lib.types.str;
+ default = "24h";
+ description = "Max result cache duration (30m, 2h, 1d)";
+ };
+ reloadDataleaksInterval = lib.mkOption {
+ type = lib.types.str;
+ default = "1h";
+ description = "Interval to reload dataleaks (30m, 2h, 1d)";
+ };
+ minimumQueryLength = lib.mkOption {
+ type = lib.types.int;
+ default = 3;
+ description = "Minimum query length";
+ };
+ baseColumns = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [];
+ description = "Base columns are used when the column searched is 'all'";
+ };
+ githubRecon = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Activate the github-recon OSINT module";
+ };
+ githubToken = lib.mkOption {
+ type = lib.types.str;
+ default = "";
+ description = "GitHub token to use for Github recon";
+ };
+ githubDeepMode = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Activate the github-recon deep mode";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ users.users."${cfg.user}" = {
+ isSystemUser = true;
+ group = cfg.group;
+ };
+ users.groups."${cfg.group}" = {};
+
+ systemd.services."${name}" = {
+ description = "${name} service";
+ after = ["network.target"];
+ wantedBy = ["multi-user.target"];
+ serviceConfig = {
+ ExecStart = "${self.packages.${pkgs.system}.backend}/bin/cmd";
+ Restart = "always";
+ User = cfg.user;
+ Group = cfg.group;
+ StateDirectory = name;
+ ReadWritePaths = ["/var/lib/${name}"];
+ WorkingDirectory = "/var/lib/${name}";
+
+ Environment = [
+ "PORT=${toString cfg.port}"
+ "DATALEAKS_FOLDERS=${lib.strings.concatStringsSep "," cfg.folders}"
+ "DATALEAKS_CACHE_FOLDER=${cfg.cacheFolder}"
+ "LIMIT=${toString cfg.limit}"
+ "PASSWORD=${toString cfg.password}"
+ "DEBUG=${
+ if cfg.debug
+ then "true"
+ else "false"
+ }"
+ "MAX_CACHE_DURATION=${cfg.maxCacheDuration}"
+ "RELOAD_DATALEAKS_INTERVAL=${cfg.reloadDataleaksInterval}"
+ "MINIMUM_QUERY_LENGTH=${toString cfg.minimumQueryLength}"
+ "BASE_COLUMNS=${lib.strings.concatStringsSep "," cfg.baseColumns}"
+ "GITHUB_RECON=${
+ if cfg.githubRecon
+ then "true"
+ else "false"
+ }"
+ "GITHUB_TOKEN=${cfg.githubToken}"
+ "GITHUB_DEEP_MODE=${
+ if cfg.githubDeepMode
+ then "true"
+ else "false"
+ }"
+ ];
+ };
+ };
+ };
+ };
+}
diff --git a/nix/devshell.nix b/nix/devshell.nix
new file mode 100644
index 0000000..e3d07f0
--- /dev/null
+++ b/nix/devshell.nix
@@ -0,0 +1,12 @@
+{pkgs, ...}: {
+ devShell = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ duckdb
+ air
+ # OSINT tools
+ ghunt
+ sherlock
+ holehe
+ ];
+ };
+}
diff --git a/nix/leak-utils.nix b/nix/leak-utils.nix
new file mode 100644
index 0000000..fbc6744
--- /dev/null
+++ b/nix/leak-utils.nix
@@ -0,0 +1,21 @@
+{
+ pkgs,
+ lib,
+ self,
+}: let
+ name = "leak-utils";
+
+ package = pkgs.buildGoModule {
+ pname = name;
+ version = "0.1.0";
+ src = ../leak-utils;
+ vendorHash = "sha256-NDY3T3FhQ2iXJr3v3sxTX9taVTU9LPCLd/emWukHZcs=";
+
+ buildInputs = [
+ pkgs.duckdb
+ pkgs.arrow-cpp
+ ];
+ };
+in {
+ package = package;
+}