init
46
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Contributions are welcome: new tool integrations especially.
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch: `git checkout -b feat/my-tool`
|
||||||
|
3. Implement your tool
|
||||||
|
4. Open a pull request
|
||||||
|
|
||||||
|
Please ensure your tool handles context cancellation, respects rate limits, and declares the correct input types. Document any required API key or external binary dependency.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Go 1.25+](https://go.dev/dl/)
|
||||||
|
- [Bun](https://bun.sh)
|
||||||
|
- [Just](https://github.com/casey/just)
|
||||||
|
|
||||||
|
Or you can use the Nix Shell by typing `nix develop`
|
||||||
|
|
||||||
|
### Run locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/anotherhadi/iknowyou.git
|
||||||
|
cd iknowyou
|
||||||
|
|
||||||
|
just dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:4321](http://localhost:4321).
|
||||||
|
|
||||||
|
The backend listens on `:8080` by default. Configure via environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| ------------ | ------------- | ---------------------------- |
|
||||||
|
| `IKY_PORT` | `8080` | HTTP port |
|
||||||
|
| `IKY_CONFIG` | `config.yaml` | Path to the YAML config file |
|
||||||
|
|
||||||
|
## Adding a Tool
|
||||||
|
|
||||||
|
1. Create `back/internal/tools/mytool/mytool.go` implementing `tools.ToolRunner`
|
||||||
|
2. Optionally implement `tools.Configurable` + `tools.ConfigDescriber` for config UI support
|
||||||
|
3. Optionally implement `tools.AvailabilityChecker` if the tool requires an external binary
|
||||||
|
4. Register in `back/cmd/server/main.go` and `back/cmd/gendocs/main.go`
|
||||||
|
5. Run `just docs` to update the docs
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: anotherhadi
|
||||||
BIN
.github/assets/banner.png
vendored
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
.github/assets/logo.png
vendored
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
36
.github/docs/how-it-works.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# How it Works
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser → POST /api/searches (target, type, profile)
|
||||||
|
↓
|
||||||
|
Backend filters tools by:
|
||||||
|
· input type compatibility
|
||||||
|
· profile enabled/disabled rules
|
||||||
|
· required config fields (skips if missing)
|
||||||
|
↓
|
||||||
|
All eligible tools run in parallel goroutines
|
||||||
|
↓
|
||||||
|
Browser polls GET /api/searches/{id}
|
||||||
|
Results render progressively as tools complete
|
||||||
|
```
|
||||||
|
|
||||||
|
Each tool is a Go struct implementing a small interface: it declares what input types it accepts, what config it needs, and how to run. The engine handles the rest.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
iknowyou/
|
||||||
|
├── back/ # Go backend
|
||||||
|
│ ├── cmd/
|
||||||
|
│ │ ├── server/ # Main HTTP server
|
||||||
|
│ │ └── gendocs/ # Doc generator
|
||||||
|
│ ├── config/ # YAML config models & builtin profiles
|
||||||
|
│ └── internal/
|
||||||
|
│ ├── api/ # Chi router + handlers
|
||||||
|
│ ├── search/ # Parallel search orchestration
|
||||||
|
│ └── tools/ # Tool interface + implementations
|
||||||
|
└── front/ # Astro + Svelte frontend
|
||||||
|
└── src/
|
||||||
|
├── pages/ # / · /tools · /profiles · /search/[id] · /cheatsheets · /help
|
||||||
|
└── components/ # Svelte interactive components
|
||||||
|
```
|
||||||
18
.github/docs/tools.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Tools
|
||||||
|
|
||||||
|
_12 tools registered._
|
||||||
|
|
||||||
|
| Tool | Input types | Description | Link |
|
||||||
|
|------|-------------|-------------|------|
|
||||||
|
| [`user-scanner`](tools/user-scanner.md) | `email`, `username` | 🕵️♂️ (2-in-1) Email & Username OSINT suite. Analyzes 195+ scan vectors (95+ email / 100+ username) for security research, investigations, and digital footprinting. | [Link](https://github.com/kaifcodec/user-scanner) |
|
||||||
|
| [`github-recon`](tools/github-recon.md) | `username`, `email` | GitHub OSINT reconnaissance tool. Gathers profile info, social links, organisations, SSH/GPG keys, commits, and more from a GitHub username or email. | [Link](https://github.com/anotherhadi/nur-osint) |
|
||||||
|
| [`whois`](tools/whois.md) | `domain`, `ip` | WHOIS lookup for domain registration and IP ownership information. | [Link](https://en.wikipedia.org/wiki/WHOIS) |
|
||||||
|
| [`dig`](tools/dig.md) | `domain`, `ip` | DNS lookup querying A, AAAA, MX, NS, TXT, and SOA records for a domain, or reverse DNS (PTR) for an IP. | [Link](https://linux.die.net/man/1/dig) |
|
||||||
|
| [`ipinfo`](tools/ipinfo.md) | `ip` | IP geolocation via ipinfo.io — returns city, region, country, coordinates, ASN/org, timezone, and hostname. | [Link](https://ipinfo.io) |
|
||||||
|
| [`gravatar-recon`](tools/gravatar-recon.md) | `email` | Gravatar OSINT tool. Extracts public profile data from a Gravatar account: name, bio, location, employment, social accounts, phone, and more. | [Link](https://github.com/anotherhadi/gravatar-recon) |
|
||||||
|
| [`whoisfreaks`](tools/whoisfreaks.md) | `email`, `name`, `domain` | Reverse WHOIS lookup via WhoisFreaks — find all domains registered by an email, owner name, or keyword across 3.6B+ WHOIS records. | [Link](https://whoisfreaks.com) |
|
||||||
|
| [`maigret`](tools/maigret.md) | `username` | Username OSINT across 3000+ sites. Searches social networks, forums, and online platforms for an account matching the target username. | [Link](https://github.com/soxoj/maigret) |
|
||||||
|
| [`leakcheck`](tools/leakcheck.md) | `email`, `username`, `phone` | Data breach lookup via LeakCheck.io — searches 7B+ leaked records for email addresses, usernames, and phone numbers across hundreds of breaches. | [Link](https://leakcheck.io) |
|
||||||
|
| [`crt.sh`](tools/crt.sh.md) | `domain` | SSL/TLS certificate transparency log search via crt.sh — enumerates subdomains and certificates issued for a domain. | [Link](https://crt.sh) |
|
||||||
|
| [`breachdirectory`](tools/breachdirectory.md) | `email`, `username` | Data breach search via BreachDirectory — checks if an email, username, or phone appears in known data breaches and returns exposed passwords/hashes. | [Link](https://breachdirectory.org) |
|
||||||
|
| [`wappalyzer`](tools/wappalyzer.md) | `domain` | Web technology fingerprinting via wappalyzergo — detects CMS, frameworks, web servers, analytics, CDN, and 1500+ other technologies running on a domain. | [Link](https://github.com/projectdiscovery/wappalyzergo) |
|
||||||
22
.github/docs/tools/breachdirectory.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# `breachdirectory`
|
||||||
|
|
||||||
|
Data breach search via BreachDirectory — checks if an email, username, or phone appears in known data breaches and returns exposed passwords/hashes.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://breachdirectory.org](https://breachdirectory.org)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `email`
|
||||||
|
- `username`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure globally via the Tools page or override per profile.
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|:--------:|---------|-------------|
|
||||||
|
| `api_key` | `string` | **yes** | - | RapidAPI key for BreachDirectory (required — get one at rapidapi.com/rohan-patra/api/breachdirectory) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
17
.github/docs/tools/crt.sh.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# `crt.sh`
|
||||||
|
|
||||||
|
SSL/TLS certificate transparency log search via crt.sh — enumerates subdomains and certificates issued for a domain.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://crt.sh](https://crt.sh)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `domain`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This tool requires no configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
24
.github/docs/tools/dig.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# `dig`
|
||||||
|
|
||||||
|
DNS lookup querying A, AAAA, MX, NS, TXT, and SOA records for a domain, or reverse DNS (PTR) for an IP.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://linux.die.net/man/1/dig](https://linux.die.net/man/1/dig)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `domain`
|
||||||
|
- `ip`
|
||||||
|
|
||||||
|
## External dependencies
|
||||||
|
|
||||||
|
The following binaries must be installed and available in `$PATH`:
|
||||||
|
|
||||||
|
- `dig`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This tool requires no configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
30
.github/docs/tools/github-recon.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# `github-recon`
|
||||||
|
|
||||||
|
GitHub OSINT reconnaissance tool. Gathers profile info, social links, organisations, SSH/GPG keys, commits, and more from a GitHub username or email.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://github.com/anotherhadi/nur-osint](https://github.com/anotherhadi/nur-osint)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `username`
|
||||||
|
- `email`
|
||||||
|
|
||||||
|
## External dependencies
|
||||||
|
|
||||||
|
The following binaries must be installed and available in `$PATH`:
|
||||||
|
|
||||||
|
- `github-recon`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure globally via the Tools page or override per profile.
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|:--------:|---------|-------------|
|
||||||
|
| `token` | `string` | - | - | GitHub personal access token (enables higher rate limits and more data) |
|
||||||
|
| `deepscan` | `bool` | - | `false` | Enable deep scan (slower - scans all repositories for authors/emails) |
|
||||||
|
| `spoof_email` | `bool` | - | `false` | Include email spoofing check (email mode only, requires token) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
23
.github/docs/tools/gravatar-recon.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# `gravatar-recon`
|
||||||
|
|
||||||
|
Gravatar OSINT tool. Extracts public profile data from a Gravatar account: name, bio, location, employment, social accounts, phone, and more.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://github.com/anotherhadi/gravatar-recon](https://github.com/anotherhadi/gravatar-recon)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `email`
|
||||||
|
|
||||||
|
## External dependencies
|
||||||
|
|
||||||
|
The following binaries must be installed and available in `$PATH`:
|
||||||
|
|
||||||
|
- `gravatar-recon`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This tool requires no configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
21
.github/docs/tools/ipinfo.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# `ipinfo`
|
||||||
|
|
||||||
|
IP geolocation via ipinfo.io — returns city, region, country, coordinates, ASN/org, timezone, and hostname.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://ipinfo.io](https://ipinfo.io)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `ip`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure globally via the Tools page or override per profile.
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|:--------:|---------|-------------|
|
||||||
|
| `token` | `string` | - | - | ipinfo.io API token (optional — free tier allows 50k req/month without one) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
23
.github/docs/tools/leakcheck.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# `leakcheck`
|
||||||
|
|
||||||
|
Data breach lookup via LeakCheck.io — searches 7B+ leaked records for email addresses, usernames, and phone numbers across hundreds of breaches.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://leakcheck.io](https://leakcheck.io)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `email`
|
||||||
|
- `username`
|
||||||
|
- `phone`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure globally via the Tools page or override per profile.
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|:--------:|---------|-------------|
|
||||||
|
| `api_key` | `string` | **yes** | - | LeakCheck API key (required — get one at leakcheck.io) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
27
.github/docs/tools/maigret.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# `maigret`
|
||||||
|
|
||||||
|
Username OSINT across 3000+ sites. Searches social networks, forums, and online platforms for an account matching the target username.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://github.com/soxoj/maigret](https://github.com/soxoj/maigret)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `username`
|
||||||
|
|
||||||
|
## External dependencies
|
||||||
|
|
||||||
|
The following binaries must be installed and available in `$PATH`:
|
||||||
|
|
||||||
|
- `maigret`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure globally via the Tools page or override per profile.
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|:--------:|---------|-------------|
|
||||||
|
| `all_sites` | `bool` | - | `false` | Scan all sites in the database instead of just the top 500 (slower) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
29
.github/docs/tools/user-scanner.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# `user-scanner`
|
||||||
|
|
||||||
|
🕵️♂️ (2-in-1) Email & Username OSINT suite. Analyzes 195+ scan vectors (95+ email / 100+ username) for security research, investigations, and digital footprinting.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://github.com/kaifcodec/user-scanner](https://github.com/kaifcodec/user-scanner)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `email`
|
||||||
|
- `username`
|
||||||
|
|
||||||
|
## External dependencies
|
||||||
|
|
||||||
|
The following binaries must be installed and available in `$PATH`:
|
||||||
|
|
||||||
|
- `user-scanner`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure globally via the Tools page or override per profile.
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|:--------:|---------|-------------|
|
||||||
|
| `allow_loud` | `bool` | - | `false` | Enable scanning sites that may send emails/notifications (password resets, etc.) |
|
||||||
|
| `only_found` | `bool` | - | `true` | Only show sites where the username/email was found |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
17
.github/docs/tools/wappalyzer.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# `wappalyzer`
|
||||||
|
|
||||||
|
Web technology fingerprinting via wappalyzergo — detects CMS, frameworks, web servers, analytics, CDN, and 1500+ other technologies running on a domain.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `domain`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This tool requires no configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
24
.github/docs/tools/whois.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# `whois`
|
||||||
|
|
||||||
|
WHOIS lookup for domain registration and IP ownership information.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://en.wikipedia.org/wiki/WHOIS](https://en.wikipedia.org/wiki/WHOIS)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `domain`
|
||||||
|
- `ip`
|
||||||
|
|
||||||
|
## External dependencies
|
||||||
|
|
||||||
|
The following binaries must be installed and available in `$PATH`:
|
||||||
|
|
||||||
|
- `whois`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This tool requires no configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
23
.github/docs/tools/whoisfreaks.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# `whoisfreaks`
|
||||||
|
|
||||||
|
Reverse WHOIS lookup via WhoisFreaks — find all domains registered by an email, owner name, or keyword across 3.6B+ WHOIS records.
|
||||||
|
|
||||||
|
**Source / documentation:** [https://whoisfreaks.com](https://whoisfreaks.com)
|
||||||
|
|
||||||
|
## Input types
|
||||||
|
|
||||||
|
- `email`
|
||||||
|
- `name`
|
||||||
|
- `domain`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure globally via the Tools page or override per profile.
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|:--------:|---------|-------------|
|
||||||
|
| `api_key` | `string` | **yes** | - | WhoisFreaks API key (required — free account at whoisfreaks.com) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← Back to tools index](../tools.md)
|
||||||
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.claude/
|
||||||
|
todolist.md
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 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.
|
||||||
196
README.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img alt="logo" src="./.github/assets/logo.png" width="120px" />
|
||||||
|
|
||||||
|
# I Know You
|
||||||
|
|
||||||
|
**Self-hosted OSINT aggregation platform**
|
||||||
|
|
||||||
|
Run dozens of open-source intelligence tools against a single target in parallel; all from one clean web interface.
|
||||||
|
|
||||||
|
[](https://go.dev)
|
||||||
|
[](https://astro.build)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/anotherhadi/iknowyou)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is it?
|
||||||
|
|
||||||
|
IKY (iknowyou) is a **self-hosted OSINT (Open-Source Intelligence) platform** that centralises reconnaissance tools into a single reactive web interface. Instead of juggling terminals, browser tabs, and disconnected CLI tools, you type a target once and get results streaming in real time.
|
||||||
|
|
||||||
|
Designed for security researchers, penetration testers, and OSINT investigators who need speed and visibility without compromising on control.
|
||||||
|
|
||||||
|
**Supported target types:** `email`, `username`, `domain`, `IP`, `phone`, ...
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Parallel execution**: all tools run simultaneously; results stream in as they arrive
|
||||||
|
- **Profile system**: create named profiles to enable/disable specific tools or override their config per investigation type (quick recon vs. thorough sweep)
|
||||||
|
- **Per-tool configuration**: set API keys, rate limits, and options globally or per profile
|
||||||
|
- **Tool availability checks**: tools that depend on an external binary report their status; the interface shows which tools are ready, which need config, and which are unavailable
|
||||||
|
- **Search history**: completed searches are kept in memory; results can be reviewed without re-running
|
||||||
|
- **Extensible architecture**: adding a new tool is a single Go file implementing one interface, registered in one line
|
||||||
|
- **Production-ready**: The configuration is YAML-based and read-only mode is supported (for Nix-managed deployments). A NixOS module is available.
|
||||||
|
|
||||||
|
## Profiles
|
||||||
|
|
||||||
|
Profiles let you tailor which tools run and how, without touching the global config.
|
||||||
|
|
||||||
|
| Profile | Type | Description |
|
||||||
|
| ---------- | -------- | ---------------------------------------------------------------------------- |
|
||||||
|
| `default` | Built-in | All tools active, default settings |
|
||||||
|
| `hard` | Built-in | All tools active, including those that may leave traces at the target |
|
||||||
|
| _(custom)_ | User | Your own combination of enabled/disabled tools and per-tool config overrides |
|
||||||
|
|
||||||
|
Create and manage custom profiles from the **Profiles** page.
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
- [See the list of tools](.github/docs/tools.md)
|
||||||
|
- [Learn how it works](.github/docs/how-it-works.md)
|
||||||
|
- [Learn how to contribute](.github/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Deploy on NixOS</summary>
|
||||||
|
|
||||||
|
1. In the `flake.nix` file, add `iknowyou` in the `inputs` section and import
|
||||||
|
the `iknowyou.nixosModules.default` module:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
iknowyou.url = "github:anotherhadi/iknowyou";
|
||||||
|
};
|
||||||
|
outputs = {
|
||||||
|
# ...
|
||||||
|
modules = [
|
||||||
|
inputs.iknowyou.nixosModules.default
|
||||||
|
];
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Enable the service:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.iknowyou = {
|
||||||
|
enable = true;
|
||||||
|
port = 8080;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
All tool dependencies are included automatically.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Configuring
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Via the web interface</summary>
|
||||||
|
|
||||||
|
No files needed. API keys, tool settings, and profiles can all be managed from the **Settings** page. Changes are written back to the config file automatically.
|
||||||
|
|
||||||
|
This only works if the config file is writable. On NixOS with a read-only store path, use one of the methods below instead.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Via a YAML file</summary>
|
||||||
|
|
||||||
|
Create `/etc/iky/config.yaml` (or any path, then point `IKY_CONFIG` to it):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tools:
|
||||||
|
github-recon:
|
||||||
|
token: ghp_yourtoken
|
||||||
|
whoisfreaks:
|
||||||
|
api_key: yourkey
|
||||||
|
ipinfo:
|
||||||
|
token: yourtoken
|
||||||
|
breachdirectory:
|
||||||
|
api_key: yourkey
|
||||||
|
|
||||||
|
profiles:
|
||||||
|
quick:
|
||||||
|
enabled:
|
||||||
|
- whois
|
||||||
|
- dig
|
||||||
|
- crt.sh
|
||||||
|
disabled: []
|
||||||
|
```
|
||||||
|
|
||||||
|
Only include the tools you want to configure — everything else falls back to defaults.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Via Nix + sops-nix (NixOS)</summary>
|
||||||
|
|
||||||
|
API keys should never go in the Nix store (world-readable). Use [sops-nix](https://github.com/Mic92/sops-nix) to store the config file as an encrypted secret and have it decrypted at boot with the right permissions.
|
||||||
|
|
||||||
|
1. Add your IKY config to your sops-encrypted secrets file (e.g. `secrets/iky.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
iky-config: |
|
||||||
|
tools:
|
||||||
|
github-recon:
|
||||||
|
token: ghp_yourtoken
|
||||||
|
whoisfreaks:
|
||||||
|
api_key: yourkey
|
||||||
|
ipinfo:
|
||||||
|
token: yourtoken
|
||||||
|
profiles:
|
||||||
|
quick:
|
||||||
|
enabled:
|
||||||
|
- whois
|
||||||
|
- dig
|
||||||
|
- crt.sh
|
||||||
|
disabled: []
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Declare the secret and point the service to it:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
sops.secrets."iky-config" = {
|
||||||
|
owner = "iknowyou";
|
||||||
|
mode = "0400";
|
||||||
|
restartUnits = [ "iknowyou.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.iknowyou = {
|
||||||
|
enable = true;
|
||||||
|
configFile = config.sops.secrets."iky-config".path;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The module creates the `iknowyou` group automatically and adds it as a supplementary group to the service, so the `DynamicUser` can read the secret without needing a static user.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## ⚠️ Legal Disclaimer
|
||||||
|
|
||||||
|
**IKY is intended for legal, authorized use only.**
|
||||||
|
|
||||||
|
This software is designed for:
|
||||||
|
|
||||||
|
- Security research on systems you own or have **explicit written permission** to test
|
||||||
|
- Penetration testing engagements conducted under a signed scope of work
|
||||||
|
- OSINT investigations carried out in compliance with applicable laws and regulations
|
||||||
|
- Educational purposes in controlled, isolated environments
|
||||||
|
|
||||||
|
**Using IKY against targets without prior authorization may be illegal** under computer fraud and privacy laws. The author(s) of IKY accept **no responsibility** for any misuse, damage, legal consequences, or harm caused by this software. By using IKY you agree that you are solely responsible for ensuring your use is lawful and ethical.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/anotherhadi/iknowyou">github</a> |
|
||||||
|
<a href="https://gitlab.com/anotherhadi_mirror/iknowyou">gitlab (mirror)</a> |
|
||||||
|
<a href="https://git.hadi.icu/anotherhadi/iknowyou">gitea (mirror)</a>
|
||||||
|
</div>
|
||||||
24
back/.air.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ./cmd/server/main.go"
|
||||||
|
include_ext = ["go", "toml"]
|
||||||
|
exclude_dir = ["tmp", "vendor", "node_modules"]
|
||||||
|
delay = 1000
|
||||||
|
stop_on_error = true
|
||||||
|
clean_on_exit = true
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = true
|
||||||
|
|
||||||
|
[color]
|
||||||
|
main = "magenta"
|
||||||
|
watcher = "cyan"
|
||||||
|
build = "yellow"
|
||||||
|
runner = "green"
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = true
|
||||||
2
back/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
tmp/
|
||||||
|
config.yaml
|
||||||
155
back/cmd/gendocs/main.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/registry"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out := flag.String("out", "../.github/docs", "output directory for generated docs")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
toolsDir := filepath.Join(*out, "tools")
|
||||||
|
if _, err := os.Stat(toolsDir); err == nil {
|
||||||
|
if err := os.RemoveAll(toolsDir); err != nil {
|
||||||
|
fatalf("removing tools dir: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(toolsDir, 0o755); err != nil {
|
||||||
|
fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runners := make([]tools.ToolRunner, len(registry.Factories))
|
||||||
|
for i, f := range registry.Factories {
|
||||||
|
runners[i] = f()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeIndex(*out, runners); err != nil {
|
||||||
|
fatalf("index: %v", err)
|
||||||
|
}
|
||||||
|
for _, r := range runners {
|
||||||
|
if err := writeTool(*out, r); err != nil {
|
||||||
|
fatalf("tool %s: %v", r.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ generated docs for %d tools → %s\n", len(runners), *out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeIndex writes the tools.md index table.
|
||||||
|
func writeIndex(outDir string, runners []tools.ToolRunner) error {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString("# Tools\n\n")
|
||||||
|
fmt.Fprintf(&b, "_%d tools registered._\n\n", len(runners))
|
||||||
|
|
||||||
|
b.WriteString("| Tool | Input types | Description | Link |\n")
|
||||||
|
b.WriteString("|------|-------------|-------------|------|\n")
|
||||||
|
|
||||||
|
for _, r := range runners {
|
||||||
|
types := make([]string, len(r.InputTypes()))
|
||||||
|
for i, t := range r.InputTypes() {
|
||||||
|
types[i] = fmt.Sprintf("`%s`", t)
|
||||||
|
}
|
||||||
|
link := fmt.Sprintf("[`%s`](tools/%s.md)", r.Name(), r.Name())
|
||||||
|
projectLink := ""
|
||||||
|
if r.Link() != "" {
|
||||||
|
projectLink = fmt.Sprintf("[Link](%s)", r.Link())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "| %s | %s | %s | %s |\n",
|
||||||
|
link,
|
||||||
|
strings.Join(types, ", "),
|
||||||
|
r.Description(),
|
||||||
|
projectLink,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeFile(filepath.Join(outDir, "tools.md"), b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTool writes the per-tool detail page.
|
||||||
|
func writeTool(outDir string, r tools.ToolRunner) error {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, "# `%s`\n\n", r.Name())
|
||||||
|
fmt.Fprintf(&b, "%s\n\n", r.Description())
|
||||||
|
|
||||||
|
if r.Link() != "" {
|
||||||
|
fmt.Fprintf(&b, "**Source / documentation:** [%s](%s)\n\n", r.Link(), r.Link())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input types
|
||||||
|
b.WriteString("## Input types\n\n")
|
||||||
|
for _, t := range r.InputTypes() {
|
||||||
|
fmt.Fprintf(&b, "- `%s`\n", t)
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
// External binary dependencies
|
||||||
|
if lister, ok := r.(tools.DependencyLister); ok {
|
||||||
|
if deps := lister.Dependencies(); len(deps) > 0 {
|
||||||
|
b.WriteString("## External dependencies\n\n")
|
||||||
|
b.WriteString("The following binaries must be installed and available in `$PATH`:\n\n")
|
||||||
|
for _, dep := range deps {
|
||||||
|
fmt.Fprintf(&b, "- `%s`\n", dep)
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
if d, ok := r.(tools.ConfigDescriber); ok {
|
||||||
|
fields := d.ConfigFields()
|
||||||
|
if len(fields) > 0 {
|
||||||
|
b.WriteString("## Configuration\n\n")
|
||||||
|
b.WriteString("Configure globally via the Tools page or override per profile.\n\n")
|
||||||
|
b.WriteString("| Field | Type | Required | Default | Description |\n")
|
||||||
|
b.WriteString("|-------|------|:--------:|---------|-------------|\n")
|
||||||
|
for _, f := range fields {
|
||||||
|
req := "-"
|
||||||
|
if f.Required {
|
||||||
|
req = "**yes**"
|
||||||
|
}
|
||||||
|
def := "-"
|
||||||
|
if f.Default != nil && fmt.Sprintf("%v", f.Default) != "" {
|
||||||
|
def = fmt.Sprintf("`%v`", f.Default)
|
||||||
|
}
|
||||||
|
desc := f.Description
|
||||||
|
if desc == "" {
|
||||||
|
desc = "-"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "| `%s` | `%s` | %s | %s | %s |\n",
|
||||||
|
f.Name, f.Type, req, def, desc,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString("## Configuration\n\nThis tool has no configuration fields.\n\n")
|
||||||
|
}
|
||||||
|
} else if _, ok := r.(tools.Configurable); ok {
|
||||||
|
b.WriteString("## Configuration\n\nThis tool is configurable but does not expose field metadata.\n\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString("## Configuration\n\nThis tool requires no configuration.\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("---\n\n")
|
||||||
|
b.WriteString("[← Back to tools index](../tools.md)\n")
|
||||||
|
|
||||||
|
return writeFile(filepath.Join(outDir, "tools", r.Name()+".md"), b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(path, content string) error {
|
||||||
|
return os.WriteFile(path, []byte(content), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatalf(format string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, "gendocs: "+format+"\n", args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
62
back/cmd/server/main.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/config/env"
|
||||||
|
internalapi "github.com/anotherhadi/iknowyou/internal/api"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/registry"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := env.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("env: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := search.NewManager(cfg.ConfigPath, registry.Factories, cfg.SearchTTL, cfg.CleanupInterval)
|
||||||
|
defer manager.Stop()
|
||||||
|
|
||||||
|
if cfg.Demo {
|
||||||
|
manager.InjectDemoSearches()
|
||||||
|
log.Println("demo mode enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
router := internalapi.NewRouter(manager, registry.Factories, cfg.ConfigPath, cfg.FrontDir, cfg.Demo)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||||
|
Handler: router,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("listening on :%d (config: %s)", cfg.Port, cfg.ConfigPath)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
log.Println("shutting down...")
|
||||||
|
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Fatalf("graceful shutdown failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("stopped")
|
||||||
|
}
|
||||||
62
back/config/builtin.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
// BuiltinProfile is a hardcoded, read-only profile with optional tool config overrides.
|
||||||
|
type BuiltinProfile struct {
|
||||||
|
Notes string
|
||||||
|
Profile Profile
|
||||||
|
Tools map[string]map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
var BuiltinProfiles = map[string]BuiltinProfile{
|
||||||
|
"default": {
|
||||||
|
Notes: "Standard profile. All tools are active with default settings.",
|
||||||
|
Profile: Profile{},
|
||||||
|
},
|
||||||
|
"hard": {
|
||||||
|
Notes: "Aggressive profile. All tools are active, including those that may send notifications to the target.",
|
||||||
|
Profile: Profile{},
|
||||||
|
Tools: map[string]map[string]any{
|
||||||
|
"user-scanner": {"allow_loud": true},
|
||||||
|
"github-recon": {"deepscan": true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyBuiltinToolOverride(profileName, toolName string, dst any) error {
|
||||||
|
builtin, ok := BuiltinProfiles[profileName]
|
||||||
|
if !ok || builtin.Tools == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
overrides, hasOverride := builtin.Tools[toolName]
|
||||||
|
if !hasOverride {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, err := yaml.Marshal(overrides)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return yaml.Unmarshal(b, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ActiveToolsForProfile(p Profile, allToolNames []string) []string {
|
||||||
|
active := allToolNames
|
||||||
|
if len(p.Enabled) > 0 {
|
||||||
|
active = p.Enabled
|
||||||
|
}
|
||||||
|
if len(p.Disabled) > 0 {
|
||||||
|
blacklist := make(map[string]struct{}, len(p.Disabled))
|
||||||
|
for _, n := range p.Disabled {
|
||||||
|
blacklist[n] = struct{}{}
|
||||||
|
}
|
||||||
|
var filtered []string
|
||||||
|
for _, n := range active {
|
||||||
|
if _, skip := blacklist[n]; !skip {
|
||||||
|
filtered = append(filtered, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active = filtered
|
||||||
|
}
|
||||||
|
return active
|
||||||
|
}
|
||||||
144
back/config/config.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Tools map[string]yaml.Node `yaml:"tools" json:"tools"`
|
||||||
|
Profiles map[string]Profile `yaml:"profiles" json:"profiles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
Notes string `yaml:"notes,omitempty" json:"notes,omitempty"`
|
||||||
|
Tools map[string]yaml.Node `yaml:"tools" json:"tools"`
|
||||||
|
Enabled []string `yaml:"enabled" json:"enabled"`
|
||||||
|
Disabled []string `yaml:"disabled" json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) DecodeEffective(toolName, profileName string, dst any) error {
|
||||||
|
if node, ok := c.Tools[toolName]; ok {
|
||||||
|
if err := node.Decode(dst); err != nil {
|
||||||
|
return fmt.Errorf("config: decoding global config for tool %q: %w", toolName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if profileName != "" {
|
||||||
|
// Builtin profiles have their overrides defined in Go, not in YAML.
|
||||||
|
if _, isBuiltin := BuiltinProfiles[profileName]; isBuiltin {
|
||||||
|
return ApplyBuiltinToolOverride(profileName, toolName, dst)
|
||||||
|
}
|
||||||
|
p, ok := c.Profiles[profileName]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("config: unknown profile %q", profileName)
|
||||||
|
}
|
||||||
|
if node, ok := p.Tools[toolName]; ok {
|
||||||
|
if err := node.Decode(dst); err != nil {
|
||||||
|
return fmt.Errorf("config: decoding profile %q override for tool %q: %w", profileName, toolName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ActiveTools(profileName string, allToolNames []string) ([]string, error) {
|
||||||
|
if profileName == "" {
|
||||||
|
return allToolNames, nil
|
||||||
|
}
|
||||||
|
if builtin, ok := BuiltinProfiles[profileName]; ok {
|
||||||
|
return ActiveToolsForProfile(builtin.Profile, allToolNames), nil
|
||||||
|
}
|
||||||
|
p, ok := c.Profiles[profileName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("config: unknown profile %q", profileName)
|
||||||
|
}
|
||||||
|
return ActiveToolsForProfile(p, allToolNames), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReadonly reports whether the config file at path cannot be written to.
|
||||||
|
// Returns false if the file does not exist (it can still be created).
|
||||||
|
func IsReadonly(path string) bool {
|
||||||
|
f, err := os.OpenFile(path, os.O_WRONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return os.IsPermission(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(path string) (*Config, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Printf("config: %q not found, starting with empty config", path)
|
||||||
|
return Default(), nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("config: open %q: %w", path, err)
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
dec := yaml.NewDecoder(f)
|
||||||
|
dec.KnownFields(true)
|
||||||
|
if err := dec.Decode(&cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("config: decode: %w", err)
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() *Config {
|
||||||
|
return &Config{
|
||||||
|
Tools: make(map[string]yaml.Node),
|
||||||
|
Profiles: make(map[string]Profile),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save(path string, cfg *Config) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("config: create %q: %w", path, err)
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
enc := yaml.NewEncoder(f)
|
||||||
|
enc.SetIndent(2)
|
||||||
|
if err := enc.Encode(cfg); err != nil {
|
||||||
|
return fmt.Errorf("config: encode: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeNodePatch merges patch key-values into an existing yaml.Node (mapping).
|
||||||
|
// If existing is a zero value, it starts from an empty mapping.
|
||||||
|
func MergeNodePatch(existing yaml.Node, patch map[string]any) (yaml.Node, error) {
|
||||||
|
var m map[string]any
|
||||||
|
if existing.Kind != 0 {
|
||||||
|
if err := existing.Decode(&m); err != nil {
|
||||||
|
return yaml.Node{}, fmt.Errorf("config: decode node: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m == nil {
|
||||||
|
m = make(map[string]any)
|
||||||
|
}
|
||||||
|
for k, v := range patch {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := yaml.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return yaml.Node{}, fmt.Errorf("config: marshal: %w", err)
|
||||||
|
}
|
||||||
|
var doc yaml.Node
|
||||||
|
if err := yaml.Unmarshal(b, &doc); err != nil {
|
||||||
|
return yaml.Node{}, fmt.Errorf("config: unmarshal: %w", err)
|
||||||
|
}
|
||||||
|
if doc.Kind == yaml.DocumentNode && len(doc.Content) == 1 {
|
||||||
|
return *doc.Content[0], nil
|
||||||
|
}
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
64
back/config/env/env.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port int
|
||||||
|
ConfigPath string
|
||||||
|
FrontDir string // when set, serves the Astro static build at "/"
|
||||||
|
SearchTTL time.Duration
|
||||||
|
CleanupInterval time.Duration
|
||||||
|
Demo bool // when true, disables searches and config mutations
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
cfg := &Config{
|
||||||
|
Port: 8080,
|
||||||
|
ConfigPath: "/etc/iky/config.yaml",
|
||||||
|
SearchTTL: 48 * time.Hour,
|
||||||
|
CleanupInterval: time.Hour,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("IKY_PORT"); v != "" {
|
||||||
|
p, err := strconv.Atoi(v)
|
||||||
|
if err != nil || p < 1 || p > 65535 {
|
||||||
|
return nil, fmt.Errorf("env: IKY_PORT %q is not a valid port number", v)
|
||||||
|
}
|
||||||
|
cfg.Port = p
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("IKY_CONFIG"); v != "" {
|
||||||
|
cfg.ConfigPath = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("IKY_FRONT_DIR"); v != "" {
|
||||||
|
cfg.FrontDir = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("IKY_SEARCH_TTL"); v != "" {
|
||||||
|
d, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("env: IKY_SEARCH_TTL %q is not a valid duration", v)
|
||||||
|
}
|
||||||
|
cfg.SearchTTL = d
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("IKY_CLEANUP_INTERVAL"); v != "" {
|
||||||
|
d, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("env: IKY_CLEANUP_INTERVAL %q is not a valid duration", v)
|
||||||
|
}
|
||||||
|
cfg.CleanupInterval = d
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("IKY_DEMO"); v == "true" || v == "1" {
|
||||||
|
cfg.Demo = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
20
back/go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module github.com/anotherhadi/iknowyou
|
||||||
|
|
||||||
|
go 1.25.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require github.com/creack/pty v1.1.24
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/projectdiscovery/wappalyzergo v0.2.75 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
golang.org/x/net v0.52.0 // indirect
|
||||||
|
golang.org/x/time v0.15.0 // indirect
|
||||||
|
)
|
||||||
22
back/go.sum
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
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/projectdiscovery/wappalyzergo v0.2.75 h1:ScmpgoYuIzERh4lJpjWPPY89PUWbhUu6vFbCYAr0kWc=
|
||||||
|
github.com/projectdiscovery/wappalyzergo v0.2.75/go.mod h1:hRsnKNleH693FFJsBOD5NMUDbxw/Q94f0Oq2OV04Q6M=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
593
back/internal/api/handler/config.go
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/config"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/respond"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigHandler struct {
|
||||||
|
configPath string
|
||||||
|
factories []func() tools.ToolRunner
|
||||||
|
demo bool
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigHandler(configPath string, factories []func() tools.ToolRunner, demo bool) *ConfigHandler {
|
||||||
|
return &ConfigHandler{configPath: configPath, factories: factories, demo: demo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/config
|
||||||
|
func (h *ConfigHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolConfigs := make(map[string]any, len(cfg.Tools))
|
||||||
|
for toolName, node := range cfg.Tools {
|
||||||
|
var m map[string]any
|
||||||
|
if err := node.Decode(&m); err == nil {
|
||||||
|
toolConfigs[toolName] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respond.JSON(w, http.StatusOK, map[string]any{
|
||||||
|
"tools": toolConfigs,
|
||||||
|
"profiles": cfg.Profiles,
|
||||||
|
"readonly": h.demo || config.IsReadonly(h.configPath),
|
||||||
|
"demo": h.demo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type profileSummary struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
Readonly bool `json:"readonly"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/config/profiles
|
||||||
|
func (h *ConfigHandler) ListProfiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builtinNames := make([]string, 0, len(config.BuiltinProfiles))
|
||||||
|
for name := range config.BuiltinProfiles {
|
||||||
|
builtinNames = append(builtinNames, name)
|
||||||
|
}
|
||||||
|
sort.Strings(builtinNames)
|
||||||
|
summaries := make([]profileSummary, 0, len(builtinNames)+len(cfg.Profiles))
|
||||||
|
for _, name := range builtinNames {
|
||||||
|
summaries = append(summaries, profileSummary{Name: name, Notes: config.BuiltinProfiles[name].Notes, Readonly: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(cfg.Profiles))
|
||||||
|
for name := range cfg.Profiles {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, name := range names {
|
||||||
|
p := cfg.Profiles[name]
|
||||||
|
summaries = append(summaries, profileSummary{Name: name, Notes: p.Notes, Readonly: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, summaries)
|
||||||
|
}
|
||||||
|
|
||||||
|
type profileDetail struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
Readonly bool `json:"readonly"`
|
||||||
|
Enabled []string `json:"enabled"`
|
||||||
|
Disabled []string `json:"disabled"`
|
||||||
|
Tools map[string]any `json:"tools"`
|
||||||
|
ActiveTools []string `json:"active_tools"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/config/profiles/{name}
|
||||||
|
func (h *ConfigHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
|
||||||
|
allNames := make([]string, 0, len(h.factories))
|
||||||
|
for _, factory := range h.factories {
|
||||||
|
allNames = append(allNames, factory().Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if builtin, ok := config.BuiltinProfiles[name]; ok {
|
||||||
|
activeTools := config.ActiveToolsForProfile(builtin.Profile, allNames)
|
||||||
|
if activeTools == nil {
|
||||||
|
activeTools = allNames
|
||||||
|
}
|
||||||
|
enabled := builtin.Profile.Enabled
|
||||||
|
if enabled == nil {
|
||||||
|
enabled = []string{}
|
||||||
|
}
|
||||||
|
disabled := builtin.Profile.Disabled
|
||||||
|
if disabled == nil {
|
||||||
|
disabled = []string{}
|
||||||
|
}
|
||||||
|
respond.JSON(w, http.StatusOK, profileDetail{
|
||||||
|
Name: name,
|
||||||
|
Notes: builtin.Notes,
|
||||||
|
Readonly: true,
|
||||||
|
Enabled: enabled,
|
||||||
|
Disabled: disabled,
|
||||||
|
Tools: map[string]any{},
|
||||||
|
ActiveTools: activeTools,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, ok := cfg.Profiles[name]
|
||||||
|
if !ok {
|
||||||
|
respond.Error(w, http.StatusNotFound, "profile not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toolOverrides := make(map[string]any, len(p.Tools))
|
||||||
|
for toolName, node := range p.Tools {
|
||||||
|
var m map[string]any
|
||||||
|
if err := node.Decode(&m); err == nil {
|
||||||
|
toolOverrides[toolName] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTools, _ := cfg.ActiveTools(name, allNames)
|
||||||
|
|
||||||
|
enabled := p.Enabled
|
||||||
|
if enabled == nil {
|
||||||
|
enabled = []string{}
|
||||||
|
}
|
||||||
|
disabled := p.Disabled
|
||||||
|
if disabled == nil {
|
||||||
|
disabled = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, profileDetail{
|
||||||
|
Name: name,
|
||||||
|
Notes: p.Notes,
|
||||||
|
Readonly: false,
|
||||||
|
Enabled: enabled,
|
||||||
|
Disabled: disabled,
|
||||||
|
Tools: toolOverrides,
|
||||||
|
ActiveTools: activeTools,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/config/profiles
|
||||||
|
func (h *ConfigHandler) CreateProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.IsReadonly(h.configPath) {
|
||||||
|
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
Enabled []string `json:"enabled"`
|
||||||
|
Disabled []string `json:"disabled"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Name == "" {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateProfileName(req.Name); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, isBuiltin := config.BuiltinProfiles[req.Name]; isBuiltin {
|
||||||
|
respond.Error(w, http.StatusForbidden, fmt.Sprintf("profile %q is reserved", req.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, exists := cfg.Profiles[req.Name]; exists {
|
||||||
|
respond.Error(w, http.StatusConflict, "profile already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Profiles[req.Name] = config.Profile{
|
||||||
|
Notes: req.Notes,
|
||||||
|
Enabled: req.Enabled,
|
||||||
|
Disabled: req.Disabled,
|
||||||
|
}
|
||||||
|
if err := config.Save(h.configPath, cfg); err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respond.JSON(w, http.StatusCreated, cfg.Profiles[req.Name])
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /api/config/profiles/{name}
|
||||||
|
func (h *ConfigHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.IsReadonly(h.configPath) {
|
||||||
|
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
if _, isBuiltin := config.BuiltinProfiles[name]; isBuiltin {
|
||||||
|
respond.Error(w, http.StatusForbidden, fmt.Sprintf("profile %q is read-only", name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
Enabled *[]string `json:"enabled"`
|
||||||
|
Disabled *[]string `json:"disabled"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, ok := cfg.Profiles[name]
|
||||||
|
if !ok {
|
||||||
|
respond.Error(w, http.StatusNotFound, "profile not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Notes != nil {
|
||||||
|
p.Notes = *req.Notes
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
p.Enabled = *req.Enabled
|
||||||
|
}
|
||||||
|
if req.Disabled != nil {
|
||||||
|
p.Disabled = *req.Disabled
|
||||||
|
}
|
||||||
|
cfg.Profiles[name] = p
|
||||||
|
|
||||||
|
if err := config.Save(h.configPath, cfg); err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respond.JSON(w, http.StatusOK, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/config/profiles/{name}
|
||||||
|
func (h *ConfigHandler) DeleteProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.IsReadonly(h.configPath) {
|
||||||
|
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
if _, isBuiltin := config.BuiltinProfiles[name]; isBuiltin {
|
||||||
|
respond.Error(w, http.StatusForbidden, fmt.Sprintf("profile %q is read-only", name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := cfg.Profiles[name]; !ok {
|
||||||
|
respond.Error(w, http.StatusNotFound, "profile not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(cfg.Profiles, name)
|
||||||
|
|
||||||
|
if err := config.Save(h.configPath, cfg); err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /api/config/tools/{toolName}
|
||||||
|
func (h *ConfigHandler) UpdateToolConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.IsReadonly(h.configPath) {
|
||||||
|
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolName := chi.URLParam(r, "toolName")
|
||||||
|
|
||||||
|
var patch map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.validatePatch(toolName, patch); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := config.MergeNodePatch(cfg.Tools[toolName], patch)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.Tools[toolName] = merged
|
||||||
|
|
||||||
|
if err := config.Save(h.configPath, cfg); err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
_ = merged.Decode(&result)
|
||||||
|
respond.JSON(w, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/config/tools/{toolName}
|
||||||
|
func (h *ConfigHandler) DeleteToolConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.IsReadonly(h.configPath) {
|
||||||
|
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolName := chi.URLParam(r, "toolName")
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := cfg.Tools[toolName]; !ok {
|
||||||
|
respond.Error(w, http.StatusNotFound, "no global config for this tool")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(cfg.Tools, toolName)
|
||||||
|
|
||||||
|
if err := config.Save(h.configPath, cfg); err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /api/config/profiles/{name}/tools/{toolName}
|
||||||
|
func (h *ConfigHandler) UpdateProfileToolConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.IsReadonly(h.configPath) {
|
||||||
|
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
toolName := chi.URLParam(r, "toolName")
|
||||||
|
|
||||||
|
if _, isBuiltin := config.BuiltinProfiles[name]; isBuiltin {
|
||||||
|
respond.Error(w, http.StatusForbidden, fmt.Sprintf("profile %q is read-only", name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var patch map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.validatePatch(toolName, patch); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, ok := cfg.Profiles[name]
|
||||||
|
if !ok {
|
||||||
|
respond.Error(w, http.StatusNotFound, "profile not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.Tools == nil {
|
||||||
|
p.Tools = make(map[string]yaml.Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := config.MergeNodePatch(p.Tools[toolName], patch)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.Tools[toolName] = merged
|
||||||
|
cfg.Profiles[name] = p
|
||||||
|
|
||||||
|
if err := config.Save(h.configPath, cfg); err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
_ = merged.Decode(&result)
|
||||||
|
respond.JSON(w, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/config/profiles/{name}/tools/{toolName}
|
||||||
|
func (h *ConfigHandler) DeleteProfileToolConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.IsReadonly(h.configPath) {
|
||||||
|
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
toolName := chi.URLParam(r, "toolName")
|
||||||
|
|
||||||
|
if _, isBuiltin := config.BuiltinProfiles[name]; isBuiltin {
|
||||||
|
respond.Error(w, http.StatusForbidden, fmt.Sprintf("profile %q is read-only", name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg, err := config.Load(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, ok := cfg.Profiles[name]
|
||||||
|
if !ok {
|
||||||
|
respond.Error(w, http.StatusNotFound, "profile not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := p.Tools[toolName]; !ok {
|
||||||
|
respond.Error(w, http.StatusNotFound, "no config override for this tool in profile")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(p.Tools, toolName)
|
||||||
|
cfg.Profiles[name] = p
|
||||||
|
|
||||||
|
if err := config.Save(h.configPath, cfg); err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateProfileName(name string) error {
|
||||||
|
for _, c := range name {
|
||||||
|
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') {
|
||||||
|
return fmt.Errorf("profile name must contain only lowercase letters (a-z), digits (0-9), and hyphens (-)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ConfigHandler) validatePatch(toolName string, patch map[string]any) error {
|
||||||
|
var fields []tools.ConfigField
|
||||||
|
for _, factory := range h.factories {
|
||||||
|
t := factory()
|
||||||
|
if t.Name() == toolName {
|
||||||
|
if d, ok := t.(tools.ConfigDescriber); ok {
|
||||||
|
fields = d.ConfigFields()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fieldMap := make(map[string]tools.ConfigField, len(fields))
|
||||||
|
for _, f := range fields {
|
||||||
|
fieldMap[f.Name] = f
|
||||||
|
}
|
||||||
|
for key, val := range patch {
|
||||||
|
f, ok := fieldMap[key]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := validateFieldValue(f, val); err != nil {
|
||||||
|
return fmt.Errorf("field %q: %w", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFieldValue(f tools.ConfigField, val any) error {
|
||||||
|
if val == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch f.Type {
|
||||||
|
case "string":
|
||||||
|
if _, ok := val.(string); !ok {
|
||||||
|
return fmt.Errorf("expected string, got %T", val)
|
||||||
|
}
|
||||||
|
case "bool":
|
||||||
|
if _, ok := val.(bool); !ok {
|
||||||
|
return fmt.Errorf("expected bool, got %T", val)
|
||||||
|
}
|
||||||
|
case "int":
|
||||||
|
switch v := val.(type) {
|
||||||
|
case float64:
|
||||||
|
if v != float64(int64(v)) {
|
||||||
|
return fmt.Errorf("expected integer, got float")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("expected int, got %T", val)
|
||||||
|
}
|
||||||
|
case "float":
|
||||||
|
if _, ok := val.(float64); !ok {
|
||||||
|
return fmt.Errorf("expected number, got %T", val)
|
||||||
|
}
|
||||||
|
case "enum":
|
||||||
|
s, ok := val.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expected string, got %T", val)
|
||||||
|
}
|
||||||
|
for _, opt := range f.Options {
|
||||||
|
if s == opt {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid value %q, must be one of: %v", s, f.Options)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
146
back/internal/api/handler/search.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/respond"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/search"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SearchHandler struct {
|
||||||
|
manager *search.Manager
|
||||||
|
demo bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSearchHandler(manager *search.Manager, demo bool) *SearchHandler {
|
||||||
|
return &SearchHandler{manager: manager, demo: demo}
|
||||||
|
}
|
||||||
|
|
||||||
|
type postSearchRequest struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
InputType tools.InputType `json:"input_type"`
|
||||||
|
Profile string `json:"profile,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type searchSummary struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
InputType tools.InputType `json:"input_type"`
|
||||||
|
Profile string `json:"profile,omitempty"`
|
||||||
|
Status search.Status `json:"status"`
|
||||||
|
StartedAt string `json:"started_at"`
|
||||||
|
PlannedTools []search.ToolStatus `json:"planned_tools"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type searchDetail struct {
|
||||||
|
searchSummary
|
||||||
|
Events []tools.Event `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSummary(s *search.Search) searchSummary {
|
||||||
|
planned := s.PlannedTools
|
||||||
|
if planned == nil {
|
||||||
|
planned = []search.ToolStatus{}
|
||||||
|
}
|
||||||
|
return searchSummary{
|
||||||
|
ID: s.ID,
|
||||||
|
Target: s.Target,
|
||||||
|
InputType: s.InputType,
|
||||||
|
Profile: s.Profile,
|
||||||
|
Status: s.Status(),
|
||||||
|
StartedAt: s.StartedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
|
PlannedTools: planned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var validInputTypes = map[tools.InputType]struct{}{
|
||||||
|
tools.InputTypeEmail: {},
|
||||||
|
tools.InputTypeUsername: {},
|
||||||
|
tools.InputTypePhone: {},
|
||||||
|
tools.InputTypeIP: {},
|
||||||
|
tools.InputTypeDomain: {},
|
||||||
|
tools.InputTypePassword: {},
|
||||||
|
tools.InputTypeName: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /searches
|
||||||
|
func (h *SearchHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.demo {
|
||||||
|
respond.Error(w, http.StatusForbidden, "demo mode: searches are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req postSearchRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "invalid JSON body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Target == "" {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "target is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Target) > 500 {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "target is too long (max 500 characters)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Target[0] == '-' || req.Target[0] == '@' {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "invalid target")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.InputType == "" {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "input_type is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := validInputTypes[req.InputType]; !ok {
|
||||||
|
respond.Error(w, http.StatusBadRequest, "invalid input_type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := h.manager.Start(context.WithoutCancel(r.Context()), req.Target, req.InputType, req.Profile)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusCreated, toSummary(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /searches
|
||||||
|
func (h *SearchHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||||
|
all := h.manager.All()
|
||||||
|
summaries := make([]searchSummary, len(all))
|
||||||
|
for i, s := range all {
|
||||||
|
summaries[i] = toSummary(s)
|
||||||
|
}
|
||||||
|
respond.JSON(w, http.StatusOK, summaries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /searches/{id}
|
||||||
|
func (h *SearchHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
s, err := h.manager.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
respond.Error(w, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detail := searchDetail{
|
||||||
|
searchSummary: toSummary(s),
|
||||||
|
Events: s.Events(),
|
||||||
|
}
|
||||||
|
respond.JSON(w, http.StatusOK, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /searches/{id}
|
||||||
|
func (h *SearchHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
if err := h.manager.Delete(id); err != nil {
|
||||||
|
respond.Error(w, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
91
back/internal/api/handler/tools.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/respond"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToolsHandler struct {
|
||||||
|
factories []func() tools.ToolRunner
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewToolsHandler(factories []func() tools.ToolRunner) *ToolsHandler {
|
||||||
|
return &ToolsHandler{factories: factories}
|
||||||
|
}
|
||||||
|
|
||||||
|
type toolInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Link string `json:"link,omitempty"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
|
InputTypes []tools.InputType `json:"input_types"`
|
||||||
|
Configurable bool `json:"configurable"`
|
||||||
|
ConfigFields []tools.ConfigField `json:"config_fields,omitempty"`
|
||||||
|
Available *bool `json:"available,omitempty"`
|
||||||
|
UnavailableReason string `json:"unavailable_reason,omitempty"`
|
||||||
|
Dependencies []string `json:"dependencies,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toToolInfo(t tools.ToolRunner) toolInfo {
|
||||||
|
_, configurable := t.(tools.Configurable)
|
||||||
|
|
||||||
|
var fields []tools.ConfigField
|
||||||
|
if d, ok := t.(tools.ConfigDescriber); ok {
|
||||||
|
fields = d.ConfigFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
var available *bool
|
||||||
|
var unavailableReason string
|
||||||
|
if checker, ok := t.(tools.AvailabilityChecker); ok {
|
||||||
|
avail, reason := checker.Available()
|
||||||
|
available = &avail
|
||||||
|
if !avail {
|
||||||
|
unavailableReason = reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dependencies []string
|
||||||
|
if lister, ok := t.(tools.DependencyLister); ok {
|
||||||
|
dependencies = lister.Dependencies()
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolInfo{
|
||||||
|
Name: t.Name(),
|
||||||
|
Description: t.Description(),
|
||||||
|
Link: t.Link(),
|
||||||
|
Icon: t.Icon(),
|
||||||
|
InputTypes: t.InputTypes(),
|
||||||
|
Configurable: configurable,
|
||||||
|
ConfigFields: fields,
|
||||||
|
Available: available,
|
||||||
|
UnavailableReason: unavailableReason,
|
||||||
|
Dependencies: dependencies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/tools
|
||||||
|
func (h *ToolsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||||
|
infos := make([]toolInfo, 0, len(h.factories))
|
||||||
|
for _, factory := range h.factories {
|
||||||
|
infos = append(infos, toToolInfo(factory()))
|
||||||
|
}
|
||||||
|
respond.JSON(w, http.StatusOK, infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/tools/{name}
|
||||||
|
func (h *ToolsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
for _, factory := range h.factories {
|
||||||
|
t := factory()
|
||||||
|
if t.Name() == name {
|
||||||
|
respond.JSON(w, http.StatusOK, toToolInfo(t))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respond.Error(w, http.StatusNotFound, fmt.Sprintf("tool %q not found", name))
|
||||||
|
}
|
||||||
72
back/internal/api/middleware/ratelimit.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ipLimiter struct {
|
||||||
|
limiter *rate.Limiter
|
||||||
|
lastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Limiter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
visitors map[string]*ipLimiter
|
||||||
|
r rate.Limit
|
||||||
|
burst int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(r rate.Limit, burst int) *Limiter {
|
||||||
|
l := &Limiter{
|
||||||
|
visitors: make(map[string]*ipLimiter),
|
||||||
|
r: r,
|
||||||
|
burst: burst,
|
||||||
|
}
|
||||||
|
go l.cleanupLoop()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Limiter) getLimiter(ip string) *rate.Limiter {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
v, exists := l.visitors[ip]
|
||||||
|
if !exists {
|
||||||
|
v = &ipLimiter{limiter: rate.NewLimiter(l.r, l.burst)}
|
||||||
|
l.visitors[ip] = v
|
||||||
|
}
|
||||||
|
v.lastSeen = time.Now()
|
||||||
|
return v.limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Limiter) cleanupLoop() {
|
||||||
|
ticker := time.NewTicker(10 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
l.mu.Lock()
|
||||||
|
for ip, v := range l.visitors {
|
||||||
|
if time.Since(v.lastSeen) > 10*time.Minute {
|
||||||
|
delete(l.visitors, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Limiter) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
ip = r.RemoteAddr
|
||||||
|
}
|
||||||
|
if !l.getLimiter(ip).Allow() {
|
||||||
|
http.Error(w, `{"error":"rate limit exceeded, please slow down"}`, http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
73
back/internal/api/router.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/api/handler"
|
||||||
|
ikymiddleware "github.com/anotherhadi/iknowyou/internal/api/middleware"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/search"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRouter(
|
||||||
|
manager *search.Manager,
|
||||||
|
factories []func() tools.ToolRunner,
|
||||||
|
configPath string,
|
||||||
|
frontDir string,
|
||||||
|
demo bool,
|
||||||
|
) *chi.Mux {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
r.Use(chimiddleware.Logger)
|
||||||
|
r.Use(chimiddleware.Recoverer)
|
||||||
|
r.Use(chimiddleware.RequestID)
|
||||||
|
|
||||||
|
searchHandler := handler.NewSearchHandler(manager, demo)
|
||||||
|
toolsHandler := handler.NewToolsHandler(factories)
|
||||||
|
configHandler := handler.NewConfigHandler(configPath, factories, demo)
|
||||||
|
|
||||||
|
searchLimiter := ikymiddleware.New(rate.Every(10*time.Second), 3)
|
||||||
|
|
||||||
|
r.Route("/api", func(r chi.Router) {
|
||||||
|
r.Route("/searches", func(r chi.Router) {
|
||||||
|
r.With(searchLimiter.Handler).Post("/", searchHandler.Create)
|
||||||
|
r.Get("/", searchHandler.List)
|
||||||
|
r.Get("/{id}", searchHandler.Get)
|
||||||
|
r.Delete("/{id}", searchHandler.Delete)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/tools", func(r chi.Router) {
|
||||||
|
r.Get("/", toolsHandler.List)
|
||||||
|
r.Get("/{name}", toolsHandler.Get)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/config", func(r chi.Router) {
|
||||||
|
r.Get("/", configHandler.Get)
|
||||||
|
|
||||||
|
r.Route("/tools", func(r chi.Router) {
|
||||||
|
r.Patch("/{toolName}", configHandler.UpdateToolConfig)
|
||||||
|
r.Delete("/{toolName}", configHandler.DeleteToolConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/profiles", func(r chi.Router) {
|
||||||
|
r.Get("/", configHandler.ListProfiles)
|
||||||
|
r.Post("/", configHandler.CreateProfile)
|
||||||
|
r.Get("/{name}", configHandler.GetProfile)
|
||||||
|
r.Patch("/{name}", configHandler.UpdateProfile)
|
||||||
|
r.Delete("/{name}", configHandler.DeleteProfile)
|
||||||
|
r.Patch("/{name}/tools/{toolName}", configHandler.UpdateProfileToolConfig)
|
||||||
|
r.Delete("/{name}/tools/{toolName}", configHandler.DeleteProfileToolConfig)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if frontDir != "" {
|
||||||
|
r.Handle("/*", newStaticHandler(frontDir))
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
56
back/internal/api/static.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newStaticHandler serves the Astro static build with SPA fallbacks:
|
||||||
|
// /search/<id> and /tools/<name> → their respective shell pages.
|
||||||
|
func newStaticHandler(dir string) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
urlPath := r.URL.Path
|
||||||
|
|
||||||
|
if strings.HasPrefix(urlPath, "/search/") && len(urlPath) > len("/search/") {
|
||||||
|
http.ServeFile(w, r, filepath.Join(dir, "search", "_", "index.html"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(urlPath, "/tools/") {
|
||||||
|
rest := strings.TrimPrefix(urlPath, "/tools/")
|
||||||
|
if rest != "" && !strings.Contains(rest, "/") {
|
||||||
|
http.ServeFile(w, r, filepath.Join(dir, "tools", "_", "index.html"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rel := filepath.FromSlash(strings.TrimPrefix(urlPath, "/"))
|
||||||
|
full := filepath.Join(dir, rel)
|
||||||
|
|
||||||
|
info, err := os.Stat(full)
|
||||||
|
if err == nil && info.IsDir() {
|
||||||
|
full = filepath.Join(full, "index.html")
|
||||||
|
if _, err2 := os.Stat(full); err2 != nil {
|
||||||
|
serve404(w, r, dir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
serve404(w, r, dir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeFile(w, r, full)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serve404(w http.ResponseWriter, r *http.Request, dir string) {
|
||||||
|
p := filepath.Join(dir, "404.html")
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
http.ServeFile(w, r, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
32
back/internal/registry/registry.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
breachdirectory "github.com/anotherhadi/iknowyou/internal/tools/breachdirectory"
|
||||||
|
crtsh "github.com/anotherhadi/iknowyou/internal/tools/crtsh"
|
||||||
|
digtool "github.com/anotherhadi/iknowyou/internal/tools/dig"
|
||||||
|
githubrecon "github.com/anotherhadi/iknowyou/internal/tools/github-recon"
|
||||||
|
gravatarrecon "github.com/anotherhadi/iknowyou/internal/tools/gravatar-recon"
|
||||||
|
ipinfotool "github.com/anotherhadi/iknowyou/internal/tools/ipinfo"
|
||||||
|
leakcheck "github.com/anotherhadi/iknowyou/internal/tools/leakcheck"
|
||||||
|
maigret "github.com/anotherhadi/iknowyou/internal/tools/maigret"
|
||||||
|
userscanner "github.com/anotherhadi/iknowyou/internal/tools/user-scanner"
|
||||||
|
wappalyzer "github.com/anotherhadi/iknowyou/internal/tools/wappalyzer"
|
||||||
|
whoistool "github.com/anotherhadi/iknowyou/internal/tools/whois"
|
||||||
|
whoisfreaks "github.com/anotherhadi/iknowyou/internal/tools/whoisfreaks"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Factories = []func() tools.ToolRunner{
|
||||||
|
userscanner.New,
|
||||||
|
githubrecon.New,
|
||||||
|
whoistool.New,
|
||||||
|
digtool.New,
|
||||||
|
ipinfotool.New,
|
||||||
|
gravatarrecon.New,
|
||||||
|
whoisfreaks.New,
|
||||||
|
maigret.New,
|
||||||
|
leakcheck.New,
|
||||||
|
crtsh.New,
|
||||||
|
breachdirectory.New,
|
||||||
|
wappalyzer.New,
|
||||||
|
}
|
||||||
18
back/internal/respond/respond.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package respond
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON writes a JSON body with the given status code.
|
||||||
|
func JSON(w http.ResponseWriter, status int, payload any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error writes a JSON error body: {"error": "message"}.
|
||||||
|
func Error(w http.ResponseWriter, status int, msg string) {
|
||||||
|
JSON(w, status, map[string]string{"error": msg})
|
||||||
|
}
|
||||||
114
back/internal/search/demo.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ptr(n int) *int { return &n }
|
||||||
|
|
||||||
|
func (m *Manager) InjectDemoSearches() {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
_, cancel1 := context.WithCancel(context.Background())
|
||||||
|
s1 := &Search{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Target: "john.doe@example.com",
|
||||||
|
InputType: tools.InputTypeEmail,
|
||||||
|
Profile: "default",
|
||||||
|
StartedAt: now.Add(-2 * time.Hour),
|
||||||
|
PlannedTools: []ToolStatus{
|
||||||
|
{Name: "user-scanner", ResultCount: ptr(10)},
|
||||||
|
{Name: "github-recon", ResultCount: ptr(3)},
|
||||||
|
},
|
||||||
|
cancelFn: cancel1,
|
||||||
|
status: StatusDone,
|
||||||
|
finishedAt: now.Add(-2*time.Hour + 18*time.Second),
|
||||||
|
}
|
||||||
|
s1.events = []tools.Event{
|
||||||
|
{Tool: "user-scanner", Type: tools.EventTypeOutput, Payload: "\x1b[35m== ADULT SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Xvideos (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Pornhub (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== CREATOR SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Adobe (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== MUSIC SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Spotify (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== LEARNING SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Duolingo (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Vedantu (john.doe@example.com): Registered\n \x1b[36m└── Phone: +9112****07\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== SOCIAL SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Pinterest (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Facebook (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== GAMING SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Chess.com (john.doe@example.com): Registered\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== SHOPPING SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Amazon (john.doe@example.com): Registered\x1b[0m\n"},
|
||||||
|
{Tool: "user-scanner", Type: tools.EventTypeDone},
|
||||||
|
{Tool: "github-recon", Type: tools.EventTypeOutput, Payload: "\x1b[1;38;2;113;135;253m👤 Commits author\x1b[0m\n\n" +
|
||||||
|
" \x1b[38;2;125;125;125mName:\x1b[0m \x1b[38;2;166;227;161m\"fastHack2025\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mEmail:\x1b[0m \x1b[38;2;166;227;161m\"john.doe@example.com\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mUsername:\x1b[0m \x1b[38;2;166;227;161m\"Unknown\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mOccurrences:\x1b[0m \x1b[38;2;166;227;161m36\x1b[0m\n\n" +
|
||||||
|
" \x1b[38;2;125;125;125mName:\x1b[0m \x1b[38;2;166;227;161m\"Anthony\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mEmail:\x1b[0m \x1b[38;2;166;227;161m\"john.doe@example.com\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mUsername:\x1b[0m \x1b[38;2;166;227;161m\"Unknown\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mOccurrences:\x1b[0m \x1b[38;2;166;227;161m52\x1b[0m\n\n" +
|
||||||
|
" \x1b[38;2;125;125;125mName:\x1b[0m \x1b[38;2;166;227;161m\"Gill\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mEmail:\x1b[0m \x1b[38;2;166;227;161m\"john.doe@example.com\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mUsername:\x1b[0m \x1b[38;2;166;227;161m\"johndoe\"\x1b[0m\n" +
|
||||||
|
" \x1b[38;2;125;125;125mOccurrences:\x1b[0m \x1b[38;2;166;227;161m60\x1b[0m"},
|
||||||
|
{Tool: "github-recon", Type: tools.EventTypeDone},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, cancel2 := context.WithCancel(context.Background())
|
||||||
|
s2 := &Search{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Target: "janedoe",
|
||||||
|
InputType: tools.InputTypeUsername,
|
||||||
|
Profile: "default",
|
||||||
|
StartedAt: now.Add(-30 * time.Minute),
|
||||||
|
PlannedTools: []ToolStatus{
|
||||||
|
{Name: "user-scanner", ResultCount: ptr(10)},
|
||||||
|
{Name: "github-recon", ResultCount: ptr(0)},
|
||||||
|
},
|
||||||
|
cancelFn: cancel2,
|
||||||
|
status: StatusDone,
|
||||||
|
finishedAt: now.Add(-30*time.Minute + 22*time.Second),
|
||||||
|
}
|
||||||
|
s2.events = []tools.Event{
|
||||||
|
{Tool: "user-scanner", Type: tools.EventTypeOutput, Payload: "\x1b[35m== SOCIAL SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Reddit (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Threads (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] X (twitter) (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Youtube (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Telegram (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Tiktok (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Instagram (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== GAMING SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Chess.com (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Roblox (janedoe): Found\x1b[0m\n" +
|
||||||
|
"\x1b[0m\n" +
|
||||||
|
"\x1b[35m== EMAIL SITES ==\x1b[0m\n" +
|
||||||
|
"\x1b[0m \x1b[32m[✔] Protonmail (janedoe): Found\x1b[0m"},
|
||||||
|
{Tool: "user-scanner", Type: tools.EventTypeDone},
|
||||||
|
{Tool: "github-recon", Type: tools.EventTypeOutput, Payload: "\x1b[1;38;2;113;135;253m👤 User informations\x1b[0m\n\n" +
|
||||||
|
" \x1b[38;2;125;125;125mNo data found\x1b[0m"},
|
||||||
|
{Tool: "github-recon", Type: tools.EventTypeDone},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.searches[s1.ID] = s1
|
||||||
|
m.searches[s2.ID] = s2
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
274
back/internal/search/manager.go
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/config"
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
searches map[string]*Search
|
||||||
|
|
||||||
|
configPath string
|
||||||
|
factories []func() tools.ToolRunner
|
||||||
|
searchTTL time.Duration
|
||||||
|
cleanupInterval time.Duration
|
||||||
|
|
||||||
|
done chan struct{} // closed by Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(configPath string, factories []func() tools.ToolRunner, searchTTL, cleanupInterval time.Duration) *Manager {
|
||||||
|
m := &Manager{
|
||||||
|
searches: make(map[string]*Search),
|
||||||
|
configPath: configPath,
|
||||||
|
factories: factories,
|
||||||
|
searchTTL: searchTTL,
|
||||||
|
cleanupInterval: cleanupInterval,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go m.cleanupLoop()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Stop() {
|
||||||
|
close(m.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(
|
||||||
|
parentCtx context.Context,
|
||||||
|
target string,
|
||||||
|
inputType tools.InputType,
|
||||||
|
profileName string,
|
||||||
|
) (*Search, error) {
|
||||||
|
|
||||||
|
// "default" is the canonical UI name for the no-filter profile.
|
||||||
|
if profileName == "default" {
|
||||||
|
profileName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("manager: loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTools, statuses, err := m.instantiate(cfg, inputType, profileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
|
|
||||||
|
s := &Search{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Target: target,
|
||||||
|
InputType: inputType,
|
||||||
|
Profile: profileName,
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
PlannedTools: statuses,
|
||||||
|
cancelFn: cancel,
|
||||||
|
status: StatusRunning,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.searches[s.ID] = s
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
go m.runAll(ctx, s, activeTools)
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Get(id string) (*Search, error) {
|
||||||
|
return m.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) All() []*Search {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
out := make([]*Search, 0, len(m.searches))
|
||||||
|
for _, s := range m.searches {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Delete(id string) error {
|
||||||
|
s, err := m.get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Cancel()
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
delete(m.searches, id)
|
||||||
|
m.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) cleanupLoop() {
|
||||||
|
ticker := time.NewTicker(m.cleanupInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
m.purgeExpired()
|
||||||
|
case <-m.done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) purgeExpired() {
|
||||||
|
now := time.Now()
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
for id, s := range m.searches {
|
||||||
|
ft := s.FinishedAt()
|
||||||
|
if ft.IsZero() {
|
||||||
|
continue // still running
|
||||||
|
}
|
||||||
|
if now.Sub(ft) > m.searchTTL {
|
||||||
|
delete(m.searches, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) instantiate(cfg *config.Config, inputType tools.InputType, profileName string) ([]tools.ToolRunner, []ToolStatus, error) {
|
||||||
|
allNames := make([]string, len(m.factories))
|
||||||
|
allInstances := make([]tools.ToolRunner, len(m.factories))
|
||||||
|
for i, factory := range m.factories {
|
||||||
|
t := factory()
|
||||||
|
allNames[i] = t.Name()
|
||||||
|
allInstances[i] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
activeNames, err := cfg.ActiveTools(profileName, allNames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
activeSet := make(map[string]struct{}, len(activeNames))
|
||||||
|
for _, n := range activeNames {
|
||||||
|
activeSet[n] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runners []tools.ToolRunner
|
||||||
|
var statuses []ToolStatus
|
||||||
|
|
||||||
|
for _, tool := range allInstances {
|
||||||
|
if _, ok := activeSet[tool.Name()]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !acceptsInputType(tool, inputType) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, ok := tool.(tools.AvailabilityChecker); ok {
|
||||||
|
if available, reason := a.Available(); !available {
|
||||||
|
statuses = append(statuses, ToolStatus{
|
||||||
|
Name: tool.Name(),
|
||||||
|
Skipped: true,
|
||||||
|
Reason: reason,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, ok := tool.(tools.Configurable); ok {
|
||||||
|
if err := cfg.DecodeEffective(tool.Name(), profileName, c.ConfigPtr()); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("manager: configuring tool %q: %w", tool.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d, ok := tool.(tools.ConfigDescriber); ok {
|
||||||
|
if missing, fieldName := missingRequiredField(d.ConfigFields()); missing {
|
||||||
|
statuses = append(statuses, ToolStatus{
|
||||||
|
Name: tool.Name(),
|
||||||
|
Skipped: true,
|
||||||
|
Reason: fmt.Sprintf("missing required config field: %s", fieldName),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses = append(statuses, ToolStatus{Name: tool.Name()})
|
||||||
|
runners = append(runners, tool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runners, statuses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) runAll(ctx context.Context, s *Search, runners []tools.ToolRunner) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, tool := range runners {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(t tools.ToolRunner) {
|
||||||
|
defer wg.Done()
|
||||||
|
m.runOne(ctx, s, t)
|
||||||
|
}(tool)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
s.markDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) runOne(ctx context.Context, s *Search, tool tools.ToolRunner) {
|
||||||
|
out := make(chan tools.Event)
|
||||||
|
go func() {
|
||||||
|
_ = tool.Run(ctx, s.Target, s.InputType, out)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var count int
|
||||||
|
var hasCount bool
|
||||||
|
for e := range out {
|
||||||
|
if e.Type == tools.EventTypeCount {
|
||||||
|
if n, ok := e.Payload.(int); ok {
|
||||||
|
count += n
|
||||||
|
hasCount = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.append(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasCount {
|
||||||
|
s.setToolResultCount(tool.Name(), count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) get(id string) (*Search, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
s, ok := m.searches[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("search %q not found", id)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptsInputType(tool tools.ToolRunner, inputType tools.InputType) bool {
|
||||||
|
for _, t := range tool.InputTypes() {
|
||||||
|
if t == inputType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func missingRequiredField(fields []tools.ConfigField) (missing bool, fieldName string) {
|
||||||
|
for _, f := range fields {
|
||||||
|
if !f.Required {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.Value == nil || reflect.DeepEqual(f.Value, reflect.Zero(reflect.TypeOf(f.Value)).Interface()) {
|
||||||
|
return true, f.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
97
back/internal/search/search.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusRunning Status = "running"
|
||||||
|
StatusDone Status = "done"
|
||||||
|
StatusCancelled Status = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToolStatus struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Skipped bool `json:"skipped"`
|
||||||
|
Reason string `json:"reason,omitempty"` // non-empty only when Skipped is true
|
||||||
|
ResultCount *int `json:"result_count,omitempty"` // nil = pending, 0 = no results
|
||||||
|
}
|
||||||
|
|
||||||
|
type Search struct {
|
||||||
|
ID string
|
||||||
|
Target string
|
||||||
|
InputType tools.InputType
|
||||||
|
Profile string
|
||||||
|
StartedAt time.Time
|
||||||
|
PlannedTools []ToolStatus
|
||||||
|
|
||||||
|
cancelFn context.CancelFunc
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
events []tools.Event
|
||||||
|
status Status
|
||||||
|
finishedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Search) Events() []tools.Event {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
out := make([]tools.Event, len(s.events))
|
||||||
|
copy(out, s.events)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Search) Status() Status {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Search) FinishedAt() time.Time {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.finishedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Search) Cancel() {
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.status == StatusRunning {
|
||||||
|
s.status = StatusCancelled
|
||||||
|
s.finishedAt = time.Now()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
s.cancelFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Search) setToolResultCount(toolName string, count int) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
for i, t := range s.PlannedTools {
|
||||||
|
if t.Name == toolName {
|
||||||
|
s.PlannedTools[i].ResultCount = &count
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Search) append(e tools.Event) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.events = append(s.events, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Search) markDone() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.status == StatusRunning {
|
||||||
|
s.status = StatusDone
|
||||||
|
s.finishedAt = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
162
back/internal/tools/breachdirectory/tool.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package breachdirectory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "breachdirectory"
|
||||||
|
description = "Data breach search via BreachDirectory — checks if an email, username, or phone appears in known data breaches and returns exposed passwords/hashes."
|
||||||
|
link = "https://breachdirectory.org"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
APIKey string `yaml:"api_key" iky:"desc=RapidAPI key for BreachDirectory (required — get one at rapidapi.com/rohan-patra/api/breachdirectory);required=true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
cfg := Config{}
|
||||||
|
tools.ApplyDefaults(&cfg)
|
||||||
|
return &Runner{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{
|
||||||
|
tools.InputTypeEmail,
|
||||||
|
tools.InputTypeUsername,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
|
||||||
|
|
||||||
|
func (r *Runner) ConfigFields() []tools.ConfigField {
|
||||||
|
return tools.ReflectConfigFields(r.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type bdResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Found int `json:"found"`
|
||||||
|
Result json.RawMessage `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type bdEntry struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
SHA1 string `json:"sha1"`
|
||||||
|
Sources string `json:"sources"`
|
||||||
|
HasPassword bool `json:"has_password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||||
|
"https://breachdirectory.p.rapidapi.com/?func=auto&term="+target, nil)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req.Header.Set("X-RapidAPI-Key", r.cfg.APIKey)
|
||||||
|
req.Header.Set("X-RapidAPI-Host", "breachdirectory.p.rapidapi.com")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed bdResponse
|
||||||
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed.Success || parsed.Found == 0 {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []bdEntry
|
||||||
|
if err := json.Unmarshal(parsed.Result, &entries); err != nil || len(entries) == 0 {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Found in %d breach record(s)\n\n", parsed.Found))
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Sources != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("Source: %s\n", entry.Sources))
|
||||||
|
}
|
||||||
|
if entry.Password != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("Password: %s\n", entry.Password))
|
||||||
|
}
|
||||||
|
if entry.Hash != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("Hash: %s\n", entry.Hash))
|
||||||
|
}
|
||||||
|
if entry.SHA1 != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("SHA1: %s\n", entry.SHA1))
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: parsed.Found}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
155
back/internal/tools/config_reflect.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReflectConfigFields builds []ConfigField from a struct using yaml/iky tags.
|
||||||
|
// iky tag format: iky:"desc=...;default=...;required=true;options=a|b|c"
|
||||||
|
func ReflectConfigFields(cfg any) []ConfigField {
|
||||||
|
v := reflect.ValueOf(cfg)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
var fields []ConfigField
|
||||||
|
for i := range t.NumField() {
|
||||||
|
sf := t.Field(i)
|
||||||
|
fv := v.Field(i)
|
||||||
|
|
||||||
|
yamlKey := sf.Tag.Get("yaml")
|
||||||
|
if yamlKey == "" || yamlKey == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
yamlKey = strings.SplitN(yamlKey, ",", 2)[0]
|
||||||
|
|
||||||
|
meta := parseIkyTag(sf.Tag.Get("iky"))
|
||||||
|
|
||||||
|
fieldType := goKindToString(sf.Type.Kind())
|
||||||
|
if len(meta.options) > 0 {
|
||||||
|
fieldType = "enum"
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = append(fields, ConfigField{
|
||||||
|
Name: yamlKey,
|
||||||
|
Type: fieldType,
|
||||||
|
Required: meta.required,
|
||||||
|
Description: meta.desc,
|
||||||
|
Default: parseTypedDefault(meta.rawDefault, sf.Type.Kind()),
|
||||||
|
Value: fv.Interface(),
|
||||||
|
Options: meta.options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyDefaults sets each field to its iky default if the field is zero.
|
||||||
|
func ApplyDefaults(cfg any) {
|
||||||
|
v := reflect.ValueOf(cfg)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
for i := range t.NumField() {
|
||||||
|
sf := t.Field(i)
|
||||||
|
fv := v.Field(i)
|
||||||
|
|
||||||
|
if !fv.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta := parseIkyTag(sf.Tag.Get("iky"))
|
||||||
|
if meta.rawDefault == "" || !fv.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
applyDefault(fv, sf.Type.Kind(), meta.rawDefault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ikyMeta struct {
|
||||||
|
desc string
|
||||||
|
rawDefault string
|
||||||
|
required bool
|
||||||
|
options []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIkyTag(tag string) ikyMeta {
|
||||||
|
var m ikyMeta
|
||||||
|
for _, part := range strings.Split(tag, ";") {
|
||||||
|
k, v, ok := strings.Cut(strings.TrimSpace(part), "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(k) {
|
||||||
|
case "desc":
|
||||||
|
m.desc = strings.TrimSpace(v)
|
||||||
|
case "default":
|
||||||
|
m.rawDefault = strings.TrimSpace(v)
|
||||||
|
case "required":
|
||||||
|
m.required = strings.TrimSpace(v) == "true"
|
||||||
|
case "options":
|
||||||
|
for _, opt := range strings.Split(v, "|") {
|
||||||
|
if o := strings.TrimSpace(opt); o != "" {
|
||||||
|
m.options = append(m.options, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func goKindToString(k reflect.Kind) string {
|
||||||
|
switch k {
|
||||||
|
case reflect.String:
|
||||||
|
return "string"
|
||||||
|
case reflect.Bool:
|
||||||
|
return "bool"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return "int"
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return "float"
|
||||||
|
default:
|
||||||
|
return k.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTypedDefault(raw string, k reflect.Kind) any {
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch k {
|
||||||
|
case reflect.Bool:
|
||||||
|
b, _ := strconv.ParseBool(raw)
|
||||||
|
return b
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
n, _ := strconv.ParseInt(raw, 10, 64)
|
||||||
|
return int(n)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
f, _ := strconv.ParseFloat(raw, 64)
|
||||||
|
return f
|
||||||
|
default:
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyDefault(fv reflect.Value, k reflect.Kind, raw string) {
|
||||||
|
switch k {
|
||||||
|
case reflect.String:
|
||||||
|
fv.SetString(raw)
|
||||||
|
case reflect.Bool:
|
||||||
|
if b, err := strconv.ParseBool(raw); err == nil {
|
||||||
|
fv.SetBool(b)
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||||
|
fv.SetInt(n)
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
if f, err := strconv.ParseFloat(raw, 64); err == nil {
|
||||||
|
fv.SetFloat(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
back/internal/tools/crtsh/tool.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package crtsh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "crt.sh"
|
||||||
|
description = "SSL/TLS certificate transparency log search via crt.sh — enumerates subdomains and certificates issued for a domain."
|
||||||
|
link = "https://crt.sh"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Runner struct{}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
return &Runner{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{tools.InputTypeDomain}
|
||||||
|
}
|
||||||
|
|
||||||
|
type crtEntry struct {
|
||||||
|
IssuerName string `json:"issuer_name"`
|
||||||
|
CommonName string `json:"common_name"`
|
||||||
|
NameValue string `json:"name_value"`
|
||||||
|
NotBefore string `json:"not_before"`
|
||||||
|
NotAfter string `json:"not_after"`
|
||||||
|
EntryTimestamp string `json:"entry_timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("q", "%."+target)
|
||||||
|
params.Set("output", "json")
|
||||||
|
apiURL := "https://crt.sh/?" + params.Encode()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; crtsh-scanner/1.0)")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
msg := fmt.Sprintf("API error %d", resp.StatusCode)
|
||||||
|
if resp.StatusCode == http.StatusBadGateway || resp.StatusCode == http.StatusServiceUnavailable || resp.StatusCode == http.StatusGatewayTimeout {
|
||||||
|
msg = fmt.Sprintf("crt.sh is temporarily unavailable (%d), try again later", resp.StatusCode)
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []crtEntry
|
||||||
|
if err := json.Unmarshal(body, &entries); err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate subdomains from name_value fields
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, e := range entries {
|
||||||
|
for _, line := range strings.Split(e.NameValue, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" && !strings.HasPrefix(line, "*") {
|
||||||
|
seen[line] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subdomains := make([]string, 0, len(seen))
|
||||||
|
for s := range seen {
|
||||||
|
subdomains = append(subdomains, s)
|
||||||
|
}
|
||||||
|
sort.Strings(subdomains)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Found %d unique subdomains across %d certificate entries\n\n", len(subdomains), len(entries)))
|
||||||
|
for _, s := range subdomains {
|
||||||
|
sb.WriteString(s + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: len(subdomains)}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
90
back/internal/tools/dig/tool.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package dig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "dig"
|
||||||
|
description = "DNS lookup querying A, AAAA, MX, NS, TXT, and SOA records for a domain, or reverse DNS (PTR) for an IP."
|
||||||
|
link = "https://linux.die.net/man/1/dig"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
var recordTypes = []string{"A", "AAAA", "MX", "NS", "TXT", "SOA"}
|
||||||
|
|
||||||
|
type Runner struct{}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner { return &Runner{} }
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{tools.InputTypeDomain, tools.InputTypeIP}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Available() (bool, string) {
|
||||||
|
if _, err := exec.LookPath("dig"); err != nil {
|
||||||
|
return false, "dig binary not found in PATH"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Dependencies() []string { return []string{"dig"} }
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
totalCount := 0
|
||||||
|
|
||||||
|
if inputType == tools.InputTypeIP {
|
||||||
|
cmd := exec.CommandContext(ctx, "dig", "-x", target, "+noall", "+answer")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil && ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := strings.TrimSpace(string(output))
|
||||||
|
if result != "" {
|
||||||
|
sb.WriteString("=== Reverse DNS (PTR) ===\n")
|
||||||
|
sb.WriteString(result)
|
||||||
|
totalCount += strings.Count(result, "\n") + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, rtype := range recordTypes {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(ctx, "dig", target, rtype, "+noall", "+answer")
|
||||||
|
output, _ := cmd.Output()
|
||||||
|
result := strings.TrimSpace(string(output))
|
||||||
|
if result == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("=== %s ===\n", rtype))
|
||||||
|
sb.WriteString(result)
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
totalCount += strings.Count(result, "\n") + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else if sb.Len() > 0 {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: totalCount}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
91
back/internal/tools/github-recon/tool.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package githubrecon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "github-recon"
|
||||||
|
description = "GitHub OSINT reconnaissance tool. Gathers profile info, social links, organisations, SSH/GPG keys, commits, and more from a GitHub username or email."
|
||||||
|
link = "https://github.com/anotherhadi/nur-osint"
|
||||||
|
icon = "github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Token string `yaml:"token" iky:"desc=GitHub personal access token (enables higher rate limits and more data);required=false"`
|
||||||
|
Deepscan bool `yaml:"deepscan" iky:"desc=Enable deep scan (slower - scans all repositories for authors/emails);default=false"`
|
||||||
|
SpoofEmail bool `yaml:"spoof_email" iky:"desc=Include email spoofing check (email mode only, requires token);default=false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
cfg := Config{}
|
||||||
|
tools.ApplyDefaults(&cfg)
|
||||||
|
return &Runner{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{
|
||||||
|
tools.InputTypeUsername,
|
||||||
|
tools.InputTypeEmail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
|
||||||
|
|
||||||
|
func (r *Runner) ConfigFields() []tools.ConfigField {
|
||||||
|
return tools.ReflectConfigFields(r.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Available() (bool, string) {
|
||||||
|
if _, err := exec.LookPath("github-recon"); err != nil {
|
||||||
|
return false, "github-recon binary not found in PATH"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Dependencies() []string { return []string{"github-recon"} }
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
args := []string{target}
|
||||||
|
if r.cfg.Token != "" {
|
||||||
|
args = append(args, "--token", r.cfg.Token)
|
||||||
|
}
|
||||||
|
if r.cfg.Deepscan {
|
||||||
|
args = append(args, "--deepscan")
|
||||||
|
}
|
||||||
|
if r.cfg.SpoofEmail && inputType == tools.InputTypeEmail {
|
||||||
|
args = append(args, "--spoof-email")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "github-recon", args...)
|
||||||
|
output, err := tools.RunWithPTY(ctx, cmd)
|
||||||
|
|
||||||
|
// Remove banner
|
||||||
|
output = tools.RemoveFirstLines(output, 10)
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
if err != nil && ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else if output != "" {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
|
||||||
|
count = strings.Count(output, "Username:")
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
55
back/internal/tools/gravatar-recon/tool.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package gravatarrecon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "gravatar-recon"
|
||||||
|
description = "Gravatar OSINT tool. Extracts public profile data from a Gravatar account: name, bio, location, employment, social accounts, phone, and more."
|
||||||
|
link = "https://github.com/anotherhadi/gravatar-recon"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Runner struct{}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner { return &Runner{} }
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{tools.InputTypeEmail}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Available() (bool, string) {
|
||||||
|
if _, err := exec.LookPath("gravatar-recon"); err != nil {
|
||||||
|
return false, "gravatar-recon binary not found in PATH"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Dependencies() []string { return []string{"gravatar-recon"} }
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "gravatar-recon", "--silent", target)
|
||||||
|
output, err := tools.RunWithPTY(ctx, cmd)
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
if err != nil && ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else if output != "" {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
133
back/internal/tools/ipinfo/tool.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package ipinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "ipinfo"
|
||||||
|
description = "IP geolocation via ipinfo.io — returns city, region, country, coordinates, ASN/org, timezone, and hostname."
|
||||||
|
link = "https://ipinfo.io"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Token string `yaml:"token" iky:"desc=ipinfo.io API token (optional — free tier allows 50k req/month without one);required=false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
cfg := Config{}
|
||||||
|
tools.ApplyDefaults(&cfg)
|
||||||
|
return &Runner{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{tools.InputTypeIP}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
|
||||||
|
|
||||||
|
func (r *Runner) ConfigFields() []tools.ConfigField {
|
||||||
|
return tools.ReflectConfigFields(r.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipinfoResponse struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Loc string `json:"loc"`
|
||||||
|
Org string `json:"org"`
|
||||||
|
Postal string `json:"postal"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
Bogon bool `json:"bogon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://ipinfo.io/%s/json", target)
|
||||||
|
if r.cfg.Token != "" {
|
||||||
|
url += "?token=" + r.cfg.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var info ipinfoResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Bogon {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: fmt.Sprintf("IP: %s\nType: Bogon/Private address", info.IP)}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 1}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
field := func(label, value string) {
|
||||||
|
if value != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("%-12s %s\n", label+":", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
field("IP", info.IP)
|
||||||
|
field("Hostname", info.Hostname)
|
||||||
|
field("City", info.City)
|
||||||
|
field("Region", info.Region)
|
||||||
|
field("Country", info.Country)
|
||||||
|
field("Coordinates", info.Loc)
|
||||||
|
field("Postal", info.Postal)
|
||||||
|
field("Timezone", info.Timezone)
|
||||||
|
field("Org/ASN", info.Org)
|
||||||
|
|
||||||
|
result := strings.TrimSpace(sb.String())
|
||||||
|
count := 0
|
||||||
|
if result != "" {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: result}
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
178
back/internal/tools/leakcheck/tool.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package leakcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "leakcheck"
|
||||||
|
description = "Data breach lookup via LeakCheck.io — searches 7B+ leaked records for email addresses, usernames, and phone numbers across hundreds of breaches."
|
||||||
|
link = "https://leakcheck.io"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
APIKey string `yaml:"api_key" iky:"desc=LeakCheck API key (required — get one at leakcheck.io);required=true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
cfg := Config{}
|
||||||
|
tools.ApplyDefaults(&cfg)
|
||||||
|
return &Runner{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{
|
||||||
|
tools.InputTypeEmail,
|
||||||
|
tools.InputTypeUsername,
|
||||||
|
tools.InputTypePhone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
|
||||||
|
|
||||||
|
func (r *Runner) ConfigFields() []tools.ConfigField {
|
||||||
|
return tools.ReflectConfigFields(r.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type leakCheckResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Found int `json:"found"`
|
||||||
|
Result []struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Sources []string `json:"sources"`
|
||||||
|
Fields []string `json:"fields"`
|
||||||
|
} `json:"result"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
queryType := "auto"
|
||||||
|
switch inputType {
|
||||||
|
case tools.InputTypeEmail:
|
||||||
|
queryType = "email"
|
||||||
|
case tools.InputTypeUsername:
|
||||||
|
queryType = "login"
|
||||||
|
case tools.InputTypePhone:
|
||||||
|
queryType = "phone"
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://leakcheck.io/api/v2/query/%s?type=%s", target, queryType)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req.Header.Set("X-API-Key", r.cfg.APIKey)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result leakCheckResponse
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Success {
|
||||||
|
msg := result.Error
|
||||||
|
if msg == "" {
|
||||||
|
msg = "API returned failure"
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Found == 0 || len(result.Result) == 0 {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Found in %d breach(es)\n\n", result.Found))
|
||||||
|
|
||||||
|
for _, entry := range result.Result {
|
||||||
|
if len(entry.Sources) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("Sources: %s\n", strings.Join(entry.Sources, ", ")))
|
||||||
|
}
|
||||||
|
if entry.Email != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" Email: %s\n", entry.Email))
|
||||||
|
}
|
||||||
|
if entry.Username != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" Username: %s\n", entry.Username))
|
||||||
|
}
|
||||||
|
if entry.Phone != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" Phone: %s\n", entry.Phone))
|
||||||
|
}
|
||||||
|
if entry.Password != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" Password: %s\n", entry.Password))
|
||||||
|
}
|
||||||
|
if entry.Hash != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" Hash: %s\n", entry.Hash))
|
||||||
|
}
|
||||||
|
if len(entry.Fields) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf(" Fields: %s\n", strings.Join(entry.Fields, ", ")))
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: result.Found}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
89
back/internal/tools/maigret/tool.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package maigret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "maigret"
|
||||||
|
description = "Username OSINT across 3000+ sites. Searches social networks, forums, and online platforms for an account matching the target username."
|
||||||
|
link = "https://github.com/soxoj/maigret"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
var accountsRe = regexp.MustCompile(`returned (\d+) accounts`)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AllSites bool `yaml:"all_sites" iky:"desc=Scan all sites in the database instead of just the top 500 (slower);default=false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
cfg := Config{}
|
||||||
|
tools.ApplyDefaults(&cfg)
|
||||||
|
return &Runner{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{tools.InputTypeUsername}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
|
||||||
|
|
||||||
|
func (r *Runner) ConfigFields() []tools.ConfigField {
|
||||||
|
return tools.ReflectConfigFields(r.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Available() (bool, string) {
|
||||||
|
if _, err := exec.LookPath("maigret"); err != nil {
|
||||||
|
return false, "maigret binary not found in PATH"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Dependencies() []string { return []string{"maigret"} }
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
args := []string{"--no-progressbar", target}
|
||||||
|
if r.cfg.AllSites {
|
||||||
|
args = append(args, "-a")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "maigret", args...)
|
||||||
|
output, err := tools.RunWithPTY(ctx, cmd)
|
||||||
|
|
||||||
|
// Crop at Python traceback (NixOS read-only store error — results are unaffected)
|
||||||
|
if idx := strings.Index(output, "Traceback (most recent call last)"); idx != -1 {
|
||||||
|
output = strings.TrimSpace(output[:idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
if err != nil && ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else if output != "" {
|
||||||
|
// Parse count from summary line: "returned N accounts"
|
||||||
|
if m := accountsRe.FindStringSubmatch(output); len(m) == 2 {
|
||||||
|
count, _ = strconv.Atoi(m[1])
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
back/internal/tools/ptyrun.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// oscRe strips OSC terminal sequences emitted by the PTY (e.g. colour queries).
|
||||||
|
var oscRe = regexp.MustCompile(`\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)`)
|
||||||
|
|
||||||
|
// RunWithPTY runs cmd under a pseudo-terminal (preserving ANSI colours) and
|
||||||
|
// returns the full output once the process exits.
|
||||||
|
func RunWithPTY(ctx context.Context, cmd *exec.Cmd) (string, error) {
|
||||||
|
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 50, Cols: 220})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() { _ = ptmx.Close() }()
|
||||||
|
|
||||||
|
output, _ := io.ReadAll(ptmx)
|
||||||
|
_ = cmd.Wait()
|
||||||
|
|
||||||
|
return oscRe.ReplaceAllString(string(output), ""), ctx.Err()
|
||||||
|
}
|
||||||
72
back/internal/tools/tools.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeOutput EventType = "output" // raw ANSI text, payload is a plain string
|
||||||
|
EventTypeError EventType = "error"
|
||||||
|
EventTypeCount EventType = "count" // payload is int, additive — emit once or multiple times from Run
|
||||||
|
EventTypeDone EventType = "done"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InputType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
InputTypeEmail InputType = "email"
|
||||||
|
InputTypeUsername InputType = "username"
|
||||||
|
InputTypePhone InputType = "phone"
|
||||||
|
InputTypeIP InputType = "ip"
|
||||||
|
InputTypeDomain InputType = "domain"
|
||||||
|
InputTypePassword InputType = "password"
|
||||||
|
InputTypeName InputType = "name"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Tool string `json:"tool"`
|
||||||
|
Type EventType `json:"type"`
|
||||||
|
Payload interface{} `json:"payload,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolRunner is the core interface every tool must implement.
|
||||||
|
type ToolRunner interface {
|
||||||
|
Name() string
|
||||||
|
Description() string
|
||||||
|
Link() string // URL to source or documentation
|
||||||
|
Icon() string // Simple Icons slug, empty if none
|
||||||
|
|
||||||
|
InputTypes() []InputType
|
||||||
|
|
||||||
|
// Run executes the tool and sends Events to out. Must close out when done.
|
||||||
|
// inputType indicates what kind of value target is (email, username, ...).
|
||||||
|
Run(ctx context.Context, target string, inputType InputType, out chan<- Event) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Configurable interface {
|
||||||
|
ConfigPtr() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigField struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"` // "string", "bool", "int", "float", "enum"
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Default any `json:"default"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Value any `json:"value"`
|
||||||
|
Options []string `json:"options,omitempty"` // non-empty when Type == "enum"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigDescriber interface {
|
||||||
|
ConfigFields() []ConfigField
|
||||||
|
}
|
||||||
|
|
||||||
|
// AvailabilityChecker is implemented by tools that require an external binary.
|
||||||
|
type AvailabilityChecker interface {
|
||||||
|
Available() (ok bool, reason string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DependencyLister interface {
|
||||||
|
Dependencies() []string
|
||||||
|
}
|
||||||
|
|
||||||
95
back/internal/tools/user-scanner/tool.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package userscanner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "user-scanner"
|
||||||
|
description = "🕵️♂️ (2-in-1) Email & Username OSINT suite. Analyzes 195+ scan vectors (95+ email / 100+ username) for security research, investigations, and digital footprinting."
|
||||||
|
link = "https://github.com/kaifcodec/user-scanner"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AllowLoud bool `yaml:"allow_loud" iky:"desc=Enable scanning sites that may send emails/notifications (password resets, etc.);default=false"`
|
||||||
|
OnlyFound bool `yaml:"only_found" iky:"desc=Only show sites where the username/email was found;default=true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
cfg := Config{}
|
||||||
|
tools.ApplyDefaults(&cfg)
|
||||||
|
return &Runner{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{
|
||||||
|
tools.InputTypeEmail,
|
||||||
|
tools.InputTypeUsername,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
|
||||||
|
|
||||||
|
func (r *Runner) ConfigFields() []tools.ConfigField {
|
||||||
|
return tools.ReflectConfigFields(r.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Available() (bool, string) {
|
||||||
|
if _, err := exec.LookPath("user-scanner"); err != nil {
|
||||||
|
return false, "user-scanner binary not found in PATH"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Dependencies() []string { return []string{"user-scanner"} }
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
args := make([]string, 0, 6)
|
||||||
|
switch inputType {
|
||||||
|
case tools.InputTypeEmail:
|
||||||
|
args = append(args, "-e", target)
|
||||||
|
default:
|
||||||
|
args = append(args, "-u", target)
|
||||||
|
}
|
||||||
|
if r.cfg.AllowLoud {
|
||||||
|
args = append(args, "--allow-loud")
|
||||||
|
}
|
||||||
|
if r.cfg.OnlyFound {
|
||||||
|
args = append(args, "--only-found")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "user-scanner", args...)
|
||||||
|
output, err := tools.RunWithPTY(ctx, cmd)
|
||||||
|
|
||||||
|
// Removing banner
|
||||||
|
output = tools.RemoveFirstLines(output, 8)
|
||||||
|
// count =
|
||||||
|
output = tools.CropAfterExclude(output, "[i] Scan complete.")
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
if err != nil && ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else if output != "" {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
|
||||||
|
count = strings.Count(output, "[✔]")
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
61
back/internal/tools/utils.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// RemoveFirstLines removes the first n lines. Returns "" if n >= total lines.
|
||||||
|
func RemoveFirstLines(input string, n int) string {
|
||||||
|
lines := strings.Split(input, "\n")
|
||||||
|
if n >= len(lines) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(lines[n:], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveLastLines removes the last n lines. Returns "" if n >= total lines.
|
||||||
|
func RemoveLastLines(input string, n int) string {
|
||||||
|
lines := strings.Split(input, "\n")
|
||||||
|
if n >= len(lines) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(lines[:len(lines)-n], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CropBefore removes everything before the first occurrence of y (inclusive of y).
|
||||||
|
// Returns input unchanged if y is not found.
|
||||||
|
func CropBefore(input string, y string) string {
|
||||||
|
idx := strings.Index(input, y)
|
||||||
|
if idx == -1 {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return input[idx:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CropAfter removes everything after the last occurrence of y (inclusive of y).
|
||||||
|
// Returns input unchanged if y is not found.
|
||||||
|
func CropAfter(input string, y string) string {
|
||||||
|
idx := strings.LastIndex(input, y)
|
||||||
|
if idx == -1 {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return input[:idx+len(y)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CropBeforeExclude removes everything before and including the first occurrence of y.
|
||||||
|
// Returns input unchanged if y is not found.
|
||||||
|
func CropBeforeExclude(input string, y string) string {
|
||||||
|
idx := strings.Index(input, y)
|
||||||
|
if idx == -1 {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return input[idx+len(y):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CropAfterExclude removes everything from the last occurrence of y onwards.
|
||||||
|
// Returns input unchanged if y is not found.
|
||||||
|
func CropAfterExclude(input string, y string) string {
|
||||||
|
idx := strings.LastIndex(input, y)
|
||||||
|
if idx == -1 {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return input[:idx]
|
||||||
|
}
|
||||||
126
back/internal/tools/wappalyzer/tool.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package wappalyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
wappalyzergo "github.com/projectdiscovery/wappalyzergo"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "wappalyzer"
|
||||||
|
description = "Web technology fingerprinting via wappalyzergo — detects CMS, frameworks, web servers, analytics, CDN, and 1500+ other technologies running on a domain."
|
||||||
|
link = "https://github.com/projectdiscovery/wappalyzergo"
|
||||||
|
icon = "wappalyzer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
wappalyze *wappalyzergo.Wappalyze
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
w, _ := wappalyzergo.New()
|
||||||
|
return &Runner{wappalyze: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{tools.InputTypeDomain}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
// Try HTTPS first, fall back to HTTP
|
||||||
|
var (
|
||||||
|
resp *http.Response
|
||||||
|
body []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for _, scheme := range []string{"https", "http"} {
|
||||||
|
targetURL := fmt.Sprintf("%s://%s", scheme, target)
|
||||||
|
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
|
||||||
|
if reqErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)")
|
||||||
|
|
||||||
|
resp, err = http.DefaultClient.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil || resp == nil {
|
||||||
|
msg := "failed to connect to target"
|
||||||
|
if err != nil {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprints := r.wappalyze.FingerprintWithInfo(resp.Header, body)
|
||||||
|
if len(fingerprints) == 0 {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by category
|
||||||
|
byCategory := make(map[string][]string)
|
||||||
|
for tech, info := range fingerprints {
|
||||||
|
cats := info.Categories
|
||||||
|
if len(cats) == 0 {
|
||||||
|
cats = []string{"Other"}
|
||||||
|
}
|
||||||
|
for _, cat := range cats {
|
||||||
|
byCategory[cat] = append(byCategory[cat], tech)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cats := make([]string, 0, len(byCategory))
|
||||||
|
for c := range byCategory {
|
||||||
|
cats = append(cats, c)
|
||||||
|
}
|
||||||
|
sort.Strings(cats)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Detected %d technologies\n\n", len(fingerprints)))
|
||||||
|
for _, cat := range cats {
|
||||||
|
techs := byCategory[cat]
|
||||||
|
sort.Strings(techs)
|
||||||
|
sb.WriteString(fmt.Sprintf("%s:\n", cat))
|
||||||
|
for _, t := range techs {
|
||||||
|
sb.WriteString(fmt.Sprintf(" - %s\n", t))
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: len(fingerprints)}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
62
back/internal/tools/whois/tool.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package whois
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "whois"
|
||||||
|
description = "WHOIS lookup for domain registration and IP ownership information."
|
||||||
|
link = "https://en.wikipedia.org/wiki/WHOIS"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Runner struct{}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner { return &Runner{} }
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{tools.InputTypeDomain, tools.InputTypeIP}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Available() (bool, string) {
|
||||||
|
if _, err := exec.LookPath("whois"); err != nil {
|
||||||
|
return false, "whois binary not found in PATH"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Dependencies() []string { return []string{"whois"} }
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "whois", target)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
|
||||||
|
if err != nil && ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := strings.TrimSpace(string(output))
|
||||||
|
count := 0
|
||||||
|
if result != "" {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: result}
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
232
back/internal/tools/whoisfreaks/tool.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package whoisfreaks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "whoisfreaks"
|
||||||
|
description = "Reverse WHOIS lookup via WhoisFreaks — find all domains registered by an email, owner name, or keyword across 3.6B+ WHOIS records."
|
||||||
|
link = "https://whoisfreaks.com"
|
||||||
|
icon = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
APIKey string `yaml:"api_key" iky:"desc=WhoisFreaks API key (required — free account at whoisfreaks.com);required=true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() tools.ToolRunner {
|
||||||
|
cfg := Config{}
|
||||||
|
tools.ApplyDefaults(&cfg)
|
||||||
|
return &Runner{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Name() string { return name }
|
||||||
|
func (r *Runner) Description() string { return description }
|
||||||
|
func (r *Runner) Link() string { return link }
|
||||||
|
func (r *Runner) Icon() string { return icon }
|
||||||
|
|
||||||
|
func (r *Runner) InputTypes() []tools.InputType {
|
||||||
|
return []tools.InputType{
|
||||||
|
tools.InputTypeEmail,
|
||||||
|
tools.InputTypeName,
|
||||||
|
tools.InputTypeDomain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) ConfigPtr() interface{} { return &r.cfg }
|
||||||
|
|
||||||
|
func (r *Runner) ConfigFields() []tools.ConfigField {
|
||||||
|
return tools.ReflectConfigFields(r.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var skipKeys = map[string]bool{
|
||||||
|
"num": true, "status": true, "query_time": true, "update_date": true,
|
||||||
|
"iana_id": true, "whois_server": true, "handle": true,
|
||||||
|
"zip_code": true, "country_code": true, "mailing_address": true,
|
||||||
|
"phone_number": true, "administrative_contact": true, "technical_contact": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyResult(r gjson.Result, depth int) string {
|
||||||
|
indent := strings.Repeat(" ", depth)
|
||||||
|
var sb strings.Builder
|
||||||
|
r.ForEach(func(key, val gjson.Result) bool {
|
||||||
|
k := key.String()
|
||||||
|
if skipKeys[k] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch val.Type {
|
||||||
|
case gjson.JSON:
|
||||||
|
if val.IsArray() {
|
||||||
|
arr := val.Array()
|
||||||
|
if len(arr) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%s%s:\n", indent, k))
|
||||||
|
for _, item := range arr {
|
||||||
|
if item.Type == gjson.JSON {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s -\n", indent))
|
||||||
|
sb.WriteString(prettyResult(item, depth+2))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s - %s\n", indent, item.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s%s:\n", indent, k))
|
||||||
|
sb.WriteString(prettyResult(val, depth+1))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
v := val.String()
|
||||||
|
if v == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, k, v))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRequest(ctx context.Context, req *http.Request) ([]byte, *http.Response, error) {
|
||||||
|
for {
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusTooManyRequests {
|
||||||
|
return body, resp, nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, resp, ctx.Err()
|
||||||
|
case <-time.After(60 * time.Second):
|
||||||
|
}
|
||||||
|
// Rebuild the request since the body was consumed
|
||||||
|
req2, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
req2.Header = req.Header
|
||||||
|
req = req2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("whois", "reverse")
|
||||||
|
params.Set("apiKey", r.cfg.APIKey)
|
||||||
|
|
||||||
|
switch inputType {
|
||||||
|
case tools.InputTypeEmail:
|
||||||
|
params.Set("email", target)
|
||||||
|
case tools.InputTypeName:
|
||||||
|
params.Set("owner", target)
|
||||||
|
case tools.InputTypeDomain:
|
||||||
|
params.Set("keyword", target)
|
||||||
|
default:
|
||||||
|
params.Set("keyword", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://api.whoisfreaks.com/v1.0/whois?"+params.Encode(),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
body, resp, err := doRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||||
|
} else {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()}
|
||||||
|
}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
j := gjson.ParseBytes(body)
|
||||||
|
|
||||||
|
if !j.Get("whois_domains_historical").Exists() {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "unexpected response format"}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
domains := j.Get("whois_domains_historical").Array()
|
||||||
|
if len(domains) == 0 {
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
total := j.Get("total_Result").Int()
|
||||||
|
totalPages := j.Get("total_Pages").Int()
|
||||||
|
currentPage := j.Get("current_Page").Int()
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Found %d domain(s)", total))
|
||||||
|
if totalPages > 1 {
|
||||||
|
sb.WriteString(fmt.Sprintf(" across %d pages (showing page %d)", totalPages, currentPage))
|
||||||
|
}
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
|
||||||
|
for _, d := range domains {
|
||||||
|
sb.WriteString(prettyResult(d, 0))
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: int(total)}
|
||||||
|
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
157
flake.lock
generated
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"bun2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"import-tree": "import-tree",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"systems": "systems",
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770895533,
|
||||||
|
"narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "bun2nix",
|
||||||
|
"rev": "c843f477b15f51151f8c6bcc886954699440a6e1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "bun2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1769996383,
|
||||||
|
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"import-tree": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763762820,
|
||||||
|
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
|
||||||
|
"owner": "vic",
|
||||||
|
"repo": "import-tree",
|
||||||
|
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "vic",
|
||||||
|
"repo": "import-tree",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1775036866,
|
||||||
|
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1769909678,
|
||||||
|
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "72716169fe93074c333e8d0173151350670b824c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nur-osint": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774035694,
|
||||||
|
"narHash": "sha256-PtORnAJ/SKeOwrPAjZ0LR00Pu8aDIzXO8H8v9CoM7zk=",
|
||||||
|
"owner": "anotherhadi",
|
||||||
|
"repo": "nur-osint",
|
||||||
|
"rev": "813351d47721d411441bb6221faf2c6163846946",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "anotherhadi",
|
||||||
|
"repo": "nur-osint",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"bun2nix": "bun2nix",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"nur-osint": "nur-osint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"bun2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770228511,
|
||||||
|
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
74
flake.nix
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
description = "iknowyou: self-hosted OSINT aggregation platform";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
bun2nix = {
|
||||||
|
url = "github:nix-community/bun2nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
nur-osint = {
|
||||||
|
url = "github:anotherhadi/nur-osint";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
bun2nix,
|
||||||
|
nur-osint,
|
||||||
|
}: let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.permittedInsecurePackages = ["python3.13-pypdf2-3.0.1"];
|
||||||
|
};
|
||||||
|
|
||||||
|
backendPkg = import ./nix/backend.nix {inherit pkgs;};
|
||||||
|
frontendPkg = import ./nix/frontend.nix {inherit pkgs bun2nix system;};
|
||||||
|
|
||||||
|
osintTools = with pkgs; [
|
||||||
|
whois
|
||||||
|
dnsutils
|
||||||
|
maigret
|
||||||
|
nur-osint.packages.${system}.user-scanner
|
||||||
|
nur-osint.packages.${system}.github-recon
|
||||||
|
];
|
||||||
|
|
||||||
|
ikyPkg = pkgs.symlinkJoin {
|
||||||
|
name = "iky";
|
||||||
|
paths = [backendPkg] ++ osintTools;
|
||||||
|
nativeBuildInputs = [pkgs.makeWrapper];
|
||||||
|
postBuild = ''
|
||||||
|
mkdir -p $out/share/iky
|
||||||
|
cp -r ${frontendPkg} $out/share/iky/frontend
|
||||||
|
wrapProgram $out/bin/server \
|
||||||
|
--set-default IKY_FRONT_DIR $out/share/iky/frontend
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
nixosModules.default = import ./nix/module.nix;
|
||||||
|
|
||||||
|
packages.${system} = {
|
||||||
|
backend = backendPkg;
|
||||||
|
frontend = frontendPkg;
|
||||||
|
default = ikyPkg;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs;
|
||||||
|
[
|
||||||
|
bun2nix.packages.${system}.default
|
||||||
|
bun
|
||||||
|
go
|
||||||
|
air
|
||||||
|
just
|
||||||
|
]
|
||||||
|
++ osintTools;
|
||||||
|
IKY_CONFIG = "./config.yaml";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
27
front/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
result/
|
||||||
|
|
||||||
|
# code editors
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
52
front/astro.config.mjs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import svelte from "@astrojs/svelte";
|
||||||
|
import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
EventEmitter.defaultMaxListeners = 25;
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: "https://iky.hadi.icu",
|
||||||
|
output: "static",
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
noExternal: ["@lucide/svelte"],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
{
|
||||||
|
name: "shell-rewrite",
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use((req, _res, next) => {
|
||||||
|
if (/^\/tools\/[^/]+\/?(\?.*)?$/.test(req.url)) req.url = "/tools/_";
|
||||||
|
if (/^\/search\/[^/]+\/?(\?.*)?$/.test(req.url)) req.url = "/search/_";
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://localhost:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
integrations: [svelte()],
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [remarkGithubBlockquoteAlert],
|
||||||
|
shikiConfig: {
|
||||||
|
theme: "github-dark",
|
||||||
|
transformers: [
|
||||||
|
{
|
||||||
|
name: "code-block-meta",
|
||||||
|
pre(node) {
|
||||||
|
node.properties["data-lang"] = this.options.lang || "text";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
877
front/bun.lock
Normal file
@@ -0,0 +1,877 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "iknowyou",
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/svelte": "8.0.4",
|
||||||
|
"@lucide/svelte": "^1.7.0",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"ansi_up": "^6.0.6",
|
||||||
|
"astro": "6.1.2",
|
||||||
|
"dompurify": "^3.3.3",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"remark-github-blockquote-alert": "^2.1.0",
|
||||||
|
"svelte": "^5.53.12",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^25.5.0",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"daisyui": "^5.5.19",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@astrojs/compiler": ["@astrojs/compiler@3.0.1", "", {}, "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA=="],
|
||||||
|
|
||||||
|
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.8.0", "", { "dependencies": { "picomatch": "^4.0.3" } }, "sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w=="],
|
||||||
|
|
||||||
|
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/prism": "4.0.1", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ=="],
|
||||||
|
|
||||||
|
"@astrojs/prism": ["@astrojs/prism@4.0.1", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ=="],
|
||||||
|
|
||||||
|
"@astrojs/svelte": ["@astrojs/svelte@8.0.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", "svelte2tsx": "^0.7.52", "vite": "^7.3.1" }, "peerDependencies": { "astro": "^6.0.0", "svelte": "^5.43.6", "typescript": "^5.3.3" } }, "sha512-c5m3chjtgxBE3BzsE/bZbCFBkLPhq041rm2WJFaTIKGwt/3xNm/5efYCj23reuAcBsl4iYS8n2UwkAHQJzhkZA=="],
|
||||||
|
|
||||||
|
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
|
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
|
||||||
|
|
||||||
|
"@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="],
|
||||||
|
|
||||||
|
"@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
|
||||||
|
|
||||||
|
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||||
|
|
||||||
|
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
|
"@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.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@lucide/svelte": ["@lucide/svelte@1.7.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-YytBKOUBGox7yWcykZnYxOkn5WpR5G1qYXLYXV/j1B79SOTTEKzB+s5yF5Rq9l9OkweDStNH2b4yTqfvhEhV8g=="],
|
||||||
|
|
||||||
|
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||||
|
|
||||||
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
|
||||||
|
|
||||||
|
"@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="],
|
||||||
|
|
||||||
|
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="],
|
||||||
|
|
||||||
|
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="],
|
||||||
|
|
||||||
|
"@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="],
|
||||||
|
|
||||||
|
"@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="],
|
||||||
|
|
||||||
|
"@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="],
|
||||||
|
|
||||||
|
"@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
|
||||||
|
|
||||||
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
|
||||||
|
|
||||||
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
|
"@types/dompurify": ["@types/dompurify@3.2.0", "", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
||||||
|
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||||
|
|
||||||
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
|
|
||||||
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
|
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||||
|
|
||||||
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"ansi_up": ["ansi_up@6.0.6", "", {}, "sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA=="],
|
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
|
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
|
||||||
|
|
||||||
|
"astro": ["astro@6.1.2", "", { "dependencies": { "@astrojs/compiler": "^3.0.1", "@astrojs/internal-helpers": "0.8.0", "@astrojs/markdown-remark": "7.1.0", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^7.3.1", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "bin/astro.mjs" } }, "sha512-r3iIvmB6JvQxsdJLvapybKKq7Bojd1iQK6CCx5P55eRnXJIyUpHx/1UB/GdMm+em/lwaCUasxHCmIO0lCLV2uA=="],
|
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
|
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||||
|
|
||||||
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||||
|
|
||||||
|
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||||
|
|
||||||
|
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||||
|
|
||||||
|
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||||
|
|
||||||
|
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||||
|
|
||||||
|
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
|
||||||
|
|
||||||
|
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||||
|
|
||||||
|
"cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
|
||||||
|
|
||||||
|
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||||
|
|
||||||
|
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||||
|
|
||||||
|
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||||
|
|
||||||
|
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
|
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||||
|
|
||||||
|
"daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||||
|
|
||||||
|
"dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.6", "", {}, "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
|
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="],
|
||||||
|
|
||||||
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
|
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
||||||
|
|
||||||
|
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||||
|
|
||||||
|
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||||
|
|
||||||
|
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||||
|
|
||||||
|
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||||
|
|
||||||
|
"dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="],
|
||||||
|
|
||||||
|
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||||
|
|
||||||
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||||
|
|
||||||
|
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||||
|
|
||||||
|
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
|
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||||
|
|
||||||
|
"esrap": ["esrap@2.2.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@typescript-eslint/types": "^8.2.0" } }, "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||||
|
|
||||||
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
|
"fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="],
|
||||||
|
|
||||||
|
"fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="],
|
||||||
|
|
||||||
|
"fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
||||||
|
|
||||||
|
"fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="],
|
||||||
|
|
||||||
|
"fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
||||||
|
|
||||||
|
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||||
|
|
||||||
|
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
|
||||||
|
|
||||||
|
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||||
|
|
||||||
|
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
||||||
|
|
||||||
|
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||||
|
|
||||||
|
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
||||||
|
|
||||||
|
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
|
||||||
|
|
||||||
|
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||||
|
|
||||||
|
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||||
|
|
||||||
|
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
|
||||||
|
|
||||||
|
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||||
|
|
||||||
|
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||||
|
|
||||||
|
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||||
|
|
||||||
|
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||||
|
|
||||||
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
|
|
||||||
|
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
|
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||||
|
|
||||||
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
|
||||||
|
|
||||||
|
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||||
|
|
||||||
|
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
|
||||||
|
|
||||||
|
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||||
|
|
||||||
|
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||||
|
|
||||||
|
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||||
|
|
||||||
|
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||||
|
|
||||||
|
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||||
|
|
||||||
|
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||||
|
|
||||||
|
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||||
|
|
||||||
|
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||||
|
|
||||||
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||||
|
|
||||||
|
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||||
|
|
||||||
|
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||||
|
|
||||||
|
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||||
|
|
||||||
|
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||||
|
|
||||||
|
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||||
|
|
||||||
|
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||||
|
|
||||||
|
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||||
|
|
||||||
|
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||||
|
|
||||||
|
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||||
|
|
||||||
|
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||||
|
|
||||||
|
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||||
|
|
||||||
|
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||||
|
|
||||||
|
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||||
|
|
||||||
|
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||||
|
|
||||||
|
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||||
|
|
||||||
|
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||||
|
|
||||||
|
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||||
|
|
||||||
|
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||||
|
|
||||||
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
|
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
||||||
|
|
||||||
|
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||||
|
|
||||||
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
|
"node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
|
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||||
|
|
||||||
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
|
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||||
|
|
||||||
|
"oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="],
|
||||||
|
|
||||||
|
"p-queue": ["p-queue@9.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-yQS1vV2V7Q14MQrgD8jMNY5owPuGgVHVdSK8NqmKpOVajnjbaeMa6uLOzTALPtvJ7Vo4bw0BGsw7qfUT8z24Ig=="],
|
||||||
|
|
||||||
|
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
|
||||||
|
|
||||||
|
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||||
|
|
||||||
|
"parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
|
||||||
|
|
||||||
|
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||||
|
|
||||||
|
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
|
|
||||||
|
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
||||||
|
|
||||||
|
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||||
|
|
||||||
|
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||||
|
|
||||||
|
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||||
|
|
||||||
|
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||||
|
|
||||||
|
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||||
|
|
||||||
|
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||||
|
|
||||||
|
"rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
|
||||||
|
|
||||||
|
"rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
|
||||||
|
|
||||||
|
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
||||||
|
|
||||||
|
"rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
|
||||||
|
|
||||||
|
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||||
|
|
||||||
|
"remark-github-blockquote-alert": ["remark-github-blockquote-alert@2.1.0", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-J392jmIP684d7iGsENN0uguL10IGbRdc8bTUSrd/jOLzdWkwg721Fj3JPQGN8tF6fTIrE5HHOIA3nBuwuaeuPQ=="],
|
||||||
|
|
||||||
|
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||||
|
|
||||||
|
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||||
|
|
||||||
|
"remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="],
|
||||||
|
|
||||||
|
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
|
||||||
|
|
||||||
|
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
|
||||||
|
|
||||||
|
"retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
|
||||||
|
|
||||||
|
"retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
||||||
|
|
||||||
|
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||||
|
|
||||||
|
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||||
|
|
||||||
|
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
|
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||||
|
|
||||||
|
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||||
|
|
||||||
|
"shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="],
|
||||||
|
|
||||||
|
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||||
|
|
||||||
|
"smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||||
|
|
||||||
|
"svelte": ["svelte@5.55.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw=="],
|
||||||
|
|
||||||
|
"svelte2tsx": ["svelte2tsx@0.7.53", "", { "dependencies": { "dedent-js": "^1.0.1", "scule": "^1.3.0" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" } }, "sha512-ljVSwmnYRDHRm8+7ICP6QoAN7U7vgOFfPBLN6T745YWNYqRRSzHxlrzUVqMjYls2Un8MzJissfziy/38e6Deeg=="],
|
||||||
|
|
||||||
|
"svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
||||||
|
|
||||||
|
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||||
|
|
||||||
|
"tinyclip": ["tinyclip@0.1.12", "", {}, "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||||
|
|
||||||
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
|
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||||
|
|
||||||
|
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
|
||||||
|
|
||||||
|
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||||
|
|
||||||
|
"unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="],
|
||||||
|
|
||||||
|
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
||||||
|
|
||||||
|
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||||
|
|
||||||
|
"unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="],
|
||||||
|
|
||||||
|
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||||
|
|
||||||
|
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
|
||||||
|
|
||||||
|
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||||
|
|
||||||
|
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||||
|
|
||||||
|
"unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="],
|
||||||
|
|
||||||
|
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||||
|
|
||||||
|
"unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
|
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||||
|
|
||||||
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "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-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
|
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||||
|
|
||||||
|
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||||
|
|
||||||
|
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||||
|
|
||||||
|
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||||
|
|
||||||
|
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
|
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||||
|
|
||||||
|
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
||||||
|
"svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||||
|
|
||||||
|
"yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
|
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
1700
front/bun.nix
Normal file
34
front/package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "iknowyou",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev:frontend": "astro dev",
|
||||||
|
"dev:backend": "cd ../back/ && air",
|
||||||
|
"dev": "concurrently \"bun dev:frontend\" \"bun dev:backend\"",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/svelte": "8.0.4",
|
||||||
|
"@lucide/svelte": "^1.7.0",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"ansi_up": "^6.0.6",
|
||||||
|
"astro": "6.1.2",
|
||||||
|
"dompurify": "^3.3.3",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"remark-github-blockquote-alert": "^2.1.0",
|
||||||
|
"svelte": "^5.53.12",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^25.5.0",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"daisyui": "^5.5.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
front/public/.well-known/security.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Contact: mailto:anotherhadi.clapped234[at]passmail.net
|
||||||
|
Expires: 2028-12-31T23:00:00.000Z
|
||||||
|
Encryption: /anotherhadi.asc
|
||||||
|
Preferred-Languages: en, fr
|
||||||
|
Canonical: /.well-known/security.txt
|
||||||
1
front/public/Wrench.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wrench-icon lucide-wrench"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/></svg>
|
||||||
|
After Width: | Height: | Size: 441 B |
52
front/public/anotherhadi.asc
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGlUVOcBEADC8BaIMD71bTsFTOEI5pSJTiKrMQdgYkkDiK8yBdstSLInBeTV
|
||||||
|
Xsxlgd9s9Nw9sNkbIytUB3rSbVwlYbH+o6A5qaQQkOBq/3/RR+zdPB5lonpvBPYs
|
||||||
|
agcjRLc2Z0W/83ERMuiOfrJHsOjwopL72PaG1KzuXEDI2o5vUFIvt4iER+ZGXAPU
|
||||||
|
GKh7YvTWy1qPkHYeHWN3khE4hKffx+ozxQWFQEr90DrJUwDPSMwkCxkzk3R68qSG
|
||||||
|
x6dEp21XChSJmvN+SQjhMRRCcE6PIQgWBzitEHZhWdpBfbfYlqP2Fc1d56kqSYXP
|
||||||
|
5bwUXjbJ/1RgmULWjgyAlbkYkfkw2fqvgpwz4GPFZV16Qa2yh6iuswoSp4uHP1Pp
|
||||||
|
9v8TTI2xXX8FuhhdVbSu1xAbgcD7GAaTZJz8qCqU7oZieHMLQVAdEio0dDq7pO9s
|
||||||
|
bUND1Syd4tOxpiKedwOs4YFOU5tc59Ik7amb7PUZr/OR6JmFWBHuhnw763zl046G
|
||||||
|
3NoLCb3lsKq6wxBNGPoEhlDGSe7ayIY2KdawzH62ymK53FHI2d+kpuNIGLas6+JL
|
||||||
|
RqzpG5DVK4JaozoYTnckS0dK/y28gANUaZ8dFB/gcEnHHP8rHq/I4tAcPiGc+I87
|
||||||
|
o25cBwJctOA4ucD9/G8rBjlt7wVkZjFZNKDmXhHIe9Tw5+Jv5HPA7OpUYwARAQAB
|
||||||
|
tCpIYWRpIDxhbm90aGVyaGFkaS5jbGFwcGVkMjM0QHBhc3NtYWlsLm5ldD6JAk4E
|
||||||
|
EwEKADgWIQScdA7pGcNUcalhd2qk3jJylKG9VQUCaVRU5wIbAwULCQgHAgYVCgkI
|
||||||
|
CwIEFgIDAQIeAQIXgAAKCRCk3jJylKG9VTYSD/9jZxiU9SBIpxNbKtEQo2M8/QBk
|
||||||
|
4xE5m/6gYE8fx1DndEsRK3eONIXNiRAq5fSadfF0EG8tL4RoIZksSx2usu84VH3y
|
||||||
|
y3Tn/mJnK25v6DNQWZmREhvgOw0gP4py7of0fBi+T0FYFGqxdPEYwTkqZ4Hb/phz
|
||||||
|
QQHdwa8nYd0+KHMujQwwMC9m+v5qCRv4F4sMAPfs9jZpxMD/gpsV0Cnrg1Vq+JDm
|
||||||
|
l2upEioKLbq8cPPCJyT85wACu6zb/OiObu2Fs4IKvC97kcaKUyfjGHGN15384bgM
|
||||||
|
HOihrKVbNc6SK44kOp1fdUz4zz3gXT7Qq2EJHrt33VxLT/QnhihD7MZaDqlpdrjh
|
||||||
|
w0y3tMc4dhqmDSSoDx3GANn23F5NmFtXsx1Ndhqhv5pPDLbYVAKfKCzJHrBWPUkP
|
||||||
|
n8ZS4bs9XN+Nv7lfXKxSkPg9CQARYFmrHQyL2IHTeBIOUcznji04p7AG8YtGOJ7G
|
||||||
|
ZZS3KZ4kxSKTi/QleTa9ZldRecvT4cYk1juwmoIO5pIwPOv+KsUPSn3oeLLYup/A
|
||||||
|
y8lg3zxc9jFk3tyrHgIVM7sM9VqGxOctCORNzRma8O7TbbkiPGXLZc8KaYLVo0QT
|
||||||
|
9gmwuaDpJ6hT26EVvVCj1dlOJBXqsrsaTe4ev/jllJjOMOwtc3EcCMhWMD5fTEkE
|
||||||
|
oZFDZOY3YBnbHEJaXLkCDQRpVFTnARAAn9YLGQkFiVeovLCbyHt62aVOFOp7j2AS
|
||||||
|
Jj/vg+P96D71Wn8NomNLjKokEkYnMsgKb9D+jSWRFiUmgHdQYDQuouz4w8bH6Bwz
|
||||||
|
owJL489QyQLml9uFL4I7ZwLzQSnxxVMGYe+1XKJNXPTlhwroKNCMe5wfuzMRZvGc
|
||||||
|
CzfLmkvNDXY36GVfHIRkh31R/6ILIa+s5HAmjz1560RcaJ9qxQDFdJGUdbbaP2Ag
|
||||||
|
lCYZXHTFwK8X8cFi+qpW31vY3nzSzcNc+DZdPa7Mkhsa3dHt7gLu1GLjeoyUThmu
|
||||||
|
Q/+orpqagj04i8T1pjucAmhbnw8N9UwpLAUwUhhMTsPik1EvQiKJq2CgHddtuPDx
|
||||||
|
ekJzBU6/fZQu8czaIezjqJ7yD0PYqSnOqtZ+MNoWE8HVUyXZPdVC5Fpnm5Lix/Tr
|
||||||
|
ie9ylu3V5sSVQ5FIs+cZ1vZnOTShjw26rE2FfatVJeHpC5ioV3z6lzaCi4/zkSWi
|
||||||
|
fCKhi9cOROeV+oslV7esnFGXDWLtd22H54NW8Kc140TpPq8457wbvS5r1BMMHite
|
||||||
|
OGXcMSsKiivB+nO4acAPHpl093/qYcFMpP+2GP24ogJuL58UkfiboZz7b3gLPGZJ
|
||||||
|
rgMmdlG5p0EMKOki1pruopVWW6fca8eLx5FuX5AzHZg7lsTXIi7iTfvGAo99Udbj
|
||||||
|
RNQGHofu1qkAEQEAAYkCNgQYAQoAIBYhBJx0DukZw1RxqWF3aqTeMnKUob1VBQJp
|
||||||
|
VFTnAhsMAAoJEKTeMnKUob1V8IQP/iQZAB1dtlsTkMZyUUP1ZqY0RwEkDyYgRMMV
|
||||||
|
Xg845CoNnHss+ioHf6ObneKNwogow/r9OTXSDA4gBWvk1WsJ8j6tGwMSKY+mH/rh
|
||||||
|
lER/lBeMCuvMc2/KE8VoOJkpXBktSdwEzLAxToTkyHuevxY43/g59xbBCImrIcVa
|
||||||
|
kPJBtboxLkm0BE3nwhD6m/Uo1oECq8F2cDI5luYzOsIMjKyvavTUDsNX1RE7Ula8
|
||||||
|
m8ra3QkgM8f4f1cmJP6y927RLEgXLxsbFlUjvAXRPe2sMIGV+32RTcXbR8zizuZl
|
||||||
|
eyORq45t6EO0a8x1Bh0Jkimk0wBQKA00iiHb3vJYySJltfaWmJMPVXs1zZpK3n1l
|
||||||
|
WRX4aO8MSBvqE+ZLrWJW3M8Yb6CeFh5yQv6B+1Nqyfk4pO5Q95/aRtG6qijQsxfb
|
||||||
|
c+RqhL4yT+2KLBUjj0gg98MEM9+MoFxbWpJtnyA7LmgfatG8gHj1HxLBmEhkc5d5
|
||||||
|
moM81nG/pWfUonOaVqcJji4UyDPNTOyAqHtp7iZUB80zu0IoyFRorsE9BkN4q/oY
|
||||||
|
6lyyKcOLalvL2sJ3GtzBd1nmCjE2BMLD3jn8l7ig/FNVF2AZPWYmmCBiMuAMBRcA
|
||||||
|
NXzOwAMmrgN41olFG9nAsoLLlxH69mUJTtw0sqAqtVzEnTwynbSm6H6dIL4yW7dA
|
||||||
|
/jkWmwL9
|
||||||
|
=4VM7
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
6
front/public/favicon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M140.689 59.2354C111.761 47.8765 66.799 59.5653 55.2538 91.8918C55.2538 91.8918 107.039 91.5467 111.331 93.8711C111.331 93.8711 116.552 81.3665 122.216 74.4091C127.853 67.4848 140.689 59.2354 140.689 59.2354Z" fill="#FCD770"/>
|
||||||
|
<path d="M137.06 112.673L181.592 145C187.529 104.427 176.555 73.3189 140.689 59.2354C140.689 59.2354 127.853 67.4848 122.216 74.4091C116.552 81.3665 111.331 93.8711 111.331 93.8711C119.247 98.1593 137.06 112.673 137.06 112.673Z" fill="#FCD770"/>
|
||||||
|
<path d="M16 112.673H137.06C137.06 112.673 119.247 98.1593 111.331 93.8711C107.039 91.5467 55.2538 91.8918 55.2538 91.8918C55.2538 91.8918 37.2519 91.2968 28.5348 98.1593C23.028 102.495 16 112.673 16 112.673Z" fill="#FFEAA7"/>
|
||||||
|
<path d="M137.06 112.673H16C16 112.673 23.028 102.495 28.5348 98.1593C37.2519 91.2968 55.2538 91.8918 55.2538 91.8918M137.06 112.673L181.592 145C187.529 104.427 176.555 73.3189 140.689 59.2354M137.06 112.673C137.06 112.673 119.247 98.1593 111.331 93.8711M140.689 59.2354C111.761 47.8765 66.799 59.5653 55.2538 91.8918M140.689 59.2354C140.689 59.2354 127.853 67.4848 122.216 74.4091C116.552 81.3665 111.331 93.8711 111.331 93.8711M55.2538 91.8918C55.2538 91.8918 107.039 91.5467 111.331 93.8711" stroke="#030303" stroke-width="10.5556"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
front/public/fonts/unbounded-black.ttf
Normal file
71
front/public/logo-large.svg
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<svg width="470" height="86" viewBox="0 0 470 86" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="path-1-outside-1_439_173" maskUnits="userSpaceOnUse" x="0" y="0" width="470" height="86" fill="black">
|
||||||
|
<rect fill="white" width="470" height="86"/>
|
||||||
|
<path d="M2.215 22.222L12.502 24.328L22.789 22.222V69.769H2.215V22.222ZM12.502 19.792C9.046 19.792 6.265 18.955 4.159 17.281C2.053 15.607 1 13.312 1 10.396C1 7.48 2.053 5.185 4.159 3.511C6.265 1.837 9.046 1 12.502 1C16.012 1 18.793 1.837 20.845 3.511C22.951 5.185 24.004 7.48 24.004 10.396C24.004 13.312 22.951 15.607 20.845 17.281C18.793 18.955 16.012 19.792 12.502 19.792Z"/>
|
||||||
|
<path d="M54.0499 53.812L48.5419 52.03L71.7889 23.194H92.6869L51.6199 69.769H33.4759V7.399H54.0499V53.812ZM58.5049 47.251L74.3809 37.126L93.8209 69.769H70.9789L58.5049 47.251Z"/>
|
||||||
|
<path d="M86.5493 23.194H107.123L110.363 43.039V69.769H89.7893V39.88L86.5493 23.194ZM126.482 21.736C131.018 21.736 134.879 22.654 138.065 24.49C141.251 26.272 143.681 28.81 145.355 32.104C147.083 35.344 147.947 39.151 147.947 43.525V69.769H127.373V46.441C127.373 43.849 126.644 41.851 125.186 40.447C123.782 39.043 121.784 38.341 119.192 38.341C117.356 38.341 115.763 38.719 114.413 39.475C113.117 40.177 112.118 41.23 111.416 42.634C110.714 43.984 110.363 45.631 110.363 47.575L104.288 44.497C105.044 39.421 106.502 35.209 108.662 31.861C110.822 28.459 113.441 25.921 116.519 24.247C119.597 22.573 122.918 21.736 126.482 21.736Z"/>
|
||||||
|
<path d="M175.999 71.227C169.843 71.227 164.443 70.201 159.799 68.149C155.209 66.097 151.618 63.208 149.026 59.482C146.488 55.756 145.219 51.436 145.219 46.522C145.219 41.554 146.488 37.207 149.026 33.481C151.618 29.755 155.209 26.866 159.799 24.814C164.443 22.762 169.843 21.736 175.999 21.736C182.155 21.736 187.528 22.762 192.118 24.814C196.762 26.866 200.353 29.755 202.891 33.481C205.483 37.207 206.779 41.554 206.779 46.522C206.779 51.436 205.483 55.756 202.891 59.482C200.353 63.208 196.762 66.097 192.118 68.149C187.528 70.201 182.155 71.227 175.999 71.227ZM175.999 56.242C178.159 56.242 179.968 55.864 181.426 55.108C182.938 54.352 184.072 53.245 184.828 51.787C185.638 50.329 186.043 48.574 186.043 46.522C186.043 44.416 185.638 42.634 184.828 41.176C184.072 39.718 182.938 38.611 181.426 37.855C179.968 37.099 178.159 36.721 175.999 36.721C173.893 36.721 172.084 37.099 170.572 37.855C169.06 38.611 167.899 39.718 167.089 41.176C166.333 42.634 165.955 44.416 165.955 46.522C165.955 48.574 166.333 50.329 167.089 51.787C167.899 53.245 169.06 54.352 170.572 55.108C172.084 55.864 173.893 56.242 175.999 56.242Z"/>
|
||||||
|
<path d="M267.944 57.862H261.869L273.128 23.194H293.702L277.178 69.769H256.199L242.834 33.157H249.314L235.949 69.769H214.97L198.446 23.194H219.02L230.279 57.862H224.204L236.435 23.194H255.713L267.944 57.862Z"/>
|
||||||
|
<path d="M313.838 84.592C309.95 84.592 306.521 84.106 303.551 83.134C300.635 82.216 297.719 80.677 294.803 78.517V64.747C297.665 66.583 300.311 67.906 302.741 68.716C305.225 69.472 307.898 69.85 310.76 69.85C313.028 69.85 314.999 69.391 316.673 68.473C318.401 67.501 319.805 65.665 320.885 62.965L336.68 23.194H358.55L337.328 69.121C335.546 73.009 333.359 76.087 330.767 78.355C328.229 80.623 325.502 82.216 322.586 83.134C319.67 84.106 316.754 84.592 313.838 84.592ZM311.246 63.532L292.535 23.194H315.215L330.848 63.532H311.246Z"/>
|
||||||
|
<path d="M379.954 71.227C373.798 71.227 368.398 70.201 363.754 68.149C359.164 66.097 355.573 63.208 352.981 59.482C350.443 55.756 349.174 51.436 349.174 46.522C349.174 41.554 350.443 37.207 352.981 33.481C355.573 29.755 359.164 26.866 363.754 24.814C368.398 22.762 373.798 21.736 379.954 21.736C386.11 21.736 391.483 22.762 396.073 24.814C400.717 26.866 404.308 29.755 406.846 33.481C409.438 37.207 410.734 41.554 410.734 46.522C410.734 51.436 409.438 55.756 406.846 59.482C404.308 63.208 400.717 66.097 396.073 68.149C391.483 70.201 386.11 71.227 379.954 71.227ZM379.954 56.242C382.114 56.242 383.923 55.864 385.381 55.108C386.893 54.352 388.027 53.245 388.783 51.787C389.593 50.329 389.998 48.574 389.998 46.522C389.998 44.416 389.593 42.634 388.783 41.176C388.027 39.718 386.893 38.611 385.381 37.855C383.923 37.099 382.114 36.721 379.954 36.721C377.848 36.721 376.039 37.099 374.527 37.855C373.015 38.611 371.854 39.718 371.044 41.176C370.288 42.634 369.91 44.416 369.91 46.522C369.91 48.574 370.288 50.329 371.044 51.787C371.854 53.245 373.015 54.352 374.527 55.108C376.039 55.864 377.848 56.242 379.954 56.242Z"/>
|
||||||
|
<path d="M428.843 71.146C424.469 71.146 420.716 70.255 417.584 68.473C414.452 66.637 412.049 64.099 410.375 60.859C408.755 57.565 407.945 53.758 407.945 49.438V23.194H428.519V46.522C428.519 49.06 429.167 51.031 430.463 52.435C431.813 53.839 433.703 54.541 436.133 54.541C438.023 54.541 439.562 54.217 440.75 53.569C441.992 52.867 442.91 51.841 443.504 50.491C444.152 49.087 444.476 47.386 444.476 45.388L450.551 48.466C449.849 53.488 448.418 57.7 446.258 61.102C444.152 64.45 441.587 66.961 438.563 68.635C435.593 70.309 432.353 71.146 428.843 71.146ZM447.716 69.769L444.476 49.924V23.194H465.05V53.083L468.29 69.769H447.716Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M2.215 22.222L12.502 24.328L22.789 22.222V69.769H2.215V22.222ZM12.502 19.792C9.046 19.792 6.265 18.955 4.159 17.281C2.053 15.607 1 13.312 1 10.396C1 7.48 2.053 5.185 4.159 3.511C6.265 1.837 9.046 1 12.502 1C16.012 1 18.793 1.837 20.845 3.511C22.951 5.185 24.004 7.48 24.004 10.396C24.004 13.312 22.951 15.607 20.845 17.281C18.793 18.955 16.012 19.792 12.502 19.792Z" fill="url(#paint0_linear_439_173)"/>
|
||||||
|
<path d="M54.0499 53.812L48.5419 52.03L71.7889 23.194H92.6869L51.6199 69.769H33.4759V7.399H54.0499V53.812ZM58.5049 47.251L74.3809 37.126L93.8209 69.769H70.9789L58.5049 47.251Z" fill="url(#paint1_linear_439_173)"/>
|
||||||
|
<path d="M86.5493 23.194H107.123L110.363 43.039V69.769H89.7893V39.88L86.5493 23.194ZM126.482 21.736C131.018 21.736 134.879 22.654 138.065 24.49C141.251 26.272 143.681 28.81 145.355 32.104C147.083 35.344 147.947 39.151 147.947 43.525V69.769H127.373V46.441C127.373 43.849 126.644 41.851 125.186 40.447C123.782 39.043 121.784 38.341 119.192 38.341C117.356 38.341 115.763 38.719 114.413 39.475C113.117 40.177 112.118 41.23 111.416 42.634C110.714 43.984 110.363 45.631 110.363 47.575L104.288 44.497C105.044 39.421 106.502 35.209 108.662 31.861C110.822 28.459 113.441 25.921 116.519 24.247C119.597 22.573 122.918 21.736 126.482 21.736Z" fill="url(#paint2_linear_439_173)"/>
|
||||||
|
<path d="M175.999 71.227C169.843 71.227 164.443 70.201 159.799 68.149C155.209 66.097 151.618 63.208 149.026 59.482C146.488 55.756 145.219 51.436 145.219 46.522C145.219 41.554 146.488 37.207 149.026 33.481C151.618 29.755 155.209 26.866 159.799 24.814C164.443 22.762 169.843 21.736 175.999 21.736C182.155 21.736 187.528 22.762 192.118 24.814C196.762 26.866 200.353 29.755 202.891 33.481C205.483 37.207 206.779 41.554 206.779 46.522C206.779 51.436 205.483 55.756 202.891 59.482C200.353 63.208 196.762 66.097 192.118 68.149C187.528 70.201 182.155 71.227 175.999 71.227ZM175.999 56.242C178.159 56.242 179.968 55.864 181.426 55.108C182.938 54.352 184.072 53.245 184.828 51.787C185.638 50.329 186.043 48.574 186.043 46.522C186.043 44.416 185.638 42.634 184.828 41.176C184.072 39.718 182.938 38.611 181.426 37.855C179.968 37.099 178.159 36.721 175.999 36.721C173.893 36.721 172.084 37.099 170.572 37.855C169.06 38.611 167.899 39.718 167.089 41.176C166.333 42.634 165.955 44.416 165.955 46.522C165.955 48.574 166.333 50.329 167.089 51.787C167.899 53.245 169.06 54.352 170.572 55.108C172.084 55.864 173.893 56.242 175.999 56.242Z" fill="url(#paint3_linear_439_173)"/>
|
||||||
|
<path d="M267.944 57.862H261.869L273.128 23.194H293.702L277.178 69.769H256.199L242.834 33.157H249.314L235.949 69.769H214.97L198.446 23.194H219.02L230.279 57.862H224.204L236.435 23.194H255.713L267.944 57.862Z" fill="url(#paint4_linear_439_173)"/>
|
||||||
|
<path d="M313.838 84.592C309.95 84.592 306.521 84.106 303.551 83.134C300.635 82.216 297.719 80.677 294.803 78.517V64.747C297.665 66.583 300.311 67.906 302.741 68.716C305.225 69.472 307.898 69.85 310.76 69.85C313.028 69.85 314.999 69.391 316.673 68.473C318.401 67.501 319.805 65.665 320.885 62.965L336.68 23.194H358.55L337.328 69.121C335.546 73.009 333.359 76.087 330.767 78.355C328.229 80.623 325.502 82.216 322.586 83.134C319.67 84.106 316.754 84.592 313.838 84.592ZM311.246 63.532L292.535 23.194H315.215L330.848 63.532H311.246Z" fill="url(#paint5_linear_439_173)"/>
|
||||||
|
<path d="M379.954 71.227C373.798 71.227 368.398 70.201 363.754 68.149C359.164 66.097 355.573 63.208 352.981 59.482C350.443 55.756 349.174 51.436 349.174 46.522C349.174 41.554 350.443 37.207 352.981 33.481C355.573 29.755 359.164 26.866 363.754 24.814C368.398 22.762 373.798 21.736 379.954 21.736C386.11 21.736 391.483 22.762 396.073 24.814C400.717 26.866 404.308 29.755 406.846 33.481C409.438 37.207 410.734 41.554 410.734 46.522C410.734 51.436 409.438 55.756 406.846 59.482C404.308 63.208 400.717 66.097 396.073 68.149C391.483 70.201 386.11 71.227 379.954 71.227ZM379.954 56.242C382.114 56.242 383.923 55.864 385.381 55.108C386.893 54.352 388.027 53.245 388.783 51.787C389.593 50.329 389.998 48.574 389.998 46.522C389.998 44.416 389.593 42.634 388.783 41.176C388.027 39.718 386.893 38.611 385.381 37.855C383.923 37.099 382.114 36.721 379.954 36.721C377.848 36.721 376.039 37.099 374.527 37.855C373.015 38.611 371.854 39.718 371.044 41.176C370.288 42.634 369.91 44.416 369.91 46.522C369.91 48.574 370.288 50.329 371.044 51.787C371.854 53.245 373.015 54.352 374.527 55.108C376.039 55.864 377.848 56.242 379.954 56.242Z" fill="url(#paint6_linear_439_173)"/>
|
||||||
|
<path d="M428.843 71.146C424.469 71.146 420.716 70.255 417.584 68.473C414.452 66.637 412.049 64.099 410.375 60.859C408.755 57.565 407.945 53.758 407.945 49.438V23.194H428.519V46.522C428.519 49.06 429.167 51.031 430.463 52.435C431.813 53.839 433.703 54.541 436.133 54.541C438.023 54.541 439.562 54.217 440.75 53.569C441.992 52.867 442.91 51.841 443.504 50.491C444.152 49.087 444.476 47.386 444.476 45.388L450.551 48.466C449.849 53.488 448.418 57.7 446.258 61.102C444.152 64.45 441.587 66.961 438.563 68.635C435.593 70.309 432.353 71.146 428.843 71.146ZM447.716 69.769L444.476 49.924V23.194H465.05V53.083L468.29 69.769H447.716Z" fill="url(#paint7_linear_439_173)"/>
|
||||||
|
<path d="M2.215 22.222L12.502 24.328L22.789 22.222V69.769H2.215V22.222ZM12.502 19.792C9.046 19.792 6.265 18.955 4.159 17.281C2.053 15.607 1 13.312 1 10.396C1 7.48 2.053 5.185 4.159 3.511C6.265 1.837 9.046 1 12.502 1C16.012 1 18.793 1.837 20.845 3.511C22.951 5.185 24.004 7.48 24.004 10.396C24.004 13.312 22.951 15.607 20.845 17.281C18.793 18.955 16.012 19.792 12.502 19.792Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<path d="M54.0499 53.812L48.5419 52.03L71.7889 23.194H92.6869L51.6199 69.769H33.4759V7.399H54.0499V53.812ZM58.5049 47.251L74.3809 37.126L93.8209 69.769H70.9789L58.5049 47.251Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<path d="M86.5493 23.194H107.123L110.363 43.039V69.769H89.7893V39.88L86.5493 23.194ZM126.482 21.736C131.018 21.736 134.879 22.654 138.065 24.49C141.251 26.272 143.681 28.81 145.355 32.104C147.083 35.344 147.947 39.151 147.947 43.525V69.769H127.373V46.441C127.373 43.849 126.644 41.851 125.186 40.447C123.782 39.043 121.784 38.341 119.192 38.341C117.356 38.341 115.763 38.719 114.413 39.475C113.117 40.177 112.118 41.23 111.416 42.634C110.714 43.984 110.363 45.631 110.363 47.575L104.288 44.497C105.044 39.421 106.502 35.209 108.662 31.861C110.822 28.459 113.441 25.921 116.519 24.247C119.597 22.573 122.918 21.736 126.482 21.736Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<path d="M175.999 71.227C169.843 71.227 164.443 70.201 159.799 68.149C155.209 66.097 151.618 63.208 149.026 59.482C146.488 55.756 145.219 51.436 145.219 46.522C145.219 41.554 146.488 37.207 149.026 33.481C151.618 29.755 155.209 26.866 159.799 24.814C164.443 22.762 169.843 21.736 175.999 21.736C182.155 21.736 187.528 22.762 192.118 24.814C196.762 26.866 200.353 29.755 202.891 33.481C205.483 37.207 206.779 41.554 206.779 46.522C206.779 51.436 205.483 55.756 202.891 59.482C200.353 63.208 196.762 66.097 192.118 68.149C187.528 70.201 182.155 71.227 175.999 71.227ZM175.999 56.242C178.159 56.242 179.968 55.864 181.426 55.108C182.938 54.352 184.072 53.245 184.828 51.787C185.638 50.329 186.043 48.574 186.043 46.522C186.043 44.416 185.638 42.634 184.828 41.176C184.072 39.718 182.938 38.611 181.426 37.855C179.968 37.099 178.159 36.721 175.999 36.721C173.893 36.721 172.084 37.099 170.572 37.855C169.06 38.611 167.899 39.718 167.089 41.176C166.333 42.634 165.955 44.416 165.955 46.522C165.955 48.574 166.333 50.329 167.089 51.787C167.899 53.245 169.06 54.352 170.572 55.108C172.084 55.864 173.893 56.242 175.999 56.242Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<path d="M267.944 57.862H261.869L273.128 23.194H293.702L277.178 69.769H256.199L242.834 33.157H249.314L235.949 69.769H214.97L198.446 23.194H219.02L230.279 57.862H224.204L236.435 23.194H255.713L267.944 57.862Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<path d="M313.838 84.592C309.95 84.592 306.521 84.106 303.551 83.134C300.635 82.216 297.719 80.677 294.803 78.517V64.747C297.665 66.583 300.311 67.906 302.741 68.716C305.225 69.472 307.898 69.85 310.76 69.85C313.028 69.85 314.999 69.391 316.673 68.473C318.401 67.501 319.805 65.665 320.885 62.965L336.68 23.194H358.55L337.328 69.121C335.546 73.009 333.359 76.087 330.767 78.355C328.229 80.623 325.502 82.216 322.586 83.134C319.67 84.106 316.754 84.592 313.838 84.592ZM311.246 63.532L292.535 23.194H315.215L330.848 63.532H311.246Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<path d="M379.954 71.227C373.798 71.227 368.398 70.201 363.754 68.149C359.164 66.097 355.573 63.208 352.981 59.482C350.443 55.756 349.174 51.436 349.174 46.522C349.174 41.554 350.443 37.207 352.981 33.481C355.573 29.755 359.164 26.866 363.754 24.814C368.398 22.762 373.798 21.736 379.954 21.736C386.11 21.736 391.483 22.762 396.073 24.814C400.717 26.866 404.308 29.755 406.846 33.481C409.438 37.207 410.734 41.554 410.734 46.522C410.734 51.436 409.438 55.756 406.846 59.482C404.308 63.208 400.717 66.097 396.073 68.149C391.483 70.201 386.11 71.227 379.954 71.227ZM379.954 56.242C382.114 56.242 383.923 55.864 385.381 55.108C386.893 54.352 388.027 53.245 388.783 51.787C389.593 50.329 389.998 48.574 389.998 46.522C389.998 44.416 389.593 42.634 388.783 41.176C388.027 39.718 386.893 38.611 385.381 37.855C383.923 37.099 382.114 36.721 379.954 36.721C377.848 36.721 376.039 37.099 374.527 37.855C373.015 38.611 371.854 39.718 371.044 41.176C370.288 42.634 369.91 44.416 369.91 46.522C369.91 48.574 370.288 50.329 371.044 51.787C371.854 53.245 373.015 54.352 374.527 55.108C376.039 55.864 377.848 56.242 379.954 56.242Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<path d="M428.843 71.146C424.469 71.146 420.716 70.255 417.584 68.473C414.452 66.637 412.049 64.099 410.375 60.859C408.755 57.565 407.945 53.758 407.945 49.438V23.194H428.519V46.522C428.519 49.06 429.167 51.031 430.463 52.435C431.813 53.839 433.703 54.541 436.133 54.541C438.023 54.541 439.562 54.217 440.75 53.569C441.992 52.867 442.91 51.841 443.504 50.491C444.152 49.087 444.476 47.386 444.476 45.388L450.551 48.466C449.849 53.488 448.418 57.7 446.258 61.102C444.152 64.45 441.587 66.961 438.563 68.635C435.593 70.309 432.353 71.146 428.843 71.146ZM447.716 69.769L444.476 49.924V23.194H465.05V53.083L468.29 69.769H447.716Z" stroke="black" stroke-width="2" mask="url(#path-1-outside-1_439_173)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint4_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint5_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint6_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint7_linear_439_173" x1="234.645" y1="1" x2="234.645" y2="84.592" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E9F7A7"/>
|
||||||
|
<stop offset="0.485632" stop-color="#E9BED9"/>
|
||||||
|
<stop offset="1" stop-color="#BBA6EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 17 KiB |
6
front/public/logo.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="125" height="72" viewBox="0 0 125 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M91.9198 6.48956C72.1867 -1.25905 41.5148 6.71462 33.639 28.7668C33.639 28.7668 68.9655 28.5313 71.8928 30.1169C71.8928 30.1169 75.4549 21.5867 79.3185 16.8406C83.1638 12.1171 91.9198 6.48956 91.9198 6.48956Z" fill="#FCD770"/>
|
||||||
|
<path d="M89.4445 42.9432L119.823 64.9954C123.873 37.3176 116.386 16.0969 91.9198 6.48956C91.9198 6.48956 83.1638 12.1171 79.3185 16.8406C75.4549 21.5867 71.8928 30.1169 71.8928 30.1169C77.2933 33.0422 89.4445 42.9432 89.4445 42.9432Z" fill="#FCD770"/>
|
||||||
|
<path d="M6.86133 42.9432H89.4445C89.4445 42.9432 77.2933 33.0422 71.8928 30.1169C68.9655 28.5313 33.639 28.7668 33.639 28.7668C33.639 28.7668 21.3587 28.3608 15.4122 33.0422C11.6556 35.9996 6.86133 42.9432 6.86133 42.9432Z" fill="#FFEAA7"/>
|
||||||
|
<path d="M89.4445 42.9432H6.86133C6.86133 42.9432 11.6556 35.9996 15.4122 33.0422C21.3587 28.3608 33.639 28.7668 33.639 28.7668M89.4445 42.9432L119.823 64.9954C123.873 37.3176 116.386 16.0969 91.9198 6.48956M89.4445 42.9432C89.4445 42.9432 77.2933 33.0422 71.8928 30.1169M91.9198 6.48956C72.1867 -1.25905 41.5148 6.71462 33.639 28.7668M91.9198 6.48956C91.9198 6.48956 83.1638 12.1171 79.3185 16.8406C75.4549 21.5867 71.8928 30.1169 71.8928 30.1169M33.639 28.7668C33.639 28.7668 68.9655 28.5313 71.8928 30.1169" stroke="#030303" stroke-width="7.20072"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
front/public/op.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
5
front/public/security.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Contact: mailto:anotherhadi.clapped234[at]passmail.net
|
||||||
|
Expires: 2028-12-31T23:00:00.000Z
|
||||||
|
Encryption: /anotherhadi.asc
|
||||||
|
Preferred-Languages: en, fr
|
||||||
|
Canonical: /.well-known/security.txt
|
||||||
83
front/src/components/CheatsheetList.svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Sheet {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let { sheets }: { sheets: Sheet[] } = $props();
|
||||||
|
|
||||||
|
let search = $state("");
|
||||||
|
let activeTag: string | null = $state(
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? new URLSearchParams(window.location.search).get("tag")
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const allTags = [...new Set(sheets.flatMap((s) => s.tags ?? []))].sort();
|
||||||
|
|
||||||
|
const filtered = $derived(
|
||||||
|
sheets.filter((s) => {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
const matchSearch =
|
||||||
|
!q ||
|
||||||
|
s.title.toLowerCase().includes(q) ||
|
||||||
|
(s.description?.toLowerCase().includes(q) ?? false);
|
||||||
|
const matchTag = !activeTag || (s.tags?.includes(activeTag) ?? false);
|
||||||
|
return matchSearch && matchTag;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search cheatsheets..."
|
||||||
|
bind:value={search}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if allTags.length > 0}
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each allTags as tag}
|
||||||
|
<button
|
||||||
|
class="badge badge-md cursor-pointer transition-colors {activeTag === tag
|
||||||
|
? 'badge-primary'
|
||||||
|
: 'badge-ghost hover:badge-outline'}"
|
||||||
|
onclick={() => (activeTag = activeTag === tag ? null : tag)}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
{#if filtered.length === 0}
|
||||||
|
<p class="text-base-content/40 text-sm py-6 text-center">No results.</p>
|
||||||
|
{:else}
|
||||||
|
{#each filtered as sheet}
|
||||||
|
<a
|
||||||
|
href={`/cheatsheets/${sheet.id}`}
|
||||||
|
class="card bg-base-200 hover:bg-base-300 transition-colors p-4 flex flex-row items-center gap-4"
|
||||||
|
>
|
||||||
|
<div class="size-2 rounded-full bg-primary shrink-0"></div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="font-semibold text-sm">{sheet.title}</div>
|
||||||
|
{#if sheet.description}
|
||||||
|
<div class="text-base-content/50 text-xs mt-0.5">{sheet.description}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if sheet.tags && sheet.tags.length > 0}
|
||||||
|
<div class="flex gap-1 shrink-0">
|
||||||
|
{#each sheet.tags as tag}
|
||||||
|
<span class="badge badge-xs badge-ghost">{tag}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
24
front/src/components/DemoBanner.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
import { FlaskConical } from "@lucide/svelte";
|
||||||
|
|
||||||
|
let demo = $state(false);
|
||||||
|
|
||||||
|
async function checkDemo() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/config");
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
demo = data.demo === true;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDemo();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if demo}
|
||||||
|
<div class="w-full bg-warning/15 border-b border-warning/30 py-1.5 px-4 flex items-center justify-center gap-2 text-xs text-warning">
|
||||||
|
<FlaskConical size={13} class="shrink-0" />
|
||||||
|
<span>Demo mode — searches and configuration changes are disabled</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
98
front/src/components/HomePage.svelte
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<script>
|
||||||
|
import { RotateCw, AlertTriangle } from "@lucide/svelte";
|
||||||
|
import SearchBar from "./SearchBar.svelte";
|
||||||
|
import SearchList from "./SearchList.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let searches = $state([]);
|
||||||
|
let loadError = $state("");
|
||||||
|
let redirecting = $state(false);
|
||||||
|
let redirectTarget = $state("");
|
||||||
|
let demo = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
loadSearches();
|
||||||
|
fetch("/api/config")
|
||||||
|
.then((r) => r.ok ? r.json() : null)
|
||||||
|
.then((d) => { if (d) demo = d.demo === true; })
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const target = params.get("target");
|
||||||
|
const type = params.get("type");
|
||||||
|
if (target && type) {
|
||||||
|
// Clean URL before launching so a refresh doesn't re-trigger
|
||||||
|
window.history.replaceState({}, "", window.location.pathname);
|
||||||
|
await handleSearch(target, type, params.get("profile") || "default");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadSearches() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/searches");
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
searches = (data ?? []).sort(
|
||||||
|
(a, b) => new Date(b.started_at) - new Date(a.started_at)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearch(target, inputType, profile) {
|
||||||
|
redirectTarget = target;
|
||||||
|
redirecting = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/searches", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ target, input_type: inputType, profile: profile || undefined }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const s = await res.json();
|
||||||
|
window.location.href = `/search/${s.id}`;
|
||||||
|
} catch (e) {
|
||||||
|
redirecting = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(id) {
|
||||||
|
await fetch(`/api/searches/${id}`, { method: "DELETE" });
|
||||||
|
searches = searches.filter((s) => s.id !== id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
|
|
||||||
|
<div class="card bg-base-200 shadow p-6">
|
||||||
|
{#if redirecting}
|
||||||
|
<div class="flex flex-col items-center justify-center gap-3 py-4">
|
||||||
|
<span class="loading loading-dots loading-md text-primary"></span>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
Searching <span class="font-mono text-base-content/90">{redirectTarget}</span>...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<SearchBar onSearch={handleSearch} {demo} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-xs uppercase tracking-widest text-base-content/50">Recent searches</h2>
|
||||||
|
<button class="btn btn-ghost btn-xs" onclick={loadSearches}><RotateCw class="size-3" /> refresh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loadError}
|
||||||
|
<div class="alert alert-error text-sm gap-2"><AlertTriangle size={15} class="shrink-0" />{loadError}</div>
|
||||||
|
{:else}
|
||||||
|
<SearchList {searches} onDelete={handleDelete} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
136
front/src/components/Nav.svelte
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Search,
|
||||||
|
Hammer,
|
||||||
|
SlidersHorizontal,
|
||||||
|
GitBranch,
|
||||||
|
User,
|
||||||
|
BookOpen,
|
||||||
|
Bug,
|
||||||
|
ClipboardList,
|
||||||
|
} from "@lucide/svelte";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
action,
|
||||||
|
}: {
|
||||||
|
title?: string;
|
||||||
|
action?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ label: "Search", href: "/", icon: Search },
|
||||||
|
{ label: "Tools", href: "/tools", icon: Hammer },
|
||||||
|
{ label: "Profiles", href: "/profiles", icon: SlidersHorizontal },
|
||||||
|
{ label: "Cheatsheets", href: "/cheatsheets", icon: ClipboardList },
|
||||||
|
{
|
||||||
|
label: "More",
|
||||||
|
children: [
|
||||||
|
{ label: "How it works", href: "/help", icon: BookOpen },
|
||||||
|
{
|
||||||
|
label: "Source code",
|
||||||
|
href: "https://github.com/anotherhadi/iknowyou",
|
||||||
|
icon: GitBranch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report a Bug",
|
||||||
|
href: "https://github.com/anotherhadi/iknowyou/issues",
|
||||||
|
icon: Bug,
|
||||||
|
},
|
||||||
|
{ label: "About me", href: "https://hadi.icu", icon: User },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-base-200">
|
||||||
|
<div class="navbar max-w-5xl m-auto">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<div class="dropdown">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||||
|
<Menu size={20} />
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="-1"
|
||||||
|
class="menu menu-sm dropdown-content bg-base-300 rounded-box z-50 mt-3 w-52 p-2"
|
||||||
|
>
|
||||||
|
{#each navLinks as link}
|
||||||
|
<li>
|
||||||
|
{#if link.children}
|
||||||
|
<span>{link.label}</span>
|
||||||
|
<ul class="p-2">
|
||||||
|
{#each link.children as sublink}
|
||||||
|
<li>
|
||||||
|
<a href={sublink.href} class="flex items-center gap-2">
|
||||||
|
{#if sublink.icon}
|
||||||
|
{@const Icon = sublink.icon}
|
||||||
|
<Icon size={12} />
|
||||||
|
{/if}
|
||||||
|
{sublink.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<a href={link.href} class="flex items-center gap-2">
|
||||||
|
{#if link.icon}
|
||||||
|
{@const Icon = link.icon}
|
||||||
|
<Icon size={12} />
|
||||||
|
{/if}
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="btn btn-ghost text-xl flex justify-center gap-2 items-center"
|
||||||
|
>
|
||||||
|
<img src="/logo.svg" class="m-auto h-6" alt="iky logo" />
|
||||||
|
<img src="/logo-large.svg" class="m-auto h-6" alt="iky logo large" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-center hidden lg:flex">
|
||||||
|
<ul class="menu menu-horizontal px-1">
|
||||||
|
{#each navLinks as link}
|
||||||
|
<li>
|
||||||
|
{#if link.children}
|
||||||
|
<details>
|
||||||
|
<summary>{link.label}</summary>
|
||||||
|
<ul class="p-2 bg-base-300 w-52 z-50">
|
||||||
|
{#each link.children as sublink}
|
||||||
|
<li>
|
||||||
|
<a href={sublink.href} class="flex items-center gap-2">
|
||||||
|
{#if sublink.icon}
|
||||||
|
{@const Icon = sublink.icon}
|
||||||
|
<Icon size={12} />
|
||||||
|
{/if}
|
||||||
|
{sublink.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
{:else}
|
||||||
|
<a href={link.href} class="flex items-center gap-2">
|
||||||
|
{#if link.icon}
|
||||||
|
{@const Icon = link.icon}
|
||||||
|
<Icon size={14} />
|
||||||
|
{/if}
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
{@render action?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
571
front/src/components/ProfileSettings.svelte
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Plus, Trash2, Save, ChevronRight, X, Lock, AlertTriangle } from "@lucide/svelte";
|
||||||
|
import Select from "./comps/Select.svelte";
|
||||||
|
import Badge from "./comps/Badge.svelte";
|
||||||
|
import InfoTip from "./comps/InfoTip.svelte";
|
||||||
|
|
||||||
|
let tools = $state([]);
|
||||||
|
let profiles = $state([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state("");
|
||||||
|
let configReadonly = $state(false);
|
||||||
|
|
||||||
|
let selectedProfile = $state(null);
|
||||||
|
let profileDetail = $state(null);
|
||||||
|
let profileLoading = $state(false);
|
||||||
|
|
||||||
|
let notesEdit = $state("");
|
||||||
|
let enabledEdit = $state([]);
|
||||||
|
let disabledEdit = $state([]);
|
||||||
|
let rulesSaving = $state(false);
|
||||||
|
let rulesMsg = $state(null);
|
||||||
|
|
||||||
|
let overrideEdits = $state({});
|
||||||
|
let overrideSaving = $state({});
|
||||||
|
let overrideMsg = $state({});
|
||||||
|
|
||||||
|
let showNewProfile = $state(false);
|
||||||
|
let newName = $state("");
|
||||||
|
let newProfileSaving = $state(false);
|
||||||
|
let newProfileError = $state("");
|
||||||
|
|
||||||
|
let overrideToolNames = $derived(Object.keys(profileDetail?.tools ?? {}));
|
||||||
|
let configurableTools = $derived(tools.filter((t) => t.config_fields?.length > 0));
|
||||||
|
let availableForOverride = $derived(configurableTools.filter((t) => !overrideToolNames.includes(t.name)));
|
||||||
|
let allToolNames = $derived(tools.map((t) => t.name));
|
||||||
|
let isReadonly = $derived((profileDetail?.readonly ?? false) || configReadonly);
|
||||||
|
|
||||||
|
onMount(loadAll);
|
||||||
|
|
||||||
|
async function loadAll() {
|
||||||
|
loading = true;
|
||||||
|
error = "";
|
||||||
|
try {
|
||||||
|
const [tr, pr, cr] = await Promise.all([
|
||||||
|
fetch("/api/tools"),
|
||||||
|
fetch("/api/config/profiles"),
|
||||||
|
fetch("/api/config"),
|
||||||
|
]);
|
||||||
|
if (!tr.ok) throw new Error(`HTTP ${tr.status}`);
|
||||||
|
if (!pr.ok) throw new Error(`HTTP ${pr.status}`);
|
||||||
|
tools = await tr.json();
|
||||||
|
profiles = await pr.json();
|
||||||
|
if (cr.ok) {
|
||||||
|
const cfg = await cr.json();
|
||||||
|
configReadonly = cfg.readonly ?? false;
|
||||||
|
}
|
||||||
|
if (!selectedProfile && profiles.length > 0) {
|
||||||
|
const def = profiles.find((p) => p.name === "default");
|
||||||
|
await selectProfile(def ? "default" : profiles[0].name);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectProfile(name) {
|
||||||
|
selectedProfile = name;
|
||||||
|
profileLoading = true;
|
||||||
|
profileDetail = null;
|
||||||
|
overrideEdits = {};
|
||||||
|
overrideSaving = {};
|
||||||
|
overrideMsg = {};
|
||||||
|
rulesMsg = null;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/config/profiles/${encodeURIComponent(name)}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
profileDetail = await res.json();
|
||||||
|
notesEdit = profileDetail.notes ?? "";
|
||||||
|
enabledEdit = [...(profileDetail.enabled ?? [])];
|
||||||
|
disabledEdit = [...(profileDetail.disabled ?? [])];
|
||||||
|
const nextEdits = {};
|
||||||
|
for (const [toolName, toolConf] of Object.entries(profileDetail.tools ?? {})) {
|
||||||
|
const tool = tools.find((t) => t.name === toolName);
|
||||||
|
if (!tool?.config_fields?.length) continue;
|
||||||
|
nextEdits[toolName] = {};
|
||||||
|
for (const f of tool.config_fields) {
|
||||||
|
nextEdits[toolName][f.name] =
|
||||||
|
toolConf?.[f.name] !== undefined ? toolConf[f.name] : (f.default ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overrideEdits = nextEdits;
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
profileLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateNewName(name) {
|
||||||
|
if (!name) return "Name is required";
|
||||||
|
if (!/^[a-z0-9-]+$/.test(name)) return "Only lowercase letters (a-z), digits (0-9), and hyphens (-) are allowed";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProfile() {
|
||||||
|
const name = newName.trim();
|
||||||
|
const nameError = validateNewName(name);
|
||||||
|
if (nameError) { newProfileError = nameError; return; }
|
||||||
|
newProfileSaving = true;
|
||||||
|
newProfileError = "";
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/config/profiles", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
|
||||||
|
showNewProfile = false;
|
||||||
|
newName = "";
|
||||||
|
await loadAll();
|
||||||
|
await selectProfile(name);
|
||||||
|
} catch (e) {
|
||||||
|
newProfileError = e.message;
|
||||||
|
} finally {
|
||||||
|
newProfileSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProfile(name) {
|
||||||
|
if (!confirm(`Delete profile "${name}"?`)) return;
|
||||||
|
try {
|
||||||
|
await fetch(`/api/config/profiles/${encodeURIComponent(name)}`, { method: "DELETE" });
|
||||||
|
if (selectedProfile === name) {
|
||||||
|
selectedProfile = null;
|
||||||
|
profileDetail = null;
|
||||||
|
}
|
||||||
|
await loadAll();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRules() {
|
||||||
|
rulesSaving = true;
|
||||||
|
rulesMsg = null;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/config/profiles/${encodeURIComponent(selectedProfile)}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ enabled: enabledEdit, disabled: disabledEdit, notes: notesEdit }),
|
||||||
|
});
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
|
||||||
|
rulesMsg = { ok: true, text: "Saved" };
|
||||||
|
setTimeout(() => (rulesMsg = null), 3000);
|
||||||
|
await selectProfile(selectedProfile);
|
||||||
|
} catch (e) {
|
||||||
|
rulesMsg = { ok: false, text: e.message };
|
||||||
|
} finally {
|
||||||
|
rulesSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveOverride(toolName) {
|
||||||
|
const tool = tools.find((t) => t.name === toolName);
|
||||||
|
for (const f of tool?.config_fields ?? []) {
|
||||||
|
if (f.required) {
|
||||||
|
const v = overrideEdits[toolName]?.[f.name];
|
||||||
|
if (v === undefined || v === null || v === "") {
|
||||||
|
flashOverride(toolName, { ok: false, text: `"${f.name}" is required` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideSaving = { ...overrideSaving, [toolName]: true };
|
||||||
|
overrideMsg = { ...overrideMsg, [toolName]: null };
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/config/profiles/${encodeURIComponent(selectedProfile)}/tools/${encodeURIComponent(toolName)}`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(overrideEdits[toolName]),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
|
||||||
|
flashOverride(toolName, { ok: true, text: "Saved" });
|
||||||
|
} catch (e) {
|
||||||
|
flashOverride(toolName, { ok: false, text: e.message });
|
||||||
|
} finally {
|
||||||
|
overrideSaving = { ...overrideSaving, [toolName]: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOverride(toolName) {
|
||||||
|
if (!confirm(`Remove "${toolName}" override from "${selectedProfile}"?`)) return;
|
||||||
|
try {
|
||||||
|
await fetch(
|
||||||
|
`/api/config/profiles/${encodeURIComponent(selectedProfile)}/tools/${encodeURIComponent(toolName)}`,
|
||||||
|
{ method: "DELETE" }
|
||||||
|
);
|
||||||
|
await selectProfile(selectedProfile);
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOverrideFor(toolName) {
|
||||||
|
if (!toolName) return;
|
||||||
|
const tool = tools.find((t) => t.name === toolName);
|
||||||
|
if (!tool) return;
|
||||||
|
const toolEdits = {};
|
||||||
|
for (const f of tool.config_fields ?? []) toolEdits[f.name] = f.default ?? "";
|
||||||
|
overrideEdits = { ...overrideEdits, [toolName]: toolEdits };
|
||||||
|
profileDetail = {
|
||||||
|
...profileDetail,
|
||||||
|
tools: { ...(profileDetail.tools ?? {}), [toolName]: {} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function flashOverride(toolName, val) {
|
||||||
|
overrideMsg = { ...overrideMsg, [toolName]: val };
|
||||||
|
setTimeout(() => {
|
||||||
|
overrideMsg = { ...overrideMsg, [toolName]: null };
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="flex justify-center py-16">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="alert alert-error gap-3"><AlertTriangle size={18} class="shrink-0" />{error}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col md:flex-row gap-0 items-start">
|
||||||
|
|
||||||
|
<div class="w-full md:w-52 shrink-0 flex flex-col gap-1 border-b border-base-300 pb-4 mb-4 md:border-b-0 md:border-r md:pb-0 md:mb-0 md:pr-4 md:mr-4">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-xs uppercase tracking-widest text-base-content/50">Profiles</span>
|
||||||
|
{#if !configReadonly}
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
onclick={() => { showNewProfile = !showNewProfile; newName = ""; newProfileError = ""; }}
|
||||||
|
>
|
||||||
|
<Plus size={14} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showNewProfile && !configReadonly}
|
||||||
|
<div class="flex flex-col gap-2 p-3 bg-base-300 rounded-box mb-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm w-full {newProfileError && !/^[a-z0-9-]*$/.test(newName) ? 'input-error' : ''}"
|
||||||
|
placeholder="profile-name"
|
||||||
|
bind:value={newName}
|
||||||
|
onkeydown={(e) => e.key === "Enter" && createProfile()}
|
||||||
|
/>
|
||||||
|
{#if newProfileError}
|
||||||
|
<p class="text-xs text-error">{newProfileError}</p>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-xs w-full"
|
||||||
|
onclick={createProfile}
|
||||||
|
disabled={newProfileSaving || !newName.trim()}
|
||||||
|
>
|
||||||
|
{#if newProfileSaving}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
Create
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each profiles as p}
|
||||||
|
<div class="flex items-center gap-1 group">
|
||||||
|
<button
|
||||||
|
class="flex-1 btn btn-sm {selectedProfile === p.name ? 'btn-primary' : 'btn-ghost'} justify-start gap-1 truncate"
|
||||||
|
onclick={() => selectProfile(p.name)}
|
||||||
|
>
|
||||||
|
{#if selectedProfile === p.name}
|
||||||
|
<ChevronRight size={14} class="shrink-0" />
|
||||||
|
{/if}
|
||||||
|
{#if p.readonly}
|
||||||
|
<Lock size={10} class="shrink-0 opacity-50" />
|
||||||
|
{/if}
|
||||||
|
{p.name}
|
||||||
|
</button>
|
||||||
|
{#if !p.readonly}
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs text-error opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
||||||
|
onclick={() => deleteProfile(p.name)}
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if profiles.length === 0}
|
||||||
|
<p class="text-base-content/40 text-xs text-center py-4">No profiles yet.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0 pt-4 md:pt-0">
|
||||||
|
{#if configReadonly}
|
||||||
|
<div class="alert alert-warning mb-4 py-2 px-3 text-sm gap-2">
|
||||||
|
<Lock size={14} class="shrink-0" />
|
||||||
|
Config is managed externally and is read-only.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !selectedProfile}
|
||||||
|
<p class="text-base-content/40 text-sm text-center py-8">Select a profile to view it.</p>
|
||||||
|
{:else if profileLoading}
|
||||||
|
<div class="flex justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-md"></span>
|
||||||
|
</div>
|
||||||
|
{:else if profileDetail}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 flex-wrap">
|
||||||
|
<h2 class="font-bold text-lg flex items-center gap-2">
|
||||||
|
{#if isReadonly}<Lock size={14} class="text-base-content/40" />{/if}
|
||||||
|
{selectedProfile}
|
||||||
|
</h2>
|
||||||
|
{#if isReadonly}
|
||||||
|
<Badge text="read-only" size="sm" />
|
||||||
|
{/if}
|
||||||
|
{#if profileDetail.active_tools?.length > 0}
|
||||||
|
<span class="text-xs text-base-content/50">
|
||||||
|
{profileDetail.active_tools.length} active tool{profileDetail.active_tools.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isReadonly}
|
||||||
|
{#if profileDetail.notes}
|
||||||
|
<p class="text-sm text-base-content/60 italic">{profileDetail.notes}</p>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-xs uppercase tracking-widest text-base-content/50">Notes</span>
|
||||||
|
<textarea
|
||||||
|
class="textarea textarea-bordered text-sm resize-none"
|
||||||
|
placeholder="Add a description for this profile..."
|
||||||
|
rows="2"
|
||||||
|
bind:value={notesEdit}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="card bg-base-200 shadow">
|
||||||
|
<div class="card-body gap-4 p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-xs uppercase tracking-widest text-base-content/50">Rules</h3>
|
||||||
|
{#if !isReadonly && rulesMsg}
|
||||||
|
<span class="text-xs {rulesMsg.ok ? 'text-success' : 'text-error'}">{rulesMsg.text}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm font-semibold">
|
||||||
|
Enabled
|
||||||
|
<InfoTip tooltip="Tools in the enabled list will be allowed for this profile. If the enabled list is empty, all tools will be enabled." />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-wrap gap-1 items-center min-h-8">
|
||||||
|
{#if isReadonly}
|
||||||
|
{#each (profileDetail.enabled ?? []) as toolName}
|
||||||
|
<span class="badge badge-outline gap-1">{toolName}</span>
|
||||||
|
{/each}
|
||||||
|
{#if (profileDetail.enabled ?? []).length === 0}
|
||||||
|
<span class="text-xs text-base-content/40">All tools</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{#each enabledEdit as toolName}
|
||||||
|
<span class="badge badge-outline gap-1">
|
||||||
|
{toolName}
|
||||||
|
<button onclick={() => (enabledEdit = enabledEdit.filter((x) => x !== toolName))}>
|
||||||
|
<X size={10} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
<Select
|
||||||
|
options={allToolNames.filter((n) => !enabledEdit.includes(n))}
|
||||||
|
placeholder="add tool"
|
||||||
|
size="xs"
|
||||||
|
onselect={(val) => (enabledEdit = [...enabledEdit, val])}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm font-semibold">
|
||||||
|
Disabled
|
||||||
|
<InfoTip tooltip="Tools in the disabled list will be blocked for this profile. Applied after enabled rules, so if a tool is in both lists, it will be disabled." />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-wrap gap-1 items-center min-h-8">
|
||||||
|
{#if isReadonly}
|
||||||
|
{#each (profileDetail.disabled ?? []) as toolName}
|
||||||
|
<span class="badge badge-error gap-1">{toolName}</span>
|
||||||
|
{/each}
|
||||||
|
{#if (profileDetail.disabled ?? []).length === 0}
|
||||||
|
<span class="text-xs text-base-content/40">None</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{#each disabledEdit as toolName}
|
||||||
|
<span class="badge badge-error gap-1">
|
||||||
|
{toolName}
|
||||||
|
<button onclick={() => (disabledEdit = disabledEdit.filter((x) => x !== toolName))}>
|
||||||
|
<X size={10} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
<Select
|
||||||
|
options={allToolNames.filter((n) => !disabledEdit.includes(n))}
|
||||||
|
placeholder="add tool"
|
||||||
|
size="xs"
|
||||||
|
onselect={(val) => (disabledEdit = [...disabledEdit, val])}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isReadonly}
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm gap-1 self-start"
|
||||||
|
onclick={saveRules}
|
||||||
|
disabled={rulesSaving}
|
||||||
|
>
|
||||||
|
{#if rulesSaving}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Save size={14} />
|
||||||
|
{/if}
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isReadonly}
|
||||||
|
<div class="card bg-base-200 shadow">
|
||||||
|
<div class="card-body gap-4 p-4">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<h3 class="text-xs uppercase tracking-widest text-base-content/50">Tool overrides</h3>
|
||||||
|
{#if availableForOverride.length > 0}
|
||||||
|
<Select
|
||||||
|
options={availableForOverride.map((t) => t.name)}
|
||||||
|
placeholder="add override"
|
||||||
|
size="xs"
|
||||||
|
onselect={(val) => addOverrideFor(val)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if overrideToolNames.length === 0}
|
||||||
|
<p class="text-sm text-base-content/40">No overrides configured.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
{#each overrideToolNames as toolName}
|
||||||
|
{@const tool = tools.find((t) => t.name === toolName)}
|
||||||
|
<div class="border border-base-300 rounded-box p-3 flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span class="font-semibold text-sm">{toolName}</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if overrideMsg[toolName]}
|
||||||
|
<span class="text-xs {overrideMsg[toolName].ok ? 'text-success' : 'text-error'}">
|
||||||
|
{overrideMsg[toolName].text}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs text-error"
|
||||||
|
onclick={() => deleteOverride(toolName)}
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if tool?.config_fields?.length && overrideEdits[toolName]}
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3">
|
||||||
|
{#each tool.config_fields as field}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs font-semibold">{field.name}</span>
|
||||||
|
<span class="badge badge-ghost badge-xs">{field.type}</span>
|
||||||
|
{#if field.required}
|
||||||
|
<span class="badge badge-error badge-xs">required</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if field.type === "bool"}
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer mt-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-sm toggle-primary"
|
||||||
|
bind:checked={overrideEdits[toolName][field.name]}
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-base-content/50">
|
||||||
|
{overrideEdits[toolName][field.name] ? "enabled" : "disabled"}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{:else if field.type === "int"}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
class="input input-bordered input-sm font-mono"
|
||||||
|
bind:value={overrideEdits[toolName][field.name]}
|
||||||
|
/>
|
||||||
|
{:else if field.type === "float"}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
class="input input-bordered input-sm font-mono"
|
||||||
|
bind:value={overrideEdits[toolName][field.name]}
|
||||||
|
/>
|
||||||
|
{:else if field.type === "enum"}
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-sm font-mono"
|
||||||
|
bind:value={overrideEdits[toolName][field.name]}
|
||||||
|
>
|
||||||
|
{#each field.options as opt}
|
||||||
|
<option value={opt}>{opt}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm font-mono"
|
||||||
|
bind:value={overrideEdits[toolName][field.name]}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm gap-1 self-start"
|
||||||
|
onclick={() => saveOverride(toolName)}
|
||||||
|
disabled={overrideSaving[toolName]}
|
||||||
|
>
|
||||||
|
{#if overrideSaving[toolName]}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Save size={14} />
|
||||||
|
{/if}
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<p class="text-xs text-base-content/40">This tool has no configurable fields.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
232
front/src/components/SearchBar.svelte
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<script>
|
||||||
|
import { Search, AlertTriangle } from "@lucide/svelte";
|
||||||
|
import Select from "./comps/Select.svelte";
|
||||||
|
import { INPUT_TYPES } from "@src/lib/vars";
|
||||||
|
|
||||||
|
let { onSearch = async () => {}, demo = false } = $props();
|
||||||
|
|
||||||
|
const DETECTORS = {
|
||||||
|
email: (_raw, v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
|
||||||
|
phone: (_raw, v) => /^\+\d{1,4} \d{4,}$/.test(v),
|
||||||
|
ip: (_raw, v) => /^(\d{1,3}\.){3}\d{1,3}$/.test(v) || /^[0-9a-fA-F:]{3,39}$/.test(v),
|
||||||
|
domain: (raw, v) => /^https?:\/\//.test(raw) || /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(v),
|
||||||
|
name: (_raw, v) => /^[a-zA-ZÀ-ÿ'-]+(?: [a-zA-ZÀ-ÿ'-]+){1,2}$/.test(v),
|
||||||
|
};
|
||||||
|
|
||||||
|
const VALIDATORS = {
|
||||||
|
email: { test: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), msg: "Invalid email address" },
|
||||||
|
username: { test: (v) => /^[a-zA-Z0-9._-]+$/.test(v), msg: "Username may only contain a-z, 0-9, . - _" },
|
||||||
|
phone: { test: (v) => /^\+\d{1,4} \d{4,}$/.test(v), msg: "Format: +INDICATIF NUMERO (ex: +33 0612345678)" },
|
||||||
|
ip: { test: (v) => /^(\d{1,3}\.){3}\d{1,3}$/.test(v) || /^[0-9a-fA-F:]{3,39}$/.test(v), msg: "Invalid IP address" },
|
||||||
|
domain: { test: (v) => /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(v), msg: "Invalid domain name" },
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = $state("");
|
||||||
|
let inputType = $state("email");
|
||||||
|
let profile = $state("default");
|
||||||
|
let profiles = $state([]);
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state("");
|
||||||
|
let validationError = $state("");
|
||||||
|
// null = auto-switch free; "TYPE" = auto-switched from TYPE (show revert); "__locked__" = user overrode, no auto-switch
|
||||||
|
let prevType = $state(null);
|
||||||
|
|
||||||
|
let showRevert = $derived(prevType !== null && prevType !== "__locked__");
|
||||||
|
let profileOptions = $derived(profiles.map((p) => p.name));
|
||||||
|
|
||||||
|
let strippedTarget = $derived.by(() => {
|
||||||
|
let v = target.trim();
|
||||||
|
v = v.replace(/^https?:\/\//, "");
|
||||||
|
if (v.startsWith("@")) v = v.slice(1);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
|
||||||
|
let detectedType = $derived.by(() => {
|
||||||
|
const raw = target.trim();
|
||||||
|
const v = strippedTarget;
|
||||||
|
if (!v && !raw) return null;
|
||||||
|
if (raw.startsWith("@")) return "username";
|
||||||
|
for (const [type, fn] of Object.entries(DETECTORS)) {
|
||||||
|
if (fn(raw, v)) return type;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadProfiles() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/config/profiles");
|
||||||
|
if (res.ok) profiles = await res.json();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProfiles();
|
||||||
|
|
||||||
|
function sanitize(s) {
|
||||||
|
return s.replace(/[<>"'`&]/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(val, type) {
|
||||||
|
const v = VALIDATORS[type];
|
||||||
|
if (!v) return "";
|
||||||
|
return v.test(val) ? "" : v.msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTargetInput() {
|
||||||
|
if (!strippedTarget) prevType = null; // reset when field is cleared
|
||||||
|
|
||||||
|
if (validationError) validationError = validate(strippedTarget, inputType);
|
||||||
|
|
||||||
|
if (prevType === null && detectedType && detectedType !== inputType) {
|
||||||
|
prevType = inputType;
|
||||||
|
inputType = detectedType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function revert() {
|
||||||
|
inputType = prevType;
|
||||||
|
prevType = "__locked__";
|
||||||
|
validationError = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectType(v) {
|
||||||
|
inputType = v;
|
||||||
|
prevType = "__locked__";
|
||||||
|
validationError = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (demo) return;
|
||||||
|
const clean = sanitize(strippedTarget);
|
||||||
|
if (!clean) return;
|
||||||
|
validationError = validate(clean, inputType);
|
||||||
|
if (validationError) return;
|
||||||
|
error = "";
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
await onSearch(clean, inputType, profile);
|
||||||
|
target = "";
|
||||||
|
prevType = null;
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#if error}
|
||||||
|
<div class="alert alert-error text-sm py-2 gap-2"><AlertTriangle size={15} class="shrink-0" />{error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showRevert}
|
||||||
|
<div class="flex items-center gap-2 px-1 text-xs text-base-content/50">
|
||||||
|
<span>Switched to <span class="text-base-content/70 font-medium">{inputType}</span></span>
|
||||||
|
<button
|
||||||
|
class="badge badge-ghost badge-sm hover:badge-primary transition-colors cursor-pointer"
|
||||||
|
onclick={revert}
|
||||||
|
>
|
||||||
|
← {prevType}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Mobile layout -->
|
||||||
|
<div class="flex flex-col gap-2 sm:hidden">
|
||||||
|
<div class="flex items-center rounded-xl border border-base-content/15 bg-base-300
|
||||||
|
focus-within:border-primary/40 transition-colors">
|
||||||
|
<input
|
||||||
|
class="flex-1 bg-transparent px-4 py-3 outline-none text-sm
|
||||||
|
placeholder:text-base-content/30 min-w-0"
|
||||||
|
placeholder={demo ? "Search disabled in demo mode" : "Enter target..."}
|
||||||
|
bind:value={target}
|
||||||
|
oninput={onTargetInput}
|
||||||
|
onkeydown={(e) => e.key === "Enter" && submit()}
|
||||||
|
disabled={demo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-xs text-base-content/25 pl-1">type</span>
|
||||||
|
<Select
|
||||||
|
options={INPUT_TYPES}
|
||||||
|
selected={inputType}
|
||||||
|
size="xs"
|
||||||
|
onselect={onSelectType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-xs text-base-content/25">profile</span>
|
||||||
|
<Select
|
||||||
|
options={profileOptions}
|
||||||
|
selected={profile}
|
||||||
|
size="xs"
|
||||||
|
onselect={(v) => { profile = v; }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm flex-1 gap-1"
|
||||||
|
onclick={submit}
|
||||||
|
disabled={demo || loading || !target.trim()}
|
||||||
|
>
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Search size={14} />
|
||||||
|
{/if}
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop layout -->
|
||||||
|
<div
|
||||||
|
class="hidden sm:flex items-center rounded-xl border border-base-content/15 bg-base-300
|
||||||
|
focus-within:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div class="border-r border-base-content/10 flex items-center gap-1 pl-3">
|
||||||
|
<span class="text-xs text-base-content/25 shrink-0">type</span>
|
||||||
|
<Select
|
||||||
|
options={INPUT_TYPES}
|
||||||
|
selected={inputType}
|
||||||
|
onselect={onSelectType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="flex-1 bg-transparent px-4 py-2.5 outline-none text-sm
|
||||||
|
placeholder:text-base-content/30 min-w-0"
|
||||||
|
placeholder={demo ? "Search disabled in demo mode" : "Enter target..."}
|
||||||
|
bind:value={target}
|
||||||
|
oninput={onTargetInput}
|
||||||
|
onkeydown={(e) => e.key === "Enter" && submit()}
|
||||||
|
disabled={demo}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="border-l border-base-content/10 flex items-center gap-1 pr-1 pl-3">
|
||||||
|
<span class="text-xs text-base-content/25 shrink-0">profile</span>
|
||||||
|
<Select
|
||||||
|
options={profileOptions}
|
||||||
|
selected={profile}
|
||||||
|
onselect={(v) => { profile = v; }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm m-1 rounded-lg gap-1 shrink-0"
|
||||||
|
onclick={submit}
|
||||||
|
disabled={demo || loading || !target.trim()}
|
||||||
|
>
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Search size={14} />
|
||||||
|
{/if}
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if validationError}
|
||||||
|
<p class="text-xs text-error pl-1">{validationError}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
349
front/src/components/SearchDetail.svelte
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
<script>
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import { RefreshCw, ChevronRight, Check, X, AlertTriangle } from "@lucide/svelte";
|
||||||
|
import TtyOutput from "@src/components/comps/TtyOutput.svelte";
|
||||||
|
|
||||||
|
let { id = null } = $props();
|
||||||
|
|
||||||
|
let demo = $state(false);
|
||||||
|
async function checkDemo() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/config");
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
demo = data.demo === true;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
checkDemo();
|
||||||
|
|
||||||
|
let resolvedId = $state("");
|
||||||
|
|
||||||
|
let search = $state(null);
|
||||||
|
let error = $state("");
|
||||||
|
let pollTimeout = null;
|
||||||
|
let pollDelay = $state(800);
|
||||||
|
const POLL_MIN = 800;
|
||||||
|
const POLL_MAX = 5000;
|
||||||
|
|
||||||
|
let grouped = $derived(groupByTool(search?.events ?? []));
|
||||||
|
let toolProgress = $derived(computeProgress(search?.planned_tools ?? [], grouped));
|
||||||
|
let totalResults = $derived(
|
||||||
|
(search?.planned_tools ?? []).reduce((sum, t) => sum + (t.result_count ?? 0), 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
let sortedEntries = $derived((() => {
|
||||||
|
const entries = Object.entries(grouped);
|
||||||
|
const hasResults = ([toolName, d]) => {
|
||||||
|
const count = search?.planned_tools?.find(t => t.name === toolName)?.result_count ?? null;
|
||||||
|
return d.done && (count !== null ? count > 0 : d.output.length > 0);
|
||||||
|
};
|
||||||
|
const withResults = entries.filter(([n, d]) => hasResults([n, d]));
|
||||||
|
const running = entries.filter(([_, d]) => !d.done);
|
||||||
|
const noResults = entries.filter(([n, d]) => d.done && !hasResults([n, d]));
|
||||||
|
return { withResults, running, noResults };
|
||||||
|
})());
|
||||||
|
|
||||||
|
function groupByTool(events) {
|
||||||
|
const map = {};
|
||||||
|
for (const e of events) {
|
||||||
|
if (!map[e.tool]) map[e.tool] = { errors: [], output: "", done: false };
|
||||||
|
if (e.type === "output") map[e.tool].output += e.payload;
|
||||||
|
else if (e.type === "error") map[e.tool].errors.push(e.payload);
|
||||||
|
else if (e.type === "done") map[e.tool].done = true;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeProgress(planned, grouped) {
|
||||||
|
const skipped = planned.filter((t) => t.skipped);
|
||||||
|
const active = planned.filter((t) => !t.skipped);
|
||||||
|
const errored = active.filter((t) => {
|
||||||
|
const d = grouped[t.name];
|
||||||
|
return d?.done && d.errors.length > 0 && d.output.length === 0;
|
||||||
|
});
|
||||||
|
const done = active.filter((t) => grouped[t.name]?.done);
|
||||||
|
const running = active.filter((t) => !grouped[t.name]?.done);
|
||||||
|
const skippedTotal = skipped.length + errored.length;
|
||||||
|
return { skipped, active, done, running, errored, skippedTotal };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/searches/${resolvedId}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
search = await res.json();
|
||||||
|
if (search.status !== "running") stopPolling();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
stopPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNext() {
|
||||||
|
pollTimeout = setTimeout(async () => {
|
||||||
|
await refresh();
|
||||||
|
if (search?.status === "running") {
|
||||||
|
pollDelay = Math.min(pollDelay * 2, POLL_MAX);
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
}, pollDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() { stopPolling(); pollDelay = POLL_MIN; scheduleNext(); }
|
||||||
|
function stopPolling() { if (pollTimeout) { clearTimeout(pollTimeout); pollTimeout = null; } }
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
resolvedId = id ?? window.location.pathname.replace(/^\/search\//, "").replace(/\/$/, "");
|
||||||
|
await refresh();
|
||||||
|
if (search?.status === "running") startPolling();
|
||||||
|
});
|
||||||
|
onDestroy(stopPolling);
|
||||||
|
|
||||||
|
let toast = $state(null); // { msg, type }
|
||||||
|
|
||||||
|
async function cancel() {
|
||||||
|
stopPolling();
|
||||||
|
await fetch(`/api/searches/${resolvedId}`, { method: "DELETE" });
|
||||||
|
toast = { msg: "Search cancelled.", type: "alert-warning" };
|
||||||
|
setTimeout(() => { window.location.href = "/"; }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(iso) {
|
||||||
|
return new Date(iso).toLocaleString(undefined, {
|
||||||
|
month: "short", day: "numeric",
|
||||||
|
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_BADGE = {
|
||||||
|
running: "badge-warning",
|
||||||
|
done: "badge-success",
|
||||||
|
cancelled: "badge-error",
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="alert alert-error gap-3"><AlertTriangle size={18} class="shrink-0" />{error}</div>
|
||||||
|
|
||||||
|
{:else if !search}
|
||||||
|
<div class="flex justify-center py-16">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-4 mb-6">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="flex items-center gap-3 mb-1 flex-wrap">
|
||||||
|
<h1 class="font-mono text-xl sm:text-2xl font-bold truncate">{search.target}</h1>
|
||||||
|
<span class="badge {STATUS_BADGE[search.status] ?? 'badge-ghost'} shrink-0">
|
||||||
|
{#if search.status === "running"}
|
||||||
|
<span class="loading loading-ring loading-xs mr-1"></span>
|
||||||
|
{/if}
|
||||||
|
{search.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap text-sm text-base-content/50">
|
||||||
|
<span>{search.input_type}</span>
|
||||||
|
{#if search.profile}
|
||||||
|
<span class="badge badge-outline badge-sm font-semibold">{search.profile}</span>
|
||||||
|
{/if}
|
||||||
|
<span>· started {fmtDate(search.started_at)}</span>
|
||||||
|
{#if search.status !== "running" && totalResults > 0}
|
||||||
|
<span>· <span class="text-base-content/70 font-medium">{totalResults} result{totalResults !== 1 ? "s" : ""}</span></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 shrink-0">
|
||||||
|
<button class="btn btn-sm btn-ghost gap-1" onclick={refresh}>
|
||||||
|
<RefreshCw size={14} /> Refresh
|
||||||
|
</button>
|
||||||
|
{#if search.status === "running"}
|
||||||
|
<button class="btn btn-sm btn-error btn-outline gap-1" onclick={cancel}>
|
||||||
|
<X size={14} /> Cancel
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if search.planned_tools?.length > 0}
|
||||||
|
<div class="card bg-base-200 shadow mb-6">
|
||||||
|
<div class="card-body p-4 gap-3">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between flex-wrap gap-2">
|
||||||
|
<h2 class="text-xs uppercase tracking-widest text-base-content/50">Tools</h2>
|
||||||
|
<div class="flex gap-3 text-sm">
|
||||||
|
<span class="text-success font-mono">
|
||||||
|
{toolProgress.done.length}/{toolProgress.active.length} done
|
||||||
|
</span>
|
||||||
|
{#if toolProgress.skippedTotal > 0}
|
||||||
|
<span class="text-warning font-mono">
|
||||||
|
{toolProgress.skippedTotal} skipped
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-2 bg-base-300 rounded-full overflow-hidden flex relative">
|
||||||
|
<div class="h-full bg-success transition-all duration-500" style="width:{toolProgress.active.length > 0 ? (toolProgress.done.length - toolProgress.errored.length) / toolProgress.active.length * 100 : 0}%"></div>
|
||||||
|
<div class="h-full bg-warning/70 transition-all duration-500" style="width:{toolProgress.active.length > 0 ? toolProgress.errored.length / toolProgress.active.length * 100 : 0}%"></div>
|
||||||
|
{#if search.status === "running"}
|
||||||
|
<div class="shimmer absolute inset-0 pointer-events-none"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="group">
|
||||||
|
<summary
|
||||||
|
class="flex items-center gap-1 cursor-pointer text-xs text-base-content/40
|
||||||
|
hover:text-base-content/70 transition-colors list-none select-none w-fit"
|
||||||
|
>
|
||||||
|
<ChevronRight size={12} class="transition-transform duration-200 group-open:rotate-90" />
|
||||||
|
Show tools
|
||||||
|
</summary>
|
||||||
|
<div class="flex flex-wrap gap-2 pt-3">
|
||||||
|
{#each search.planned_tools as t}
|
||||||
|
{@const d = grouped[t.name]}
|
||||||
|
{@const isErrored = d?.done && d.errors.length > 0 && d.output.length === 0}
|
||||||
|
{#if t.skipped}
|
||||||
|
<div class="tooltip" data-tip={t.reason}>
|
||||||
|
<span class="badge badge-warning badge-sm font">{t.name}</span>
|
||||||
|
</div>
|
||||||
|
{:else if isErrored}
|
||||||
|
<div class="tooltip" data-tip={d.errors[0] ?? "error"}>
|
||||||
|
<span class="badge badge-warning badge-sm">{t.name}</span>
|
||||||
|
</div>
|
||||||
|
{:else if d?.done}
|
||||||
|
<a href="/tools/{t.name}" class="badge badge-success badge-sm hover:badge-outline transition-all gap-1">
|
||||||
|
<Check size={10} />{t.name}
|
||||||
|
</a>
|
||||||
|
{:else if search.status === "running"}
|
||||||
|
<a href="/tools/{t.name}" class="badge badge-ghost badge-sm hover:badge-outline transition-all">
|
||||||
|
<span class="loading loading-ring loading-xs mr-1"></span>{t.name}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a href="/tools/{t.name}" class="badge badge-ghost badge-sm hover:badge-outline transition-all">
|
||||||
|
{t.name}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{#if demo}
|
||||||
|
<p class="text-xs text-base-content/40 italic">Results shown are not exhaustive — demo mode only displays a subset of what the tools can find.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if Object.keys(grouped).length === 0 && search.status === "running"}
|
||||||
|
<p class="text-base-content/40 text-sm text-center py-8">Waiting for results...</p>
|
||||||
|
{:else if Object.keys(grouped).length === 0}
|
||||||
|
<p class="text-base-content/40 text-sm text-center py-8">No results.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
|
||||||
|
{#each sortedEntries.withResults as [toolName, data]}
|
||||||
|
{@render toolCard(toolName, data)}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each sortedEntries.running as [toolName, data]}
|
||||||
|
{@render toolCard(toolName, data)}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if sortedEntries.noResults.length > 0}
|
||||||
|
<details class="group">
|
||||||
|
<summary
|
||||||
|
class="flex items-center gap-2 cursor-pointer select-none list-none
|
||||||
|
text-sm text-base-content/40 hover:text-base-content/60 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight size={14} class="transition-transform duration-200 group-open:rotate-90" />
|
||||||
|
No results
|
||||||
|
<span class="font-mono text-xs">({sortedEntries.noResults.length})</span>
|
||||||
|
</summary>
|
||||||
|
<div class="flex flex-col gap-3 pt-3">
|
||||||
|
{#each sortedEntries.noResults as [toolName, data]}
|
||||||
|
{@render toolCard(toolName, data)}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#snippet toolCard(toolName, data)}
|
||||||
|
{@const toolStatus = search.planned_tools?.find((t) => t.name === toolName)}
|
||||||
|
{@const resultCount = data.output.length > 0 ? (toolStatus?.result_count ?? null) : null}
|
||||||
|
<div class="card bg-base-200 shadow">
|
||||||
|
<details class="group" open>
|
||||||
|
<summary class="card-body gap-3 p-4 cursor-pointer list-none">
|
||||||
|
<div class="flex items-center gap-3 flex-wrap">
|
||||||
|
<ChevronRight size={14} class="shrink-0 opacity-40 transition-transform duration-200 group-open:rotate-90" />
|
||||||
|
<a href="/tools/{toolName}" class="font-bold hover:underline underline-offset-2" onclick={(e) => e.stopPropagation()}>
|
||||||
|
{toolName}
|
||||||
|
</a>
|
||||||
|
{#if !data.done}
|
||||||
|
<span class="badge badge-warning badge-sm">
|
||||||
|
<span class="loading loading-ring loading-xs mr-1"></span>running
|
||||||
|
</span>
|
||||||
|
{:else if data.errors.length > 0 && data.output.length === 0}
|
||||||
|
<span class="badge badge-error badge-sm">error</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-success badge-sm">done</span>
|
||||||
|
{/if}
|
||||||
|
<span class="text-xs text-base-content/40 ml-auto">
|
||||||
|
{#if resultCount === null}
|
||||||
|
output
|
||||||
|
{:else}
|
||||||
|
{resultCount} result{resultCount !== 1 ? "s" : ""}
|
||||||
|
{/if}
|
||||||
|
{#if data.errors.length > 0}
|
||||||
|
· <span class="text-error">{data.errors.length} error{data.errors.length !== 1 ? "s" : ""}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="px-4 pb-4 flex flex-col gap-3">
|
||||||
|
{#each data.errors as err}
|
||||||
|
<div class="alert alert-error py-2 text-sm gap-2"><AlertTriangle size={14} class="shrink-0" />{err}</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if data.output.length > 0}
|
||||||
|
<TtyOutput output={data.output} />
|
||||||
|
{:else if data.done && data.errors.length === 0}
|
||||||
|
<p class="text-sm text-base-content/40">No results.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if toast}
|
||||||
|
<div class="toast toast-end toast-bottom z-50">
|
||||||
|
<div class="alert {toast.type} shadow-lg">
|
||||||
|
<span>{toast.msg}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.shimmer {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(255, 255, 255, 0.18) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
animation: shimmer 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
from { transform: translateX(-100%); }
|
||||||
|
to { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
front/src/components/SearchList.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script>
|
||||||
|
import { INPUT_TYPE_ICON } from "@src/lib/vars";
|
||||||
|
import { FileText, X } from "@lucide/svelte";
|
||||||
|
|
||||||
|
let { searches = [], onDelete = async () => {} } = $props();
|
||||||
|
|
||||||
|
const STATUS_BADGE = {
|
||||||
|
running: "badge-warning",
|
||||||
|
done: "badge-success",
|
||||||
|
cancelled: "badge-error",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function fmtDate(iso) {
|
||||||
|
return new Date(iso).toLocaleString(undefined, {
|
||||||
|
month: "short", day: "numeric",
|
||||||
|
hour: "2-digit", minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if searches.length === 0}
|
||||||
|
<p class="text-base-content/40 text-sm text-center py-8">No searches yet. Run one above.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each searches as s (s.id)}
|
||||||
|
<a
|
||||||
|
href={`/search/${s.id}`}
|
||||||
|
class="card bg-base-200 hover:bg-base-300 transition-colors shadow-sm cursor-pointer"
|
||||||
|
>
|
||||||
|
<div class="card-body flex-row items-center gap-4 py-3 px-4">
|
||||||
|
<div class="text-base-content/40 w-6 flex items-center justify-center shrink-0">
|
||||||
|
{#each [INPUT_TYPE_ICON[s.input_type] ?? FileText] as Icon}
|
||||||
|
<Icon size={16} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col min-w-0 flex-1">
|
||||||
|
<span class="font-mono font-semibold truncate">{s.target}</span>
|
||||||
|
<div class="flex items-center gap-1.5 flex-wrap text-xs text-base-content/50">
|
||||||
|
<span>{s.input_type}</span>
|
||||||
|
{#if s.profile}
|
||||||
|
<span class="badge badge-outline badge-xs font-semibold">{s.profile}</span>
|
||||||
|
{/if}
|
||||||
|
<span>· {fmtDate(s.started_at)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if s.status !== "running"}
|
||||||
|
{@const total = (s.planned_tools ?? []).reduce((sum, t) => sum + (t.result_count ?? 0), 0)}
|
||||||
|
{#if total > 0}
|
||||||
|
<span class="text-xs font-mono text-base-content/50 shrink-0">{total} result{total !== 1 ? "s" : ""}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="badge {STATUS_BADGE[s.status] ?? 'badge-ghost'} badge-sm shrink-0">
|
||||||
|
{#if s.status === "running"}
|
||||||
|
<span class="loading loading-ring loading-xs mr-1"></span>
|
||||||
|
{/if}
|
||||||
|
{s.status}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs text-base-content/30 hover:text-error shrink-0"
|
||||||
|
onclick={(e) => { e.preventDefault(); onDelete(s.id); }}
|
||||||
|
title="Delete"
|
||||||
|
><X size={14} /></button>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
265
front/src/components/ToolDetail.svelte
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Save, Trash2, AlertTriangle, Package } from "@lucide/svelte";
|
||||||
|
import ToolIcon from "./comps/ToolIcon.svelte";
|
||||||
|
|
||||||
|
let { name = null } = $props();
|
||||||
|
|
||||||
|
let resolvedName = $state("");
|
||||||
|
let tool = $state(null);
|
||||||
|
let error = $state("");
|
||||||
|
let edits = $state({});
|
||||||
|
let saving = $state(false);
|
||||||
|
let msg = $state(null);
|
||||||
|
let hasGlobalConfig = $state(false);
|
||||||
|
let configReadonly = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
resolvedName = name ?? window.location.pathname.replace(/^\/tools\//, "").replace(/\/$/, "");
|
||||||
|
try {
|
||||||
|
const [toolRes, cfgRes] = await Promise.all([
|
||||||
|
fetch(`/api/tools/${encodeURIComponent(resolvedName)}`),
|
||||||
|
fetch("/api/config"),
|
||||||
|
]);
|
||||||
|
if (!toolRes.ok) {
|
||||||
|
const body = await toolRes.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error || `HTTP ${toolRes.status}`);
|
||||||
|
}
|
||||||
|
tool = await toolRes.json();
|
||||||
|
const cfg = cfgRes.ok ? await cfgRes.json() : { tools: {} };
|
||||||
|
configReadonly = cfg.readonly ?? false;
|
||||||
|
const curMap = cfg.tools?.[resolvedName] ?? {};
|
||||||
|
hasGlobalConfig = !!cfg.tools?.[resolvedName];
|
||||||
|
|
||||||
|
const next = {};
|
||||||
|
for (const f of tool.config_fields ?? []) {
|
||||||
|
const saved = curMap[f.name];
|
||||||
|
next[f.name] = saved !== undefined && saved !== null
|
||||||
|
? saved
|
||||||
|
: (f.value !== undefined && f.value !== null ? f.value : (f.default ?? defaultForType(f.type)));
|
||||||
|
}
|
||||||
|
edits = next;
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function defaultForType(type) {
|
||||||
|
if (type === "bool") return false;
|
||||||
|
if (type === "int" || type === "float") return 0;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
saving = true;
|
||||||
|
msg = null;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/config/tools/${encodeURIComponent(resolvedName)}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(edits),
|
||||||
|
});
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
|
||||||
|
hasGlobalConfig = true;
|
||||||
|
msg = { ok: true, text: "Saved" };
|
||||||
|
} catch (e) {
|
||||||
|
msg = { ok: false, text: e.message };
|
||||||
|
} finally {
|
||||||
|
saving = false;
|
||||||
|
setTimeout(() => (msg = null), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearConfig() {
|
||||||
|
if (!confirm(`Clear global config for "${resolvedName}"?`)) return;
|
||||||
|
try {
|
||||||
|
await fetch(`/api/config/tools/${encodeURIComponent(resolvedName)}`, { method: "DELETE" });
|
||||||
|
hasGlobalConfig = false;
|
||||||
|
const next = {};
|
||||||
|
for (const f of tool.config_fields ?? []) next[f.name] = f.default ?? defaultForType(f.type);
|
||||||
|
edits = next;
|
||||||
|
msg = { ok: true, text: "Cleared" };
|
||||||
|
} catch (e) {
|
||||||
|
msg = { ok: false, text: e.message };
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => (msg = null), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="alert alert-error gap-3"><AlertTriangle size={18} class="shrink-0" />{error}</div>
|
||||||
|
|
||||||
|
{:else if !tool}
|
||||||
|
<div class="flex justify-center py-16">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-6 max-w-2xl">
|
||||||
|
|
||||||
|
{#if tool.available === false}
|
||||||
|
<div class="alert alert-error gap-3">
|
||||||
|
<AlertTriangle size={18} class="shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-sm">Tool unavailable</p>
|
||||||
|
{#if tool.unavailable_reason}
|
||||||
|
<p class="text-sm opacity-80">{tool.unavailable_reason}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<ToolIcon iconName={tool.icon} size={32} />
|
||||||
|
<div class="pl-4">
|
||||||
|
<h1 class="text-2xl sm:text-3xl font-bold mb-1">{tool.name}</h1>
|
||||||
|
{#if tool.description}
|
||||||
|
<p class="text-base-content/60">{tool.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if tool.link}
|
||||||
|
<a href={tool.link} target="_blank" rel="noopener noreferrer" class="btn btn-ghost btn-sm gap-1">
|
||||||
|
↗ source
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body gap-3 p-4">
|
||||||
|
<h2 class="text-xs uppercase tracking-widest text-base-content/50">Accepted input types</h2>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each tool.input_types as t}
|
||||||
|
<span class="badge badge-outline border-base-content/20">{t}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if tool.dependencies?.length > 0}
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body gap-3 p-4">
|
||||||
|
<h2 class="text-xs uppercase tracking-widest text-base-content/50 flex items-center gap-2">
|
||||||
|
<Package size={13} /> External dependencies
|
||||||
|
</h2>
|
||||||
|
<ul class="flex flex-col gap-1">
|
||||||
|
{#each tool.dependencies as dep}
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-sm bg-base-300 px-2 py-0.5 rounded">{dep}</span>
|
||||||
|
<span class="text-xs text-base-content/40">must be in <code>$PATH</code></span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if tool.config_fields?.length > 0}
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body gap-4 p-4">
|
||||||
|
<div class="flex items-center justify-between flex-wrap gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h2 class="text-xs uppercase tracking-widest text-base-content/50">Global config</h2>
|
||||||
|
{#if hasGlobalConfig}
|
||||||
|
<span class="badge badge-outline badge-xs">configured</span>
|
||||||
|
{/if}
|
||||||
|
{#if configReadonly}
|
||||||
|
<span class="badge badge-ghost badge-xs">read-only</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if msg}
|
||||||
|
<span class="text-xs {msg.ok ? 'text-success' : 'text-error'}">{msg.text}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each tool.config_fields as field}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<span class="font-mono font-semibold text-sm">{field.name}</span>
|
||||||
|
<span class="badge badge-ghost badge-xs">{field.type}</span>
|
||||||
|
{#if field.required}
|
||||||
|
<span class="badge badge-error badge-xs">required</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if field.description}
|
||||||
|
<p class="text-sm text-base-content/60">{field.description}</p>
|
||||||
|
{/if}
|
||||||
|
<div class="text-xs text-base-content/40 font-mono mb-1">
|
||||||
|
default: {field.default ?? "-"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if field.type === "bool"}
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-sm toggle-primary"
|
||||||
|
bind:checked={edits[field.name]}
|
||||||
|
disabled={configReadonly}
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-base-content/50">
|
||||||
|
{edits[field.name] ? "enabled" : "disabled"}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{:else if field.type === "int"}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
class="input input-bordered input-sm font-mono w-full max-w-48"
|
||||||
|
bind:value={edits[field.name]}
|
||||||
|
/>
|
||||||
|
{:else if field.type === "float"}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
class="input input-bordered input-sm font-mono w-full max-w-48"
|
||||||
|
bind:value={edits[field.name]}
|
||||||
|
/>
|
||||||
|
{:else if field.type === "enum"}
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-sm font-mono w-full max-w-xs"
|
||||||
|
bind:value={edits[field.name]}
|
||||||
|
>
|
||||||
|
{#each field.options as opt}
|
||||||
|
<option value={opt}>{opt}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm font-mono w-full max-w-xs"
|
||||||
|
bind:value={edits[field.name]}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if !configReadonly}
|
||||||
|
<div class="flex gap-2 pt-1 flex-wrap">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm gap-1"
|
||||||
|
onclick={save}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{#if saving}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Save size={14} />
|
||||||
|
{/if}
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{#if hasGlobalConfig}
|
||||||
|
<button class="btn btn-ghost btn-sm gap-1 text-error" onclick={clearConfig}>
|
||||||
|
<Trash2 size={14} /> Reset to defaults
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
346
front/src/components/ToolList.svelte
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { AlertTriangle } from "@lucide/svelte";
|
||||||
|
import Select from "./comps/Select.svelte";
|
||||||
|
import { INPUT_TYPES } from "@src/lib/vars";
|
||||||
|
import ToolIcon from "./comps/ToolIcon.svelte";
|
||||||
|
|
||||||
|
let tools = $state([]);
|
||||||
|
let config = $state({ tools: {}, profiles: {} });
|
||||||
|
let profileSummaries = $state([]);
|
||||||
|
let selectedProfile = $state("default");
|
||||||
|
let profileDetail = $state(null);
|
||||||
|
let loading = $state(true);
|
||||||
|
let profileLoading = $state(false);
|
||||||
|
let error = $state("");
|
||||||
|
|
||||||
|
let selectedInputType = $state("all");
|
||||||
|
|
||||||
|
const inputTypeOptions = ["all", ...INPUT_TYPES];
|
||||||
|
|
||||||
|
let profileOptions = $derived(profileSummaries.map((p) => p.name));
|
||||||
|
|
||||||
|
let activeSet = $derived(
|
||||||
|
profileDetail
|
||||||
|
? new Set(profileDetail.active_tools ?? [])
|
||||||
|
: new Set(tools.map((t) => t.name)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let globalToolConf = $derived(config.tools ?? {});
|
||||||
|
let profileOverrides = $derived(profileDetail?.tools ?? {});
|
||||||
|
|
||||||
|
let toolsWithStatus = $derived(
|
||||||
|
tools.map((tool) => {
|
||||||
|
const isActive = activeSet.has(tool.name);
|
||||||
|
const effective = {
|
||||||
|
...(globalToolConf[tool.name] ?? {}),
|
||||||
|
...(profileOverrides[tool.name] ?? {}),
|
||||||
|
};
|
||||||
|
const missingConfig = (tool.config_fields ?? []).some((f) => {
|
||||||
|
if (!f.required) return false;
|
||||||
|
const v = effective[f.name];
|
||||||
|
return v === undefined || v === null || v === "";
|
||||||
|
});
|
||||||
|
const unavailable = tool.available === false;
|
||||||
|
return { ...tool, isActive, missingConfig, unavailable };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let visibleTools = $derived(
|
||||||
|
selectedInputType === "all"
|
||||||
|
? toolsWithStatus
|
||||||
|
: toolsWithStatus.filter((t) =>
|
||||||
|
t.input_types.includes(selectedInputType),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let active = $derived(
|
||||||
|
visibleTools.filter(
|
||||||
|
(t) => t.isActive && !t.missingConfig && !t.unavailable,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let activeMissing = $derived(
|
||||||
|
visibleTools.filter((t) => t.isActive && t.missingConfig && !t.unavailable),
|
||||||
|
);
|
||||||
|
let activeUnavail = $derived(
|
||||||
|
visibleTools.filter((t) => t.isActive && t.unavailable),
|
||||||
|
);
|
||||||
|
let inactive = $derived(
|
||||||
|
visibleTools.filter(
|
||||||
|
(t) => !t.isActive && !t.missingConfig && !t.unavailable,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let inactiveMissing = $derived(
|
||||||
|
visibleTools.filter(
|
||||||
|
(t) => !t.isActive && t.missingConfig && !t.unavailable,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let inactiveUnavail = $derived(
|
||||||
|
visibleTools.filter((t) => !t.isActive && t.unavailable),
|
||||||
|
);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const [tr, cr, pr] = await Promise.all([
|
||||||
|
fetch("/api/tools"),
|
||||||
|
fetch("/api/config"),
|
||||||
|
fetch("/api/config/profiles"),
|
||||||
|
]);
|
||||||
|
if (!tr.ok) throw new Error(`HTTP ${tr.status}`);
|
||||||
|
if (!cr.ok) throw new Error(`HTTP ${cr.status}`);
|
||||||
|
if (!pr.ok) throw new Error(`HTTP ${pr.status}`);
|
||||||
|
tools = await tr.json();
|
||||||
|
config = await cr.json();
|
||||||
|
profileSummaries = await pr.json();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function selectProfile(name) {
|
||||||
|
selectedProfile = name;
|
||||||
|
profileLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/config/profiles/${encodeURIComponent(name)}`,
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
profileDetail = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
profileLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet toolCard(tool, missing)}
|
||||||
|
<div
|
||||||
|
class="card bg-base-200 group-hover:bg-base-300 transition-colors shadow-sm h-full
|
||||||
|
{missing ? 'border border-warning/40' : ''}
|
||||||
|
{tool.unavailable ? 'border border-error/40' : ''}"
|
||||||
|
>
|
||||||
|
<div class="card-body p-4 flex-row items-start gap-0">
|
||||||
|
<div
|
||||||
|
class="size-10 rounded-lg bg-base-300 group-hover:bg-base-200 transition-colors
|
||||||
|
flex items-center justify-center shrink-0 mr-3 mt-0.5"
|
||||||
|
>
|
||||||
|
<ToolIcon iconName={tool.icon} size={20} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col min-w-0 flex-1 gap-1.5">
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<span class="font-bold text-sm leading-tight">{tool.name}</span>
|
||||||
|
{#if tool.unavailable}
|
||||||
|
<span class="badge badge-error badge-xs gap-1">
|
||||||
|
<AlertTriangle size={9} /> unavailable
|
||||||
|
</span>
|
||||||
|
{:else if missing}
|
||||||
|
<span class="badge badge-warning badge-xs gap-1">
|
||||||
|
<AlertTriangle size={9} /> config required
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if tool.unavailable && tool.unavailable_reason}
|
||||||
|
<p class="text-xs text-error/70 leading-relaxed">
|
||||||
|
{tool.unavailable_reason}
|
||||||
|
</p>
|
||||||
|
{:else if tool.description}
|
||||||
|
<p class="text-xs text-base-content/50 line-clamp-2 leading-relaxed">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#each tool.input_types as t}
|
||||||
|
<span class="badge badge-xs badge-outline border-base-content/20">{t}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="flex justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-md"></span>
|
||||||
|
</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="alert alert-error gap-3"><AlertTriangle size={18} class="shrink-0" />{error}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-wrap items-center gap-x-6 gap-y-3 mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
class="text-xs uppercase tracking-widest text-base-content/50 shrink-0"
|
||||||
|
>Profile</span
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={profileOptions}
|
||||||
|
selected={selectedProfile}
|
||||||
|
onselect={selectProfile}
|
||||||
|
/>
|
||||||
|
{#if profileLoading}
|
||||||
|
<span class="loading loading-spinner loading-xs opacity-40"></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
class="text-xs uppercase tracking-widest text-base-content/50 shrink-0"
|
||||||
|
>Input</span
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={inputTypeOptions}
|
||||||
|
selected={selectedInputType}
|
||||||
|
onselect={(val) => (selectedInputType = val)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if tools.length === 0}
|
||||||
|
<p class="text-base-content/40 text-sm text-center py-8">
|
||||||
|
No tools registered.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
{#if active.length > 0}
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="size-1.5 rounded-full bg-success shrink-0"></span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-base-content/50"
|
||||||
|
>Active</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-base-content/30">{active.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{#each active as tool}
|
||||||
|
<a href="/tools/{tool.name}" class="group">
|
||||||
|
{@render toolCard(tool, false)}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if activeMissing.length > 0}
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="size-1.5 rounded-full bg-warning shrink-0"></span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-base-content/50"
|
||||||
|
>Active - required config missing</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-base-content/30"
|
||||||
|
>{activeMissing.length}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{#each activeMissing as tool}
|
||||||
|
<a href="/tools/{tool.name}" class="group">
|
||||||
|
{@render toolCard(tool, true)}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if activeUnavail.length > 0}
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="size-1.5 rounded-full bg-error shrink-0"></span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-error/70"
|
||||||
|
>Active - unavailable</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-base-content/30"
|
||||||
|
>{activeUnavail.length}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{#each activeUnavail as tool}
|
||||||
|
<a href="/tools/{tool.name}" class="group">
|
||||||
|
{@render toolCard(tool, false)}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if active.length + activeMissing.length + activeUnavail.length > 0 && inactive.length + inactiveMissing.length + inactiveUnavail.length > 0}
|
||||||
|
<div class="divider opacity-20 my-0"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if inactive.length > 0}
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="size-1.5 rounded-full bg-base-content/20 shrink-0"
|
||||||
|
></span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-base-content/30"
|
||||||
|
>Disabled</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-base-content/20">{inactive.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 opacity-40">
|
||||||
|
{#each inactive as tool}
|
||||||
|
<a
|
||||||
|
href="/tools/{tool.name}"
|
||||||
|
class="group hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
{@render toolCard(tool, false)}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if inactiveMissing.length > 0}
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="size-1.5 rounded-full bg-base-content/20 shrink-0"
|
||||||
|
></span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-base-content/30"
|
||||||
|
>Disabled - required config missing</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-base-content/20"
|
||||||
|
>{inactiveMissing.length}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 opacity-40">
|
||||||
|
{#each inactiveMissing as tool}
|
||||||
|
<a
|
||||||
|
href="/tools/{tool.name}"
|
||||||
|
class="group hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
{@render toolCard(tool, true)}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if inactiveUnavail.length > 0}
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="size-1.5 rounded-full bg-base-content/20 shrink-0"
|
||||||
|
></span>
|
||||||
|
<span class="text-xs uppercase tracking-widest text-base-content/30"
|
||||||
|
>Disabled - unavailable</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-base-content/20"
|
||||||
|
>{inactiveUnavail.length}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 opacity-40">
|
||||||
|
{#each inactiveUnavail as tool}
|
||||||
|
<a
|
||||||
|
href="/tools/{tool.name}"
|
||||||
|
class="group hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
{@render toolCard(tool, false)}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
29
front/src/components/comps/Badge.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
let { text, color = null, icon: IconComponent = null, size = "sm", loading = false } = $props();
|
||||||
|
|
||||||
|
const colorDefaults = {
|
||||||
|
"done": "badge-success",
|
||||||
|
"error": "badge-error",
|
||||||
|
"unavailable": "badge-error",
|
||||||
|
"running": "badge-warning",
|
||||||
|
"cancelled": "badge-error",
|
||||||
|
"required": "badge-error",
|
||||||
|
"read-only": "badge-ghost",
|
||||||
|
"active": "badge-success",
|
||||||
|
"disabled": "badge-ghost",
|
||||||
|
"configured": "badge-outline",
|
||||||
|
"configurable": "badge-outline",
|
||||||
|
"config required":"badge-warning",
|
||||||
|
};
|
||||||
|
|
||||||
|
let cls = $derived(color ?? colorDefaults[text?.toLowerCase()] ?? "badge-ghost");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="badge badge-{size} {cls} gap-1">
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-ring loading-xs"></span>
|
||||||
|
{:else if IconComponent}
|
||||||
|
<IconComponent size={9} />
|
||||||
|
{/if}
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
13
front/src/components/comps/InfoTip.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { CircleQuestionMark } from "@lucide/svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
tooltip,
|
||||||
|
}: {
|
||||||
|
tooltip: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="tooltip" data-tip={tooltip}>
|
||||||
|
<CircleQuestionMark class="size-3 text-base-content/40 align-middle"/>
|
||||||
|
</div>
|
||||||
84
front/src/components/comps/Select.svelte
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script>
|
||||||
|
import { ChevronDown } from "@lucide/svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
options = [],
|
||||||
|
placeholder = "",
|
||||||
|
selected = null,
|
||||||
|
onselect,
|
||||||
|
size = "sm", // "xs" | "sm" | ""
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let query = $state("");
|
||||||
|
let container;
|
||||||
|
let input = $state();
|
||||||
|
|
||||||
|
let filtered = $derived(options.filter((o) =>
|
||||||
|
o.toLowerCase().includes(query.toLowerCase())
|
||||||
|
));
|
||||||
|
|
||||||
|
function select(value) {
|
||||||
|
onselect?.(value);
|
||||||
|
open = false;
|
||||||
|
query = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
open = !open;
|
||||||
|
if (open) setTimeout(() => input?.focus(), 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onmousedown={(e) => {
|
||||||
|
if (container && !container.contains(e.target)) {
|
||||||
|
open = false;
|
||||||
|
query = "";
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
|
||||||
|
<div class="relative" bind:this={container}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-{size} gap-1 font-normal"
|
||||||
|
onclick={toggle}
|
||||||
|
>
|
||||||
|
<ChevronDown size={11} />
|
||||||
|
{selected ?? placeholder}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
class="absolute z-50 top-full left-0 mt-1 w-52 bg-base-300 rounded-box shadow-xl border border-base-content/10 flex flex-col"
|
||||||
|
>
|
||||||
|
<div class="p-2 border-b border-base-content/10">
|
||||||
|
<input
|
||||||
|
bind:this={input}
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-xs w-full"
|
||||||
|
placeholder="Search..."
|
||||||
|
bind:value={query}
|
||||||
|
onkeydown={(e) => { if (e.key === "Escape") { open = false; query = ""; } }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul class="max-h-48 overflow-y-auto p-1">
|
||||||
|
{#if filtered.length === 0}
|
||||||
|
<li class="px-3 py-2 text-xs text-base-content/40 text-center">No results</li>
|
||||||
|
{:else}
|
||||||
|
{#each filtered as option}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full text-left px-3 py-1.5 text-sm font-mono rounded-btn transition-colors
|
||||||
|
{option === selected ? 'bg-primary/15 text-primary font-semibold' : 'hover:bg-base-content/10'}"
|
||||||
|
onclick={() => select(option)}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
29
front/src/components/comps/ToolIcon.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const { iconName = "", size=16 }: { iconName: string , size: number} = $props();
|
||||||
|
|
||||||
|
const genericFallbackUrl = "/Wrench.svg";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if iconName}
|
||||||
|
<img
|
||||||
|
src="https://cdn.simpleicons.org/{iconName}"
|
||||||
|
alt={iconName + " icon"}
|
||||||
|
class="opacity-50"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style="filter: brightness(0) invert(1);"
|
||||||
|
onerror={(e) => {
|
||||||
|
const target = e.currentTarget as HTMLImageElement;
|
||||||
|
target.src = genericFallbackUrl;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<img
|
||||||
|
src={genericFallbackUrl}
|
||||||
|
alt={"Tool icon"}
|
||||||
|
class="opacity-50"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style="filter: brightness(0) invert(1);"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
63
front/src/components/comps/TtyOutput.svelte
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script>
|
||||||
|
import { AnsiUp } from "ansi_up";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
|
let { output } = $props();
|
||||||
|
|
||||||
|
const au = new AnsiUp();
|
||||||
|
au.use_classes = false;
|
||||||
|
|
||||||
|
const ansiRe = /\x1b\[[0-9;]*m/g;
|
||||||
|
const urlRe = /https?:\/\/[^\s<>"']+/g;
|
||||||
|
const tlds = "com|org|net|io|fr|de|uk|co|info|biz|eu|us|ca|au|ru|cn|jp|br|in|es|dev|app|me|tv|cc|ch|be|nl|se|no|dk|fi|pl|cz|at|hu|ro";
|
||||||
|
const bareDomainRe = new RegExp(`(?<![a-zA-Z0-9@])[a-zA-Z0-9][a-zA-Z0-9\\-]*(?:\\.[a-zA-Z0-9][a-zA-Z0-9\\-]*)*\\.(?:${tlds})(?:/[^\\s<>"']*)?`, "g");
|
||||||
|
|
||||||
|
const makeLink = (href, text) =>
|
||||||
|
`<a href="${href}" target="_blank" rel="noopener noreferrer" class="ansi-link">${text}</a>`;
|
||||||
|
|
||||||
|
function linkifyText(text) {
|
||||||
|
// First pass: full URLs
|
||||||
|
const afterUrls = text.replace(urlRe, (url) => makeLink(url, url));
|
||||||
|
// Re-split to protect the newly created <a> tags, then linkify bare domains in remaining text
|
||||||
|
return afterUrls.split(/(<[^>]+>)/).map((part) => {
|
||||||
|
if (part.startsWith("<")) return part;
|
||||||
|
return part.replace(bareDomainRe, (domain) => makeLink(`https://${domain}`, domain));
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkify(html) {
|
||||||
|
return html.split(/(<[^>]+>)/).map((part) =>
|
||||||
|
part.startsWith("<") ? part : linkifyText(part)
|
||||||
|
).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = $derived((() => {
|
||||||
|
const lines = output.split("\n");
|
||||||
|
let start = 0;
|
||||||
|
let end = lines.length - 1;
|
||||||
|
while (start <= end && lines[start].replace(ansiRe, "").trim() === "") start++;
|
||||||
|
while (end >= start && lines[end].replace(ansiRe, "").trim() === "") end--;
|
||||||
|
return DOMPurify.sanitize(linkify(au.ansi_to_html(lines.slice(start, end + 1).join("\n"))), {
|
||||||
|
ALLOWED_TAGS: ["span", "a", "b"],
|
||||||
|
ALLOWED_ATTR: ["style", "href", "target", "rel", "class"],
|
||||||
|
});
|
||||||
|
})());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if html}
|
||||||
|
<div class="ansi-output">{@html html}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ansi-output {
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.ansi-output :global(.ansi-link) {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.ansi-output :global(.ansi-link:hover) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
front/src/content.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
import { glob } from "astro/loaders";
|
||||||
|
|
||||||
|
const cheatsheets = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.md", base: "./src/content/cheatsheets" }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
order: z.number().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { cheatsheets };
|
||||||
134
front/src/content/cheatsheets/github-osint.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
title: "Unmasking Github Users: How to Identify the Person Behind Any Github Profile"
|
||||||
|
description: "Ever wondered who is behind a specific Github username? This guide covers advanced OSINT techniques to deanonymize users, find hidden email addresses, and link Github accounts to real-world identities."
|
||||||
|
tags: [github, social]
|
||||||
|
---
|
||||||
|
|
||||||
|
In the world of Open-Source Intelligence (OSINT), we often focus on social media platforms like Twitter or LinkedIn. However, developers frequently leave behind much more detailed personal information on **Github**.
|
||||||
|
|
||||||
|
Whether you are a recruiter, a security researcher, or a digital investigator, Github is a goldmine. Why? Because while a user might choose a cryptic handle like `anotherhadi`, their Git configuration often reveals their real name and email address.
|
||||||
|
|
||||||
|
## Level 1: The Low-Hanging Fruit
|
||||||
|
|
||||||
|
Before diving into technical exploits, start with the obvious. Many users forget how much they have shared in their profile settings.
|
||||||
|
|
||||||
|
- **The Bio & Location**: Even a vague location like "Montpellier, France," combined with a niche tech stack (e.g., "COBOL expert"), significantly narrows down the search.
|
||||||
|
- **External Links**: Check the personal website or blog link. Run a WHOIS lookup on that domain to find registration details. Use other OSINT tools and techniques on those websites to pivot further.
|
||||||
|
- **The Profile Picture**: Right-click the avatar and use Google Reverse Image Search, Yandex, or other reverse image engines. Developers often use the same professional headshot on Github as they do on LinkedIn.
|
||||||
|
|
||||||
|
## Level 2: Digging into Commits
|
||||||
|
|
||||||
|
This is the **most effective OSINT** method. While Github masks author names and emails in the web view, this information is permanently embedded in the commit metadata.
|
||||||
|
|
||||||
|
### The `.patch` Method
|
||||||
|
|
||||||
|
Find a repository where the target has contributed. Open any commit they made, and simply add `.patch` to the end of the URL.
|
||||||
|
|
||||||
|
- **URL**: `https://github.com/{username}/{repo}/commit/{commit_hash}.patch`
|
||||||
|
- Look at the `From:` line. It should look like this: `From: John Doe <j.doe@company.com>`
|
||||||
|
|
||||||
|
For example, check: [github.com/anotherhadi/nixy/commit/e6873e8caae491073d8ab7daad9d2e50a04490ce.patch](https://github.com/anotherhadi/nixy/commit/e6873e8caae491073d8ab7daad9d2e50a04490ce.patch)
|
||||||
|
|
||||||
|
### The API Events Method
|
||||||
|
|
||||||
|
If you cannot find a recent commit, check their **public activity** stream via the Github API.
|
||||||
|
|
||||||
|
- **Go to**: `https://api.github.com/users/{target_username}/events/public`
|
||||||
|
- Search (Ctrl+F) for the word `email`. You will often find the **email address** associated with their `PushEvent` headers, even if they have "Keep my email addresses private" enabled in their current settings.
|
||||||
|
|
||||||
|
## The Verification Loop: Linking Email to Account
|
||||||
|
|
||||||
|
If you have found an email address and want to be 100% sure it belongs to a specific Github profile, you can use Github’s own attribution engine against itself.
|
||||||
|
|
||||||
|
### The Email Spoofing Method
|
||||||
|
|
||||||
|
While the previous methods help you find an email _from_ a profile, this technique does the opposite: it identifies which Github account is linked to a specific email address.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
Github attributes commits based on the email address found in the Git metadata. If you push a commit using a specific email, Github will automatically link that commit to the account associated with that address as its **primary email**.
|
||||||
|
|
||||||
|
**The Process:**
|
||||||
|
|
||||||
|
1. **Initialize a local repo:** `git init investigation`
|
||||||
|
2. **Configure the target email:** `git config user.email "target@example.com"` and `git config user.name "A Username"`
|
||||||
|
3. **Create a dummy commit:** `echo "test" > probe.txt && git add . && git commit -m "Probe"`
|
||||||
|
4. **Push to a repo you own:** Create a new empty repository on your Github account and push the code there.
|
||||||
|
5. **Observe the result:** Go to the commit history on the Github web interface. The avatar and username of the account linked to that email will appear as the author of the commit.
|
||||||
|
|
||||||
|
> **Note:** This method only works if the target email is set as the **Primary Email** on the user's account. It is a foolproof way to confirm if an email address you found elsewhere belongs to a specific Github user.
|
||||||
|
|
||||||
|
### The Search Index: Finding Hidden Contributions
|
||||||
|
|
||||||
|
Even if an email address is not listed on a user's profile, it may still be indexed within Github's global search.
|
||||||
|
Github allows you to filter search results by the metadata fields of a commit.
|
||||||
|
This is particularly useful if the target has **contributed to public repositories** using their real email.
|
||||||
|
|
||||||
|
You can use these specific qualifiers in the **Github search bar** (select the "Commits" tab):
|
||||||
|
|
||||||
|
- `author-email:target@example.com`: Finds commits where the target is the original author.
|
||||||
|
- `committer-email:target@example.com`: Finds commits where the target was the one who committed the code (sometimes different from the author).
|
||||||
|
|
||||||
|
## Level 3: Technical Metadata
|
||||||
|
|
||||||
|
If the email is masked or missing, we can look at the **cryptographic keys** the user uses to communicate with Github.
|
||||||
|
|
||||||
|
### SSH Keys
|
||||||
|
|
||||||
|
Every user’s public **SSH keys are public**.
|
||||||
|
|
||||||
|
- **URL**: `https://github.com/{username}.keys`
|
||||||
|
- **The Pivot**: You can take the key string and search for it on platforms like **Censys** or **Shodan**. If that same key is authorized on a specific server IP, you have successfully located the user’s infrastructure.
|
||||||
|
|
||||||
|
### GPG Keys
|
||||||
|
|
||||||
|
If a user signs their commits, their **GPG key** is available at:
|
||||||
|
|
||||||
|
- **URL**: `https://github.com/{username}.gpg`
|
||||||
|
- **The Reveal**: Import this key into your local GPG tool (`gpg --import`). It will often reveal the **Verified Identity** and the primary email address linked to the encryption key.
|
||||||
|
|
||||||
|
## Level 4: Connecting the Dots
|
||||||
|
|
||||||
|
Once you have a **name**, an **email**, or a **unique username**, it’s time to _pivot_.
|
||||||
|
|
||||||
|
- **Username Pivoting**: Use tools like [Sherlock](https://github.com/sherlock-project/sherlock) or [Maigret](https://github.com/soxoj/maigret/) to search for the same username across hundreds of other platforms. Developers are creatures of habit; they likely use the same handle on Stack Overflow, Reddit, or even old gaming forums.
|
||||||
|
- **Email Pivoting**: Use tools like [holehe](https://github.com/megadose/holehe) to find other accounts registered with the email addresses you just uncovered.
|
||||||
|
|
||||||
|
## Automating the Hunt: Github-Recon
|
||||||
|
|
||||||
|
If you want to move from manual investigation to automated intelligence, check out [Github-Recon](https://github.com/anotherhadi/github-recon).
|
||||||
|
Written in Go, this powerful CLI tool aggregates public OSINT data by automating the techniques mentioned above and more. Whether you start with a username or a single email address, it can retrieve SSH/GPG keys, enumerate social accounts, and find "close friends" based on interactions.
|
||||||
|
Its standout features include a **Deep Scan** mode-which clones repositories to perform regex searches and TruffleHog secret detection—and an automated **Email Spoofing** engine that instantly identifies the account linked to any primary email address.
|
||||||
|
|
||||||
|
<a href="https://github.com/anotherhadi/github-recon" class="link-card" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>anotherhadi/github-recon</h4>
|
||||||
|
<p>GitHub OSINT reconnaissance tool. Gathers profile info, social links, organisations, SSH/GPG keys, commits, and more from a GitHub username or email.</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Conclusion and Protection: How to Stay Anonymous
|
||||||
|
|
||||||
|
If you are a developer reading this, you might be feeling exposed.
|
||||||
|
Understanding what information about you is publicly visible is the **first step to managing your online presence**. This guide and tools like [github-recon](https://github.com/anotherhadi/github-recon) can help you identify your own publicly available data on Github. Here’s how you can take steps to protect your privacy and security:
|
||||||
|
|
||||||
|
- **Review your public profile**: Regularly check your Github profile and
|
||||||
|
repositories to ensure that you are not unintentionally exposing sensitive
|
||||||
|
information.
|
||||||
|
- **Manage email exposure**: Use Github's settings to control which email
|
||||||
|
addresses are visible on your profile and in commit history. You can also **use
|
||||||
|
a no-reply email** address for commits, and an
|
||||||
|
[alias email](https://proton.me/support/addresses-and-aliases) for your
|
||||||
|
account. Delete/modify any sensitive information in your commit history.
|
||||||
|
- **Be Mindful of Repository Content**: **Avoid including sensitive information** in
|
||||||
|
your repositories, such as API keys, passwords, emails or personal data. Use
|
||||||
|
`.gitignore` to exclude files that contain sensitive information.
|
||||||
|
|
||||||
|
You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog)
|
||||||
|
to scan your repositories specifically for exposed secrets and tokens.
|
||||||
|
|
||||||
|
**Useful links:**
|
||||||
|
|
||||||
|
- [Blocking command line pushes that expose your personal email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/blocking-command-line-pushes-that-expose-your-personal-email-address)
|
||||||
|
- [No-reply email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
|
||||||
|
|
||||||
|
In OSINT, the best hidden secrets are the ones we forget we ever shared. Happy hunting!
|
||||||
100
front/src/content/cheatsheets/google-dorks.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
title: "Google Dorks"
|
||||||
|
description: "Essential cheatsheet for Google Dorking, using advanced search operators to perform Open Source Intelligence (OSINT) and identify publicly exposed information or misconfigurations on target websites."
|
||||||
|
tags: [google, dorks]
|
||||||
|
---
|
||||||
|
|
||||||
|
[Google](https://google.com) hacking, also named Google dorking, is a hacker technique that uses Google Search and other Google applications to find security holes in the configuration and computer code that websites are using.
|
||||||
|
Dorks also works on [Startpage](https://startpage.com) or [Duckduckgo](https://duckduckgo.com).
|
||||||
|
|
||||||
|
## Basics
|
||||||
|
|
||||||
|
- `-` excludes a term
|
||||||
|
- `OR` searches for either term
|
||||||
|
- `""` searches for an exact phrase
|
||||||
|
- `*` acts as a wildcard
|
||||||
|
- `site:` restricts the search to a specific domain
|
||||||
|
- `inurl:` restricts the search to a specific URL
|
||||||
|
- `intitle:` restricts the search to a specific title
|
||||||
|
- `intext:` restricts the search to a specific text
|
||||||
|
- `allintext:` restricts the search to all text
|
||||||
|
- `filetype:` restricts the search to a specific file type
|
||||||
|
|
||||||
|
## Information gathering
|
||||||
|
|
||||||
|
Replace "{target}" with a name or other identifiers used online. Always remember
|
||||||
|
to use these queries solely for legal and ethical purposes on information you
|
||||||
|
own or have permission to check.
|
||||||
|
|
||||||
|
- **File Types:**
|
||||||
|
- `"{target}" filetype:pdf`
|
||||||
|
- `"{target}" filetype:doc OR filetype:docx OR filetype:xls OR filetype:ppt`
|
||||||
|
- Config files:
|
||||||
|
`site:{target}+filetype:xml+|+filetype:conf+|+filetype:cnf+|+filetype:reg+|+filetype:inf+|+filetype:rdp+|+filetype:cfg+|+filetype:txt+|+filetype:ora+|+filetype:ini`
|
||||||
|
- Database files: `site:{target}+filetype:sql+|+filetype:dbf+|+filetype:mdb`
|
||||||
|
- Data files: `site:{target} ext:csv OR ext:xls OR ext:log` or `site:{target} "@gmail.com" ext:csv`
|
||||||
|
- Log files: `site:{target}+filetype:log+|filetype:txt` - Backup files:
|
||||||
|
`site:{target}+filetype:bkf+|+filetype:bkp+|+filetype:bak+|+filetype:old+|+filetype:backup`
|
||||||
|
- Setup files:
|
||||||
|
`site:{target}+inurl:readme+|+inurl:license+|+inurl:install+|+inurl:setup+|+inurl:config`
|
||||||
|
- Private files:
|
||||||
|
`site:{target} "internal use only" ( you can replace with "classified", "private", "unauthorised" )`
|
||||||
|
- Sensitive docs:
|
||||||
|
`ext:txt | ext:pdf | ext:xml | ext:xls | ext:xlsx | ext:ppt | ext:pptx | ext:doc | ext:docx intext:“confidential” | intext:“Not for Public Release” | intext:”internal use only” | intext:“do not distribute” site:{target}`
|
||||||
|
- Code leaks: Check for code snippets, secrets, configs
|
||||||
|
```txt
|
||||||
|
site:pastebin.com "{target}"
|
||||||
|
site:jsfiddle.net "{target}"
|
||||||
|
site:codebeautify.org "{target}"
|
||||||
|
site:codepen.io "{target}"`
|
||||||
|
```
|
||||||
|
- Cloud File Shares: Find exposed files linked to your target
|
||||||
|
```txt
|
||||||
|
site:http://drive.google.com "{target}"
|
||||||
|
site:http://docs.google.com inurl:"/d/" "{target}"
|
||||||
|
site:http://dropbox.com/s "{target}"
|
||||||
|
```
|
||||||
|
- Other: `site:{target}+filetype:pdf+|+filetype:xlsx+|+filetype:docx`
|
||||||
|
|
||||||
|
- **Social Media & Professional Networks:**
|
||||||
|
- `site:linkedin.com/in "{target}"`
|
||||||
|
- `site:facebook.com "{target}"`
|
||||||
|
- `site:twitter.com "{target}"`
|
||||||
|
- `site:instagram.com "{target}"`
|
||||||
|
|
||||||
|
- **Profile & Resume Searches:**
|
||||||
|
- `inurl:"profile" "{target}"`
|
||||||
|
- `intitle:"{target}" "profile"`
|
||||||
|
- `"{target}" intext:"resume"`
|
||||||
|
- `intitle:"Curriculum Vitae" OR intitle:"CV" "{target}"`
|
||||||
|
|
||||||
|
- **Email and Contact Information:**
|
||||||
|
- `"{target}" intext:"@gmail.com"`
|
||||||
|
- `"{target}" intext:"email"`
|
||||||
|
- `"{target}" AND "contact"`
|
||||||
|
|
||||||
|
- **Forums and Public Repositories:**
|
||||||
|
- `site:pastebin.com "{target}"`
|
||||||
|
- `site:github.com "{target}"`
|
||||||
|
- `site:forums "{target}"`
|
||||||
|
|
||||||
|
- **Directory Listings and Miscellaneous:**
|
||||||
|
- `site:{target}+intitle:index.of`,
|
||||||
|
|
||||||
|
- **Exclusion Searches:**
|
||||||
|
- `"{target}" -site:facebook.com`
|
||||||
|
- `"{target}" -site:twitter.com`
|
||||||
|
|
||||||
|
## Advanced Google Operators
|
||||||
|
|
||||||
|
- `related:site` finds websites similar to the specified URL
|
||||||
|
- `define:term` shows a word or phrase definition directly in the results
|
||||||
|
- `inanchor:word` filters pages where the anchor text includes the specified
|
||||||
|
word
|
||||||
|
- `around(n)` restricts results to pages where two words appear within _n_ words
|
||||||
|
of each other
|
||||||
|
|
||||||
|
## Ressources
|
||||||
|
|
||||||
|
- [TakSec's google dorks](https://github.com/TakSec/google-dorks-bug-bounty/)
|
||||||
|
- [Exploit-db Google hacking database](https://www.exploit-db.com/google-hacking-database)
|
||||||
61
front/src/content/cheatsheets/sock-puppets.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: "Sock Puppets"
|
||||||
|
description: "Essential cheatsheet on creating and managing Sock Puppets (fake identities) for ethical security research and Open Source Intelligence (OSINT), focusing on maintaining separation from personal data and bypassing common verification."
|
||||||
|
tags: [sock-puppets]
|
||||||
|
---
|
||||||
|
|
||||||
|
Sock puppets are fake identities use to gather information from a target.
|
||||||
|
The sock puppet should have no link between your personal information and the fakes ones. (No ip address, mail, follow, etc..)
|
||||||
|
|
||||||
|
## Information generation
|
||||||
|
|
||||||
|
<a href="https://fakerjs.dev" class="link-card not-prose" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>Faker</h4>
|
||||||
|
<p>Generate massive amounts of fake data</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://fakenamegenerator.com/" class="link-card not-prose" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>Fake Name</h4>
|
||||||
|
<p>Personal informations</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://www.thispersondoesnotexist.com/" class="link-card not-prose" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>This Person Does Not Exist</h4>
|
||||||
|
<p>Generate fake image</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Bypass phone verification
|
||||||
|
|
||||||
|
<a href="https://www.smspool.net/" class="link-card not-prose" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>SMSPool</h4>
|
||||||
|
<p>Cheapest and Fastest Online SMS verification</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://onlinesim.io/" class="link-card not-prose" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>Online Sim</h4>
|
||||||
|
<p>SMS verification with free tier</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://sms4stats.com/" class="link-card not-prose" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>Sms 4 Sats</h4>
|
||||||
|
<p>Paid SMS verification</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="http://sms4sat6y7lkq4vscloomatwyj33cfeddukkvujo2hkdqtmyi465spid.onion" class="link-card not-prose" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>Sms 4 Sats (Onion)</h4>
|
||||||
|
<p>Paid SMS verification. Tor version</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
23
front/src/content/cheatsheets/tips.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "Tips"
|
||||||
|
description: "A cheatsheet of practical tips and unconventional methods for Open Source Intelligence (OSINT), focusing on advanced data visualization, information leakage detection, and utilizing web archives for historical data."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Visualisation
|
||||||
|
|
||||||
|
Use [OSINTracker](https://app.osintracker.com/) to visualise your findings.
|
||||||
|
It allows you to create a graph of your findings, which can help you see connections and relationships between different pieces of information.
|
||||||
|
|
||||||
|
## Forgotten passwords
|
||||||
|
|
||||||
|
To find email addresses and phone numbers associated with an account, you can click on "Forgot password?" on the login page of a website. Be careful, though, this creates notifications and can be detected by the target, and often gives your information away.
|
||||||
|
|
||||||
|
## Archive Search
|
||||||
|
|
||||||
|
- [Wayback Machine](https://web.archive.org) stores over 618 billion web captures
|
||||||
|
- [Archive.today](https://archive.ph) creates on-demand snapshots, including for JS-heavy sites, with both a functional page and screenshot version
|
||||||
|
|
||||||
|
## Bookmarklets
|
||||||
|
|
||||||
|
- [K2SOsint/Bookmarklets](https://github.com/K2SOsint/Bookmarklets)
|
||||||
|
- [MyOsint.training](https://tools.myosint.training/)
|
||||||
88
front/src/content/cheatsheets/x-twitter-osint.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: "Twitter/X OSINT"
|
||||||
|
description: "Essential cheatsheet for Open Source Intelligence (OSINT) on Twitter/X, detailing advanced search operators, engagement filters, and temporal/geographic capabilities for effective data collection."
|
||||||
|
tags: [social]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Banner last update time
|
||||||
|
|
||||||
|
The banner URL includes a Unix timestamp indicating when the banner was last
|
||||||
|
updated.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
`https://pbs.twimg.com/profile_banners/1564326938851921921/1750897704/600x200`
|
||||||
|
|
||||||
|
In this case, `1750897704` is the timestamp. You can convert it using
|
||||||
|
[unixtimestamp.com](https://www.unixtimestamp.com/) or any other Unix time converter.
|
||||||
|
|
||||||
|
## Basic Search Operators
|
||||||
|
|
||||||
|
Twitter's advanced search functionality provides powerful filtering capabilities
|
||||||
|
for OSINT investigations:
|
||||||
|
|
||||||
|
- **Keywords**: `word1 word2` (tweets containing both words)
|
||||||
|
- **Exact phrases**: `"exact phrase"` (tweets with this exact sequence)
|
||||||
|
- **Exclusion**: `-word` (excludes tweets containing this word)
|
||||||
|
- **Either/or**: `word1 OR word2` (tweets containing either term)
|
||||||
|
- **Hashtags**: `#hashtag` (tweets with specific hashtag)
|
||||||
|
- **Accounts**: `from:username` (tweets sent by specific account)
|
||||||
|
- **Mentions**: `to:username` (tweets in reply to an account)
|
||||||
|
- **Mentions in any context**: `@username` (tweets mentioning an account)
|
||||||
|
|
||||||
|
## Advanced Filters
|
||||||
|
|
||||||
|
<a href="https://x.com/search-advanced" class="link-card" target="_blank">
|
||||||
|
<span>
|
||||||
|
<h4>Twitter/X Search advanced GUI</h4>
|
||||||
|
<p>Graphical User Interface (GUI) for the twitter search advanced functionality</p>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### Engagement Filters
|
||||||
|
|
||||||
|
- **Minimum retweets**: `min_retweets:number`
|
||||||
|
- **Minimum likes**: `min_faves:number`
|
||||||
|
- **Minimum replies**: `min_replies:number`
|
||||||
|
- **Filter for links**: `filter:links`
|
||||||
|
- **Filter for media**: `filter:media`
|
||||||
|
- **Filter for images**: `filter:images`
|
||||||
|
- **Filter for videos**: `filter:videos`
|
||||||
|
|
||||||
|
### Temporal and Geographic Filters
|
||||||
|
|
||||||
|
- **Date range**: `since:YYYY-MM-DD until:YYYY-MM-DD`
|
||||||
|
- **Geolocation**: `geocode:latitude,longitude,radius` (e.g.,
|
||||||
|
`geocode:40.7128,-74.0060,5km`)
|
||||||
|
- **Language**: `lang:code` (e.g., `lang:en` for English)
|
||||||
|
|
||||||
|
### Tweet Characteristics
|
||||||
|
|
||||||
|
- **Positive attitude**: `🙂 OR :) OR filter:positive`
|
||||||
|
- **Negative attitude**: `🙁 OR :( OR filter:negative`
|
||||||
|
- **Questions**: `?` or `filter:questions`
|
||||||
|
- **Retweets only**: `filter:retweets`
|
||||||
|
- **Native retweets only**: `filter:nativeretweets`
|
||||||
|
- **Twitter Blue subscribers**: `filter:verified` (note: since 2023, "verified" means Twitter Blue subscriber, not a traditionally verified account)
|
||||||
|
- **Safe content**: `filter:safe`
|
||||||
|
|
||||||
|
## Practical Search Combinations
|
||||||
|
|
||||||
|
- **Content from a user within a date range**:
|
||||||
|
`from:username since:2023-01-01 until:2023-12-31`
|
||||||
|
|
||||||
|
- **High-engagement tweets about a topic**:
|
||||||
|
`"artificial intelligence" min_retweets:100 lang:en -filter:retweets`
|
||||||
|
|
||||||
|
- **Media shared by a specific user**:
|
||||||
|
`from:username filter:media -filter:retweets`
|
||||||
|
|
||||||
|
- **Conversations between specific users**:
|
||||||
|
`from:username1 to:username2 OR from:username2 to:username1`
|
||||||
|
|
||||||
|
- **Link sharing on a topic by verified users**:
|
||||||
|
`"climate change" filter:links filter:verified since:2023-01-01`
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Remember that all Twitter searches should comply with Twitter's Terms of Service
|
||||||
|
and appropriate legal frameworks for your jurisdiction.
|
||||||
92
front/src/layouts/Layout.astro
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
import "@src/styles/global.css";
|
||||||
|
import "@src/styles/gfm.css";
|
||||||
|
import "@src/styles/markdown.css";
|
||||||
|
import Navbar from "@src/components/Nav.svelte";
|
||||||
|
import DemoBanner from "@src/components/DemoBanner.svelte";
|
||||||
|
import { Coffee } from "@lucide/svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = "iknowyou",
|
||||||
|
description = "Self-hosted OSINT aggregation. Run multiple recon tools against a target in parallel and get results in one place.",
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const pageTitle = title === "iknowyou" ? title : `${title} — iky`;
|
||||||
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? Astro.url.origin);
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content={pageTitle} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:site_name" content="iknowyou" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-base-100 min-h-screen">
|
||||||
|
<DemoBanner client:only="svelte" />
|
||||||
|
<Navbar client:load>
|
||||||
|
<a
|
||||||
|
href="https://ko-fi.com/anotherhadi"
|
||||||
|
slot="action"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
><Coffee class="size-3" /> Support me</a
|
||||||
|
>
|
||||||
|
</Navbar>
|
||||||
|
<div class="m-auto max-w-5xl md:py-10 md:px-10 py-5 px-5 animate-fade-in">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
||||||
|
const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
||||||
|
|
||||||
|
document.querySelectorAll<HTMLElement>("pre[data-lang]").forEach((pre) => {
|
||||||
|
const lang = pre.dataset.lang ?? "text";
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "code-block";
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "code-header";
|
||||||
|
|
||||||
|
const langSpan = document.createElement("span");
|
||||||
|
langSpan.className = "code-lang";
|
||||||
|
langSpan.textContent = lang;
|
||||||
|
|
||||||
|
const copyBtn = document.createElement("button");
|
||||||
|
copyBtn.className = "copy-btn";
|
||||||
|
copyBtn.innerHTML = `${COPY_ICON} copy`;
|
||||||
|
copyBtn.addEventListener("click", () => {
|
||||||
|
const code = pre.querySelector("code");
|
||||||
|
if (!code) return;
|
||||||
|
navigator.clipboard.writeText(code.innerText).then(() => {
|
||||||
|
copyBtn.classList.add("copied");
|
||||||
|
copyBtn.innerHTML = `${CHECK_ICON} copied!`;
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.classList.remove("copied");
|
||||||
|
copyBtn.innerHTML = `${COPY_ICON} copy`;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
header.appendChild(langSpan);
|
||||||
|
header.appendChild(copyBtn);
|
||||||
|
wrapper.appendChild(header);
|
||||||
|
pre.parentNode!.insertBefore(wrapper, pre);
|
||||||
|
wrapper.appendChild(pre);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
17
front/src/lib/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function cleanUserInput(query: string | undefined | null): string {
|
||||||
|
if (!query) return "";
|
||||||
|
return query.replace(/[^a-zA-Z0-9\s.\-_/]/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomEmoji(): string {
|
||||||
|
const emojis = [
|
||||||
|
"(·.·)",
|
||||||
|
"(>_<)",
|
||||||
|
"¯\\_(ツ)_/¯",
|
||||||
|
"(╯_╰)",
|
||||||
|
"(-_-)",
|
||||||
|
"┐(‘~`;)┌",
|
||||||
|
"(X_X)",
|
||||||
|
];
|
||||||
|
return emojis[Math.floor(Math.random() * emojis.length)];
|
||||||
|
}
|
||||||
16
front/src/lib/vars.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Mail, User, Phone, Globe, Server, KeyRound, Contact } from "@lucide/svelte";
|
||||||
|
|
||||||
|
export const INPUT_TYPES = [
|
||||||
|
"email", "username", "name", "phone", "ip",
|
||||||
|
"domain", "password",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INPUT_TYPE_ICON = {
|
||||||
|
email: Mail,
|
||||||
|
username: User,
|
||||||
|
name: Contact,
|
||||||
|
phone: Phone,
|
||||||
|
domain: Globe,
|
||||||
|
ip: Server,
|
||||||
|
password: KeyRound,
|
||||||
|
};
|
||||||
30
front/src/pages/403.astro
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@src/layouts/Layout.astro";
|
||||||
|
import { ShieldAlert, Home } from "@lucide/svelte";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="403 - Access Denied">
|
||||||
|
<main
|
||||||
|
class="flex flex-col items-center justify-center gap-6 px-4 text-center"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center mt-20">
|
||||||
|
<ShieldAlert size={80} class="text-warning" />
|
||||||
|
<h1 class="text-4xl font-black text-warning opacity-50 mb-10">403</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-bold logo-gradient italic">Access Denied</h2>
|
||||||
|
<p class="opacity-60 max-w-xs mx-auto mt-2">
|
||||||
|
You don't have the necessary clearance to access this sector of the app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badge badge-outline badge-warning font-mono text-xs p-3">
|
||||||
|
ERROR_CODE: INSUFFICIENT_PERMISSIONS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="btn btn-soft btn-warning gap-2">
|
||||||
|
<Home size={16} /> Return to Surface
|
||||||
|
</a>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
26
front/src/pages/404.astro
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@src/layouts/Layout.astro";
|
||||||
|
import { Ghost, Home } from "@lucide/svelte";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="404 - Page Not Found">
|
||||||
|
<main
|
||||||
|
class="flex flex-col items-center justify-center gap-6 px-4 text-center"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center mt-20">
|
||||||
|
<Ghost size={80} class="text-primary" />
|
||||||
|
<h1 class="text-4xl font-black text-primary opacity-50 mb-10">404</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-bold logo-gradient italic">Lost?</h2>
|
||||||
|
<p class="opacity-60 max-w-xs mx-auto mt-2">
|
||||||
|
The page you are looking for doesn't exist or has been moved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="btn btn-soft btn-primary gap-2">
|
||||||
|
<Home size={16} /> Back to Home
|
||||||
|
</a>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
32
front/src/pages/500.astro
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@src/layouts/Layout.astro";
|
||||||
|
import { AlertTriangle, RefreshCw } from "@lucide/svelte";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="500 - Server Error">
|
||||||
|
<main
|
||||||
|
class="flex flex-col items-center justify-center gap-6 px-4 text-center"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center mt-20">
|
||||||
|
<AlertTriangle size={80} class="text-error" />
|
||||||
|
<h1 class="text-4xl font-black text-error opacity-50 mb-10">500</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-bold logo-gradient italic">System Failure</h2>
|
||||||
|
<p class="opacity-60 max-w-xs mx-auto mt-2">
|
||||||
|
The server encountered an unexpected error.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button
|
||||||
|
onclick="window.location.reload()"
|
||||||
|
class="btn btn-soft btn-error gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} /> Retry
|
||||||
|
</button>
|
||||||
|
<a href="/" class="btn btn-soft btn-primary">Go Home</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||