Update asset caching and documentation features
All checks were successful
CI / build (push) Successful in 1m8s
renovate / renovate (push) Successful in 1m42s

- Updated the API server to support asset caching with a new flag for enabling/disabling caching.
- Implemented asset caching logic in the DownloadProxyHandler to store and retrieve assets efficiently.
- Added tests for asset caching functionality, ensuring proper behavior for cache hits and misses.
- Introduced new documentation files for software, including multi-language support.
- Enhanced the SoftwareCard component to display documentation links for software with available docs.
- Updated the Software model to include a flag indicating the presence of documentation.
- Improved the user interface for documentation navigation and search functionality.
This commit is contained in:
2025-12-27 19:08:36 -06:00
parent 5b8daa638d
commit d8748bba77
30 changed files with 1392 additions and 93 deletions

View File

@@ -15,25 +15,27 @@ A software distribution platform for assets built and hosted on Gitea. Built wit
- **Throttling**: Tiered download speed limits and global API rate limiting.
- **RSS Feed**: XML feed for tracking new software releases.
- **i18n**: Support for English, German, Italian, and Russian.
- **Documentation**: Support for documentation for software station and each software project in `.svx` and `.md` files.
## Upcoming
- S3, SFTP, WebDAV for software assets.
- Admin panel.
- Authentication for certain software and containers.
- Automatic torrent generation and seeding for software assets.
- CDN support.
- Container scanning.
- Gitea Packages support (containers, npm, etc.).
- ISOs support (Linux distributions)
- Automatic Torrent generation and seeding for software assets.
- Software dependencies page and licenses information.
- SBOM and SPDX viewer.
- CDN support
- OSV integration for vulnerability scanning.
- Container scanning
- Authentication for certain software/containers
- Admin panel
- GPG and SBOM client-side verification via WASM.
- Infisical support for secrets management.
- Sqlite for database
- Webhook support to force refresh of specific software/containers or add a new software/container.
- Reticulum Network Stack support
- GPG, SBOM client-side verification via WASM.
- ISOs support (Linux distributions).
- OSV integration for vulnerability scanning.
- Reticulum Network Stack support.
- S3, SFTP, and WebDAV support for software assets.
- SBOM and SPDX viewer.
- Software dependencies page and license information.
- Sqlite database support.
- Webhook support to force refresh or add specific software/containers.
- Software caching for popular assets.
## Getting Started

View File

@@ -21,6 +21,7 @@
Copy,
Check,
Calendar,
BookOpen,
} from 'lucide-svelte';
import type { Software, Asset } from '$lib/types';
import VerificationModal from './VerificationModal.svelte';
@@ -94,7 +95,20 @@
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getOSIcon(os: string) {
function getOSIcon(os: string, assetName?: string) {
if (assetName) {
const lowerName = assetName.toLowerCase();
if (lowerName.endsWith('.whl')) {
return 'python';
}
if (
lowerName.endsWith('.zip') ||
lowerName.endsWith('.tar.gz') ||
lowerName.endsWith('.tgz')
) {
return 'zip';
}
}
switch (os.toLowerCase()) {
case 'macos':
return 'apple';
@@ -216,6 +230,15 @@
/>
</a>
{/if}
{#if software.has_docs}
<a
href="/docs/software/{software.name}"
class="p-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-primary"
title="Documentation"
>
<BookOpen class="w-5 h-5" />
</a>
{/if}
</div>
</div>
<div class="flex items-center gap-2 mb-2">
@@ -328,7 +351,7 @@
</div>
<div class="space-y-1">
{#each filteredAssets as asset}
{@const brand = getOSIcon(asset.os)}
{@const brand = getOSIcon(asset.os, asset.name)}
{@const FallbackIcon = getFallbackIcon(asset.os)}
<div class="group/item flex items-center gap-2">
<a
@@ -393,7 +416,7 @@
>
{#if filteredLatestAssets.length > 0}
{#each filteredLatestAssets as asset}
{@const brand = getOSIcon(asset.os)}
{@const brand = getOSIcon(asset.os, asset.name)}
{@const FallbackIcon = getFallbackIcon(asset.os)}
<div class="group/item flex items-center gap-2">
<a

View File

@@ -19,6 +19,9 @@
'M2.69,2C3.54,1.95 6.08,3.16 6.13,3.19C4.84,4 3.74,5.09 2.91,6.38C2.09,4.81 1.34,2.91 2,2.25C2.17,2.08 2.4,2 2.69,2M20.84,2.13C21.25,2.08 21.58,2.14 21.78,2.34C22.85,3.42 19.88,8.15 19.38,8.66C18.87,9.16 17.57,8.7 16.5,7.63C15.43,6.55 14.97,5.26 15.47,4.75C15.88,4.34 19.09,2.3 20.84,2.13M12,2.56C13.29,2.56 14.53,2.82 15.66,3.28C15.17,3.6 14.81,3.85 14.69,3.97C13.7,4.96 14.14,6.83 15.72,8.41C16.7,9.38 17.84,9.97 18.78,9.97C19.46,9.97 19.92,9.68 20.16,9.44C20.33,9.27 20.6,8.88 20.91,8.41C21.42,9.59 21.69,10.88 21.69,12.25C21.69,17.61 17.36,21.97 12,21.97C6.64,21.97 2.31,17.61 2.31,12.25C2.31,6.89 6.64,2.56 12,2.56Z',
openbsd:
'M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12A6,6 0 0,0 12,6M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8Z',
python:
'M19.14,7.5A2.86,2.86 0 0,1 22,10.36V14.14A2.86,2.86 0 0,1 19.14,17H12C12,17.39 12.32,17.96 12.71,17.96H17V19.64A2.86,2.86 0 0,1 14.14,22.5H9.86A2.86,2.86 0 0,1 7,19.64V15.89C7,14.31 8.28,13.04 9.86,13.04H15.11C16.69,13.04 17.96,11.76 17.96,10.18V7.5H19.14M14.86,19.29C14.46,19.29 14.14,19.59 14.14,20.18C14.14,20.77 14.46,20.89 14.86,20.89A0.71,0.71 0 0,0 15.57,20.18C15.57,19.59 15.25,19.29 14.86,19.29M4.86,17.5C3.28,17.5 2,16.22 2,14.64V10.86C2,9.28 3.28,8 4.86,8H12C12,7.61 11.68,7.04 11.29,7.04H7V5.36C7,3.78 8.28,2.5 9.86,2.5H14.14C15.72,2.5 17,3.78 17,5.36V9.11C17,10.69 15.72,11.96 14.14,11.96H8.89C7.31,11.96 6.04,13.24 6.04,14.82V17.5H4.86M9.14,5.71C9.54,5.71 9.86,5.41 9.86,4.82C9.86,4.23 9.54,4.11 9.14,4.11C8.75,4.11 8.43,4.23 8.43,4.82C8.43,5.41 8.75,5.71 9.14,5.71Z',
zip: 'M14,17H12V15H10V13H12V15H14M14,9H12V11H14V13H12V11H10V9H12V7H10V5H12V7H14M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z',
};
</script>

View File

@@ -0,0 +1,33 @@
# WebNews
WebNews ist ein leistungsstarker terminalbasierter Newsreader, der auf Einfachheit und Geschwindigkeit ausgelegt ist.
## Funktionen
- **Blitzschnell**: In Go geschrieben für optimale Performance.
- **Vim-ähnliche Tastenkombinationen**: Vertraute Steuerung für Terminal-Power-User.
- **Offline-Unterstützung**: Artikel für das Lesen ohne Internetverbindung zwischenspeichern.
- **Anpassbare Themes**: Unterstützung für helle und dunkle Terminal-Farbschemata.
## Installation
Sie können die neueste Binärdatei für Ihr Betriebssystem von der Hauptseite herunterladen.
```bash
# Beispielnutzung
webnews --sync
```
## Konfiguration
Die Konfiguration wird standardmäßig in `~/.config/webnews/config.yaml` gespeichert.
```yaml
sources:
- https://hnrss.org/frontpage
- https://lobste.rs/rss
```
---
_Dies ist eine Test-Dokumentationsdatei, die aus Markdown gerendert wurde._

View File

@@ -0,0 +1,33 @@
# WebNews
WebNews è un lettore di notizie basato su terminale ad alte prestazioni, progettato per semplicità e velocità.
## Caratteristiche
- **Velocissimo**: Scritto in Go per prestazioni ottimali.
- **Scorciatoie in stile Vim**: Controlli familiari per gli utenti esperti del terminale.
- **Supporto Offline**: Memorizza gli articoli nella cache per la lettura senza connessione internet.
- **Temi Personalizzabili**: Supporto per schemi di colori del terminale sia chiari che scuri.
## Installazione
Puoi scaricare l'ultimo binario per il tuo sistema operativo dalla pagina principale.
```bash
# Esempio di utilizzo
webnews --sync
```
## Configurazione
La configurazione è memorizzata in `~/.config/webnews/config.yaml` per impostazione predefinita.
```yaml
sources:
- https://hnrss.org/frontpage
- https://lobste.rs/rss
```
---
_Questo è un file di documentazione di test renderizzato da Markdown._

View File

@@ -0,0 +1,33 @@
# WebNews
WebNews is a high-performance terminal-based news reader designed for simplicity and speed.
## Features
- **Blazing Fast**: Written in Go for optimal performance.
- **Vim-like Keybindings**: Familiar controls for terminal power users.
- **Offline Support**: Cache articles for reading without an internet connection.
- **Customizable Themes**: Support for both light and dark terminal schemes.
## Installation
You can download the latest binary for your operating system from the main page.
```bash
# Example usage
webnews --sync
```
## Configuration
Configuration is stored in `~/.config/webnews/config.yaml` by default.
```yaml
sources:
- https://hnrss.org/frontpage
- https://lobste.rs/rss
```
---
_This is a test documentation file rendered from Markdown._

View File

@@ -0,0 +1,33 @@
# WebNews
WebNews — это высокопроизводительный терминальный агрегатор новостей, разработанный для простоты и скорости.
## Особенности
- **Невероятно быстрый**: Написан на Go для оптимальной производительности.
- **Vim-подобные горячие клавиши**: Привычное управление для продвинутых пользователей терминала.
- **Автономный режим**: Кэширование статей для чтения без подключения к интернету.
- **Настраиваемые темы**: Поддержка светлых и темных схем терминала.
## Установка
Вы можете скачать последнюю версию бинарного файла для вашей операционной системы на главной странице.
```bash
# Пример использования
webnews --sync
```
## Конфигурация
Конфигурация по умолчанию хранится в `~/.config/webnews/config.yaml`.
```yaml
sources:
- https://hnrss.org/frontpage
- https://lobste.rs/rss
```
---
_Это тестовый файл документации, отрисованный из Markdown._

View File

@@ -0,0 +1,204 @@
<script lang="ts">
import {
Shield,
ShieldCheck,
Cpu,
Lock,
Zap,
Globe,
Users,
Code2,
BookOpen,
Terminal,
CheckCircle2,
} from 'lucide-svelte';
</script>
<div class="space-y-16">
<!-- Hero Section -->
<div class="text-center space-y-4">
<div
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary border border-primary/20 text-xs font-bold uppercase tracking-wider"
>
<BookOpen class="w-3 h-3" />
Dokumentation
</div>
<h1 class="text-4xl font-black tracking-tight sm:text-5xl">Wie die Verifizierung funktioniert</h1>
<p class="text-xl text-muted-foreground max-w-2xl mx-auto">
Software Station nutzt modernste Browser-Technologie, um sicherzustellen, dass Ihre Downloads zu 100 % authentisch und unverfälscht sind.
</p>
</div>
<!-- Simple Explainer (For Everyone) -->
<section class="space-y-8 py-8">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-green-500/10 text-green-700 dark:text-green-400">
<ShieldCheck class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold">Die einfache Erklärung</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary"
>
<Lock class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Es ist ein digitales Schloss</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Jede Datei hat einen einzigartigen „digitalen Fingerabdruck“ (SHA256). Bevor Sie eine Datei speichern, berechnet unser Verifizierer diesen Fingerabdruck in Ihrem Browser neu, um sicherzustellen, dass er mit der Originalversion des Entwicklers übereinstimmt.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-700 dark:text-blue-400"
>
<Cpu class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Läuft in einer Sandbox</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Die Verifizierung erfolgt in einer „Sandbox“ einem sicheren, isolierten Bereich in Ihrem Browser. Das bedeutet, dass die Datei auf Sicherheit geprüft wird, bevor sie jemals den permanenten Speicher Ihres Computers berührt.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-700 dark:text-purple-400"
>
<Globe class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Quellen-Verifizierung</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Wir verifizieren die Verbindung zur Quelle mit industrieller Sicherheit (TLS). Dies stellt sicher, dass Sie direkt mit dem echten Repository kommunizieren und nicht mit einem Betrüger.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-orange-500/10 flex items-center justify-center text-orange-700 dark:text-orange-400"
>
<Users class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Kennen Sie die Autoren</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Jede Veröffentlichung zeigt die tatsächlichen Mitwirkenden aus dem Gitea-Repository. Sie können genau sehen, wer den Code geschrieben hat, den Sie gerade herunterladen möchten.
</p>
</div>
</div>
</section>
<!-- Technical Explainer (For Developers) -->
<section
class="space-y-8 p-8 rounded-3xl bg-zinc-100 dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 border border-border dark:border-white/5 shadow-2xl my-8 mb-16"
>
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-primary/20 text-primary">
<Terminal class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold text-zinc-900 dark:text-white">Technische Details</h2>
</div>
<div class="space-y-6">
<div class="space-y-2">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-800 dark:text-white">
<Zap class="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
WebAssembly (WASM) Engine
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
Der Verifizierer ist in **Go** geschrieben und zu **WebAssembly** kompiliert. Durch die Verwendung von WASM anstelle von Standard-JavaScript nutzen wir die <code>crypto/sha256</code>-Implementierung der Go-Standardbibliothek, was hochperformante kryptografische Operationen ermöglicht, die resistent gegen Prototyp-Pollution auf JS-Ebene oder Umgebungsmanipulationen sind.
</p>
</div>
<div class="space-y-2">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-800 dark:text-white">
<Shield class="w-4 h-4 text-blue-600 dark:text-blue-400" />
Zero-Trust-Architektur
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
Software Station arbeitet nach einem Zero-Trust-Modell in Bezug auf die Antwort des Servers. Der Browser ruft den rohen Binärstream in ein <code>Uint8Array</code> ab. Dieser Puffer wird dann direkt in den WASM-Speicherbereich übergeben. Die Hash-Berechnung erfolgt **clientseitig**, was bedeutet, dass selbst wenn der Server kompromittiert wäre und bösartige Binärdateien ausliefern würde, der Verifizierer die Hash-Abweichung sofort erkennen würde.
</p>
</div>
<div class="bg-zinc-200/50 dark:bg-white/5 rounded-xl p-6 space-y-4 border border-zinc-300 dark:border-white/10">
<h4 class="font-bold text-primary flex items-center gap-2">
<Code2 class="w-4 h-4" />
Verifizierungs-Pipeline
</h4>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
1
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Die Binärdatei wird über einen sicheren TLS-Tunnel von der Quelle (Gitea/S3) gestreamt.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
2
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Die WASM-Engine liest den Byte-Stream und berechnet den SHA256-Digest lokal.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
3
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Der berechnete Digest wird mit dem kryptografisch signierten Manifest verglichen.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
4
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Nur bei erfolgreicher Übereinstimmung wird das <code>Blob</code>-Objekt erstellt und der Download ausgelöst.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Future Roadmap -->
<div class="p-8 rounded-3xl bg-primary/5 border border-primary/20 space-y-4 my-8">
<h2 class="text-2xl font-bold flex items-center gap-2">
<Zap class="w-6 h-6 text-primary" />
Demnächst verfügbar
</h2>
<p class="text-muted-foreground">
Wir arbeiten ständig daran, die Transparenz zu verbessern. Unsere nächsten Meilensteine umfassen:
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Automatische SBOM-Generierung</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">S3-Speicher-Verifizierung</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Multi-Signatur-Verifizierung</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Überprüfung reproduzierbarer Builds</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,204 @@
<script lang="ts">
import {
Shield,
ShieldCheck,
Cpu,
Lock,
Zap,
Globe,
Users,
Code2,
BookOpen,
Terminal,
CheckCircle2,
} from 'lucide-svelte';
</script>
<div class="space-y-16">
<!-- Hero Section -->
<div class="text-center space-y-4">
<div
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary border border-primary/20 text-xs font-bold uppercase tracking-wider"
>
<BookOpen class="w-3 h-3" />
Documentazione
</div>
<h1 class="text-4xl font-black tracking-tight sm:text-5xl">Come funziona la verifica</h1>
<p class="text-xl text-muted-foreground max-w-2xl mx-auto">
Software Station utilizza una tecnologia browser all'avanguardia per garantire che i tuoi download siano autentici al 100% e non manomessi.
</p>
</div>
<!-- Simple Explainer (For Everyone) -->
<section class="space-y-8 py-8">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-green-500/10 text-green-700 dark:text-green-400">
<ShieldCheck class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold">La spiegazione semplice</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary"
>
<Lock class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">È una serratura digitale</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Ogni file ha un'impronta digitale unica (SHA256). Prima di salvare un file, il nostro verificatore ricalcola quell'impronta all'interno del tuo browser per assicurarsi che corrisponda alla versione originale dello sviluppatore.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-700 dark:text-blue-400"
>
<Cpu class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Esecuzione in Sandbox</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
La verifica avviene in una "sandbox", un'area sicura e isolata nel tuo browser. Ciò significa che il file viene controllato prima ancora di toccare la memoria permanente del tuo computer.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-700 dark:text-purple-400"
>
<Globe class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Verifica della fonte</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Verifichiamo la connessione alla sorgente utilizzando una sicurezza di livello industriale (TLS). Questo assicura che tu stia parlando direttamente con il vero repository e non con un impostore.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-orange-500/10 flex items-center justify-center text-orange-700 dark:text-orange-400"
>
<Users class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Conosci gli autori</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Ogni rilascio mostra i collaboratori effettivi dal repository Gitea. Puoi vedere esattamente chi ha scritto il codice che stai per scaricare.
</p>
</div>
</div>
</section>
<!-- Technical Explainer (For Developers) -->
<section
class="space-y-8 p-8 rounded-3xl bg-zinc-100 dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 border border-border dark:border-white/5 shadow-2xl my-8 mb-16"
>
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-primary/20 text-primary">
<Terminal class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold text-zinc-900 dark:text-white">Approfondimento tecnico</h2>
</div>
<div class="space-y-6">
<div class="space-y-2">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-800 dark:text-white">
<Zap class="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
Motore WebAssembly (WASM)
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
Il verificatore è scritto in **Go** e compilato in **WebAssembly**. Utilizzando WASM invece del JavaScript standard, sfruttiamo l'implementazione <code>crypto/sha256</code> della libreria standard di Go, garantendo operazioni crittografiche ad alte prestazioni resistenti all'inquinamento dei prototipi a livello JS o alla manipolazione dell'ambiente.
</p>
</div>
<div class="space-y-2">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-800 dark:text-white">
<Shield class="w-4 h-4 text-blue-600 dark:text-blue-400" />
Architettura Zero-Trust
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
Software Station opera su un modello zero-trust per quanto riguarda la risposta del server. Il browser recupera il flusso binario grezzo in un <code>Uint8Array</code>. Questo buffer viene poi passato direttamente nello spazio di memoria WASM. Il calcolo dell'hash viene eseguito **lato client**, il che significa che anche se il server fosse compromesso e servisse binari dannosi, il verificatore rileverebbe immediatamente la mancata corrispondenza dell'hash.
</p>
</div>
<div class="bg-zinc-200/50 dark:bg-white/5 rounded-xl p-6 space-y-4 border border-zinc-300 dark:border-white/10">
<h4 class="font-bold text-primary flex items-center gap-2">
<Code2 class="w-4 h-4" />
Pipeline di verifica
</h4>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
1
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Il binario viene trasmesso in streaming dalla sorgente (Gitea/S3) tramite un tunnel TLS sicuro.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
2
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Il motore WASM legge il flusso di byte e calcola localmente l'hash SHA256.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
3
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
L'hash calcolato viene confrontato con il manifesto firmato crittograficamente.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
4
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Solo in caso di corrispondenza positiva viene creato l'oggetto <code>Blob</code> e attivato il download.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Future Roadmap -->
<div class="p-8 rounded-3xl bg-primary/5 border border-primary/20 space-y-4 my-8">
<h2 class="text-2xl font-bold flex items-center gap-2">
<Zap class="w-6 h-6 text-primary" />
Prossimamente
</h2>
<p class="text-muted-foreground">
Lavoriamo costantemente per migliorare la trasparenza. I nostri prossimi traguardi includono:
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Generazione automatica SBOM</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Verifica dello storage S3</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Verifica Multi-Firma</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Controlli delle build riproducibili</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,204 @@
<script lang="ts">
import {
Shield,
ShieldCheck,
Cpu,
Lock,
Zap,
Globe,
Users,
Code2,
BookOpen,
Terminal,
CheckCircle2,
} from 'lucide-svelte';
</script>
<div class="space-y-16">
<!-- Hero Section -->
<div class="text-center space-y-4">
<div
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary border border-primary/20 text-xs font-bold uppercase tracking-wider"
>
<BookOpen class="w-3 h-3" />
Документация
</div>
<h1 class="text-4xl font-black tracking-tight sm:text-5xl">Как работает верификация</h1>
<p class="text-xl text-muted-foreground max-w-2xl mx-auto">
Software Station использует передовые технологии браузера, чтобы гарантировать 100% подлинность ваших загрузок.
</p>
</div>
<!-- Simple Explainer (For Everyone) -->
<section class="space-y-8 py-8">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-green-500/10 text-green-700 dark:text-green-400">
<ShieldCheck class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold">Простое объяснение</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary"
>
<Lock class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Цифровой замок</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
У каждого файла есть уникальный «цифровой отпечаток» (SHA256). Перед тем как сохранить файл, наш верификатор заново вычисляет этот отпечаток в вашем браузере, чтобы убедиться, что он совпадает с оригинальной версией разработчика.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-700 dark:text-blue-400"
>
<Cpu class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Работа в песочнице</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Верификация происходит в «песочнице» — безопасной изолированной области вашего браузера. Это означает, что файл проверяется на безопасность еще до того, как он попадет в постоянное хранилище вашего компьютера.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-700 dark:text-purple-400"
>
<Globe class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Проверка источника</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Мы проверяем соединение с источником, используя защиту промышленного класса (TLS). Это гарантирует, что вы общаетесь напрямую с реальным репозиторием, а не с самозванцем.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-4 shadow-sm hover:shadow-md transition-shadow">
<div
class="w-10 h-10 rounded-full bg-orange-500/10 flex items-center justify-center text-orange-700 dark:text-orange-400"
>
<Users class="w-5 h-5" />
</div>
<h3 class="font-bold text-lg">Знайте авторов</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
В каждом релизе указаны реальные контрибьюторы из репозитория Gitea. Вы можете точно видеть, кто написал код, который вы собираетесь скачать.
</p>
</div>
</div>
</section>
<!-- Technical Explainer (For Developers) -->
<section
class="space-y-8 p-8 rounded-3xl bg-zinc-100 dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 border border-border dark:border-white/5 shadow-2xl my-8 mb-16"
>
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-primary/20 text-primary">
<Terminal class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold text-zinc-900 dark:text-white">Технические детали</h2>
</div>
<div class="space-y-6">
<div class="space-y-2">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-800 dark:text-white">
<Zap class="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
Движок WebAssembly (WASM)
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
Верификатор написан на **Go** и скомпилирован в **WebAssembly**. Используя WASM вместо стандартного JavaScript, мы задействуем реализацию <code>crypto/sha256</code> из стандартной библиотеки Go, обеспечивая высокую производительность криптографических операций, устойчивых к загрязнению прототипов на уровне JS или манипуляциям с окружением.
</p>
</div>
<div class="space-y-2">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-800 dark:text-white">
<Shield class="w-4 h-4 text-blue-600 dark:text-blue-400" />
Архитектура Zero-Trust
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
Software Station работает по модели «нулевого доверия» в отношении ответов сервера. Браузер загружает необработанный двоичный поток в <code>Uint8Array</code>. Затем этот буфер передается напрямую в пространство памяти WASM. Расчет хэша выполняется на **стороне клиента**, что означает, что даже если сервер будет взломан и начнет выдавать вредоносные файлы, верификатор немедленно обнаружит несовпадение хэша.
</p>
</div>
<div class="bg-zinc-200/50 dark:bg-white/5 rounded-xl p-6 space-y-4 border border-zinc-300 dark:border-white/10">
<h4 class="font-bold text-primary flex items-center gap-2">
<Code2 class="w-4 h-4" />
Конвейер верификации
</h4>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
1
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Бинарный файл передается из источника (Gitea/S3) через защищенный TLS-туннель.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
2
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Движок WASM считывает поток байтов и локально вычисляет дайджест SHA256.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
3
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Вычисленный дайджест сравнивается с манифестом, подписанным криптографически.
</p>
</div>
<div class="flex items-start gap-3">
<div
class="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] font-bold text-zinc-900 dark:text-white"
>
4
</div>
<p class="text-xs text-zinc-700 dark:text-zinc-300">
Только при успешном совпадении создается объект <code>Blob</code> и инициируется загрузка.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Future Roadmap -->
<div class="p-8 rounded-3xl bg-primary/5 border border-primary/20 space-y-4 my-8">
<h2 class="text-2xl font-bold flex items-center gap-2">
<Zap class="w-6 h-6 text-primary" />
Скоро в проекте
</h2>
<p class="text-muted-foreground">
Мы постоянно работаем над улучшением прозрачности. Наши следующие вехи включают:
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Автоматическая генерация SBOM</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Верификация хранилища S3</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Верификация с несколькими подписями</span>
</div>
<div class="flex items-center gap-2 text-sm">
<CheckCircle2 class="w-4 h-4 text-green-700 dark:text-green-400 opacity-80" />
<span class="text-muted-foreground">Проверка воспроизводимости сборок</span>
</div>
</div>
</div>
</div>

View File

@@ -1,4 +1,6 @@
import { init, register, getLocaleFromNavigator } from 'svelte-i18n';
import { init, register, getLocaleFromNavigator, locale } from 'svelte-i18n';
import { browser } from '$app/environment';
import { invalidate } from '$app/navigation';
const defaultLocale = 'en';
@@ -7,7 +9,23 @@ register('de', () => import('./locales/de.json'));
register('ru', () => import('./locales/ru.json'));
register('it', () => import('./locales/it.json'));
const storedLocale = typeof window !== 'undefined' ? localStorage.getItem('locale') : null;
const initialLocale = storedLocale || getLocaleFromNavigator() || defaultLocale;
init({
fallbackLocale: defaultLocale,
initialLocale: getLocaleFromNavigator() || defaultLocale,
initialLocale: initialLocale,
});
let currentLocaleValue = '';
if (browser) {
locale.subscribe((value) => {
if (value && value !== currentLocaleValue) {
currentLocaleValue = value;
localStorage.setItem('locale', value);
document.cookie = `locale=${value}; path=/; max-age=31536000`;
invalidate('app:locale');
}
});
}

View File

@@ -49,4 +49,5 @@ export interface Software {
license?: string;
is_private: boolean;
avatar_url?: string;
has_docs: boolean;
}

View File

@@ -1,12 +1,14 @@
<script lang="ts">
import { page } from '$app/state';
import { BookOpen, Shield, Terminal, Menu, X, Search } from 'lucide-svelte';
import { BookOpen, Shield, Terminal, Menu, X, Search, Package } from 'lucide-svelte';
import { slide } from 'svelte/transition';
import { t } from 'svelte-i18n';
import { onMount } from 'svelte';
import type { Software } from '$lib/types';
let { children } = $props();
const docs = [
const mainDocs = [
{
title: 'Verification',
slug: 'verification',
@@ -23,17 +25,48 @@
},
];
let softwareList = $state<Software[]>([]);
let isMobileMenuOpen = $state(false);
let searchQuery = $state('');
const filteredDocs = $derived(
docs.filter(
onMount(async () => {
try {
const res = await fetch('/api/software');
if (res.ok) {
softwareList = await res.json();
}
} catch (e) {
console.error('Failed to fetch software for docs sidebar', e);
}
});
const softwareDocs = $derived(
softwareList
.filter((sw) => sw.has_docs)
.map((sw) => ({
title: sw.name.replace(/-/g, ' '),
slug: `software/${sw.name}`,
icon: Package,
description: sw.description,
}))
);
const filteredMainDocs = $derived(
mainDocs.filter(
(doc) =>
doc.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
doc.description.toLowerCase().includes(searchQuery.toLowerCase())
)
);
const filteredSoftwareDocs = $derived(
softwareDocs.filter(
(doc) =>
doc.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
(doc.description && doc.description.toLowerCase().includes(searchQuery.toLowerCase()))
)
);
function toggleMobileMenu() {
isMobileMenuOpen = !isMobileMenuOpen;
}
@@ -45,7 +78,7 @@
class="hidden lg:flex flex-col w-72 border-r border-border bg-card/50 backdrop-blur-xl h-full overflow-y-auto"
>
<div class="p-6">
<div class="flex items-center gap-3 mb-8">
<a href="/docs" class="flex items-center gap-3 mb-8 hover:opacity-80 transition-opacity">
<div class="p-2 rounded-xl bg-primary/10 text-primary">
<BookOpen class="w-6 h-6" />
</div>
@@ -55,7 +88,7 @@
Software Station
</p>
</div>
</div>
</a>
<div class="relative mb-6">
<Search
@@ -69,31 +102,50 @@
/>
</div>
<nav class="space-y-1">
{#each filteredDocs as doc}
{#if doc.comingSoon}
<div class="flex items-center gap-3 px-4 py-3 rounded-xl opacity-50 cursor-not-allowed">
<doc.icon class="w-4 h-4" />
<span class="text-sm font-medium">{doc.title}</span>
<span class="ml-auto text-[8px] bg-muted px-1.5 py-0.5 rounded uppercase font-bold"
>Soon</span
>
</div>
{:else}
<nav class="space-y-6">
<div class="space-y-1">
<p
class="px-4 text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2"
>
General
</p>
{#each filteredMainDocs as doc}
<a
href="/docs/{doc.slug}"
class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group {page
.url.pathname === `/docs/${doc.slug}` ||
(page.url.pathname === '/docs' && doc.slug === 'verification')
.url.pathname === `/docs/${doc.slug}`
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
: 'hover:bg-primary/10 text-muted-foreground hover:text-primary'}"
>
<doc.icon class="w-4 h-4 transition-transform duration-200 group-hover:scale-110" />
<span class="text-sm font-medium">{doc.title}</span>
</a>
{/if}
{/each}
{#if filteredDocs.length === 0}
{/each}
</div>
{#if filteredSoftwareDocs.length > 0}
<div class="space-y-1">
<p
class="px-4 text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2"
>
Software
</p>
{#each filteredSoftwareDocs as doc}
<a
href="/docs/{doc.slug}"
class="flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all duration-200 group {page
.url.pathname === `/docs/${doc.slug}`
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
: 'hover:bg-primary/10 text-muted-foreground hover:text-primary'}"
>
<doc.icon class="w-4 h-4 transition-transform duration-200 group-hover:scale-110" />
<span class="text-sm font-medium capitalize">{doc.title}</span>
</a>
{/each}
</div>
{/if}
{#if filteredMainDocs.length === 0 && filteredSoftwareDocs.length === 0}
<p class="text-center py-8 text-xs text-muted-foreground">No documentation found.</p>
{/if}
</nav>
@@ -124,17 +176,28 @@
{#if isMobileMenuOpen}
<div
transition:slide
class="lg:hidden border-b border-border bg-card/50 backdrop-blur-xl z-40 overflow-hidden"
class="lg:hidden border-b border-border bg-card/50 backdrop-blur-xl z-40 overflow-hidden h-[calc(100vh-64px)] overflow-y-auto"
>
<nav class="p-4 space-y-1">
{#each docs as doc}
{#if doc.comingSoon}
<div class="flex items-center gap-3 px-4 py-3 rounded-xl opacity-50">
<doc.icon class="w-4 h-4" />
<span class="text-sm font-medium">{doc.title}</span>
<span class="ml-auto text-[8px] bg-muted px-1.5 py-0.5 rounded">Soon</span>
</div>
{:else}
<nav class="p-4 space-y-6">
<div class="relative">
<Search
class="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground"
/>
<input
type="text"
bind:value={searchQuery}
placeholder={$t('common.search')}
class="w-full pl-9 pr-4 py-2 rounded-xl border border-border bg-background/50 focus:ring-2 focus:ring-primary/20 transition-all outline-none text-sm"
/>
</div>
<div class="space-y-1">
<p
class="px-4 text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2"
>
General
</p>
{#each filteredMainDocs as doc}
<a
href="/docs/{doc.slug}"
onclick={() => (isMobileMenuOpen = false)}
@@ -146,8 +209,31 @@
<doc.icon class="w-4 h-4" />
<span class="text-sm font-medium">{doc.title}</span>
</a>
{/if}
{/each}
{/each}
</div>
{#if filteredSoftwareDocs.length > 0}
<div class="space-y-1">
<p
class="px-4 text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2"
>
Software
</p>
{#each filteredSoftwareDocs as doc}
<a
href="/docs/{doc.slug}"
onclick={() => (isMobileMenuOpen = false)}
class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 {page
.url.pathname === `/docs/${doc.slug}`
? 'bg-primary text-primary-foreground'
: 'hover:bg-primary/10 text-muted-foreground'}"
>
<doc.icon class="w-4 h-4" />
<span class="text-sm font-medium capitalize">{doc.title}</span>
</a>
{/each}
</div>
{/if}
</nav>
</div>
{/if}

View File

@@ -1,12 +1,104 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { Shield, Terminal, BookOpen, Cpu, Zap, Code2 } from 'lucide-svelte';
onMount(() => {
goto('/docs/verification', { replaceState: true });
});
const overviewCards = [
{
title: 'Verification',
description: 'Deep dive into how our client-side WASM verification engine works.',
href: '/docs/verification',
icon: Shield,
color: 'text-blue-500',
},
{
title: 'API Reference',
description: 'Developer guide for integrating with our software metadata APIs.',
href: '/docs/api',
icon: Terminal,
color: 'text-purple-500',
},
{
title: 'Zero Trust',
description: 'Learn about our security model and why your privacy is guaranteed.',
href: '/docs/verification',
icon: Zap,
color: 'text-yellow-500',
},
{
title: 'Open Source',
description: 'How to contribute to Software Station and host your own instance.',
href: 'https://git.quad4.io/Quad4-Software/software-station',
external: true,
icon: Code2,
color: 'text-green-500',
},
];
</script>
<div class="flex items-center justify-center min-h-[50vh]">
<div class="animate-pulse text-muted-foreground font-medium">Loading documentation...</div>
<div class="space-y-12 animate-in fade-in duration-500">
<div class="text-center space-y-4">
<div
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary border border-primary/20 text-xs font-bold uppercase tracking-wider"
>
<BookOpen class="w-3 h-3" />
Welcome
</div>
<h1 class="text-4xl font-black tracking-tight sm:text-5xl">Knowledge Base</h1>
<p class="text-xl text-muted-foreground max-w-2xl mx-auto">
Everything you need to know about Software Station, from verification internals to API
integration.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{#each overviewCards as card}
<a
href={card.href}
target={card.external ? '_blank' : undefined}
rel={card.external ? 'noopener noreferrer' : undefined}
class="group p-8 rounded-3xl border border-border bg-card hover:shadow-2xl hover:border-primary/30 transition-all duration-300"
>
<div class="flex items-start justify-between mb-6">
<div
class="p-3 rounded-2xl bg-primary/5 text-primary group-hover:scale-110 transition-transform"
>
<card.icon class="w-8 h-8" />
</div>
{#if card.external}
<Zap class="w-4 h-4 text-muted-foreground opacity-50" />
{/if}
</div>
<h3 class="text-2xl font-bold mb-2 group-hover:text-primary transition-colors">
{card.title}
</h3>
<p class="text-muted-foreground leading-relaxed">
{card.description}
</p>
</a>
{/each}
</div>
<div class="p-8 rounded-3xl bg-primary/5 border border-primary/20 space-y-6">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-primary/10 text-primary">
<Cpu class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold">Project Overview</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p>
Software Station is a specialized platform designed for the <strong
>secure distribution</strong
>
of software binaries. In an era where supply chain attacks are increasing, we provide a
<strong>Zero-Trust architecture</strong> that shifts the responsibility of verification to the
client browser.
</p>
<p>
Our core philosophy is that the server serving the software should not be the sole authority
on its integrity. By leveraging <strong>WebAssembly</strong> and
<strong>SHA256 cryptography</strong>, we ensure that every byte you download matches the
original manifest signed by the developers.
</p>
</div>
</div>
</div>

View File

@@ -1,10 +1,11 @@
<script lang="ts">
let { data } = $props();
const Content = $derived(data.content);
</script>
<div class="prose dark:prose-invert max-w-none">
{#if Content}
<Content />
{#if data.content}
{#key data.content}
<data.content />
{/key}
{/if}
</div>

View File

@@ -0,0 +1,52 @@
import { error } from '@sveltejs/kit';
import { browser } from '$app/environment';
import { get } from 'svelte/store';
import { locale } from 'svelte-i18n';
export const load = async ({ params, depends }) => {
const { slug } = params;
depends('app:locale');
const currentLocale = (browser ? localStorage.getItem('locale') : null) || get(locale) || 'en';
const cleanSlug = slug.endsWith('/') ? slug.slice(0, -1) : slug;
const modules = import.meta.glob('../../../lib/docs/**/*.{svx,md}');
let match: (() => Promise<any>) | undefined;
const localeSpecificPaths = [
`../../../lib/docs/${cleanSlug}.${currentLocale}.svx`,
`../../../lib/docs/${cleanSlug}.${currentLocale}.md`,
];
for (const path of localeSpecificPaths) {
if (modules[path]) {
match = modules[path];
break;
}
}
if (!match) {
match = modules[`../../../lib/docs/${cleanSlug}.svx`];
if (!match) {
match = modules[`../../../lib/docs/${cleanSlug}.md`];
}
}
if (!match) {
throw error(404, 'Documentation not found');
}
try {
const doc = (await match()) as any;
return {
content: doc.default,
metadata: doc.metadata,
};
} catch (e) {
console.error('Error loading doc:', e);
throw error(500, 'Error loading documentation content');
}
};

View File

@@ -1,14 +0,0 @@
import { error } from '@sveltejs/kit';
export const load = async ({ params }) => {
try {
const doc = await import(`../../../lib/docs/${params.slug}.svx`);
return {
content: doc.default,
metadata: doc.metadata,
};
} catch {
throw error(404, 'Documentation not found');
}
};

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { t } from 'svelte-i18n';
import { locale, t } from 'svelte-i18n';
import { FileText, Download, ArrowLeft } from 'lucide-svelte';
let docContent = $state('');
@@ -10,7 +10,7 @@
const docType = $derived(page.params.doc);
$effect(() => {
if (docType) {
if (docType || $locale) {
fetchDoc();
}
});
@@ -19,7 +19,8 @@
loading = true;
error = false;
try {
const res = await fetch(`/api/legal?doc=${docType}`);
const lang = $locale || 'en';
const res = await fetch(`/api/legal?doc=${docType}&lang=${lang}`);
if (!res.ok) throw new Error('Failed to fetch');
docContent = await res.text();
} catch {

View File

Binary file not shown.

View File

@@ -13,4 +13,9 @@ const (
// Avatar Cache
AvatarCacheLimit = 100 * 1024 * 1024 // 100MB
AvatarCacheInterval = 1 * time.Hour
// Asset Cache
AssetCacheDir = ".cache/assets"
AssetCacheLimit = 2 * 1024 * 1024 * 1024 // 2GB
AssetCacheInterval = 6 * time.Hour
)

View File

@@ -62,16 +62,20 @@ type Server struct {
rssCache atomic.Value
rssLastMod atomic.Value
avatarCache string
assetCache string
cacheEnabled bool
salt []byte
}
func NewServer(token string, initialSoftware []models.Software, statsService *stats.Service) *Server {
func NewServer(token string, initialSoftware []models.Software, statsService *stats.Service, cacheEnabled bool) *Server {
s := &Server{
GiteaToken: token,
SoftwareList: &SoftwareCache{data: initialSoftware},
Stats: statsService,
urlMap: make(map[string]string),
avatarCache: ".cache/avatars",
assetCache: AssetCacheDir,
cacheEnabled: cacheEnabled,
}
s.loadSalt()
@@ -82,6 +86,13 @@ func NewServer(token string, initialSoftware []models.Software, statsService *st
log.Printf("Warning: failed to create avatar cache directory: %v", err)
}
if s.cacheEnabled {
if err := os.MkdirAll(s.assetCache, 0750); err != nil {
log.Printf("Warning: failed to create asset cache directory: %v", err)
}
go s.startAssetCleanup()
}
s.RefreshProxiedList()
go s.startAvatarCleanup()
@@ -176,8 +187,23 @@ func (s *Server) startAvatarCleanup() {
}
}
func (s *Server) startAssetCleanup() {
ticker := time.NewTicker(AssetCacheInterval)
for range ticker.C {
s.cleanupAssetCache()
}
}
func (s *Server) cleanupAssetCache() {
s.cleanupCacheDir(s.assetCache, AssetCacheLimit)
}
func (s *Server) cleanupAvatarCache() {
files, err := os.ReadDir(s.avatarCache)
s.cleanupCacheDir(s.avatarCache, AvatarCacheLimit)
}
func (s *Server) cleanupCacheDir(dir string, limit int64) {
files, err := os.ReadDir(dir)
if err != nil {
return
}
@@ -194,7 +220,7 @@ func (s *Server) cleanupAvatarCache() {
if f.IsDir() {
continue
}
path := filepath.Join(s.avatarCache, f.Name())
path := filepath.Join(dir, f.Name())
info, err := f.Info()
if err != nil {
continue
@@ -207,7 +233,7 @@ func (s *Server) cleanupAvatarCache() {
})
}
if totalSize <= AvatarCacheLimit {
if totalSize <= limit {
return
}
@@ -217,14 +243,14 @@ func (s *Server) cleanupAvatarCache() {
})
for _, info := range infos {
if totalSize <= AvatarCacheLimit {
if totalSize <= limit {
break
}
if err := os.Remove(info.path); err == nil {
totalSize -= info.size
}
}
log.Printf("Avatar cache cleaned up. Current size: %v bytes", totalSize)
log.Printf("Cache directory %s cleaned up. Current size: %v bytes", dir, totalSize)
}
func (s *Server) APISoftwareHandler(w http.ResponseWriter, r *http.Request) {
@@ -328,6 +354,23 @@ func (s *Server) DownloadProxyHandler(w http.ResponseWriter, r *http.Request) {
}
s.Stats.DownloadStats.Unlock()
cachePath := filepath.Join(s.assetCache, id)
if s.cacheEnabled {
if _, err := os.Stat(cachePath); err == nil {
now := time.Now()
_ = os.Chtimes(cachePath, now, now)
// Update stats for cached download
s.Stats.GlobalStats.Lock()
s.Stats.GlobalStats.SuccessDownloads[fingerprint] = true
s.Stats.GlobalStats.Unlock()
s.Stats.SaveHashes()
http.ServeFile(w, r, cachePath)
return
}
}
req, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
http.Error(w, "Failed to create request", http.StatusInternalServerError)
@@ -374,7 +417,31 @@ func (s *Server) DownloadProxyHandler(w http.ResponseWriter, r *http.Request) {
Stats: s.Stats,
}
n, err := io.Copy(w, tr)
var body io.Reader = tr
var tempFile *os.File
if s.cacheEnabled && resp.StatusCode == http.StatusOK {
var err error
tempFile, err = os.CreateTemp(s.assetCache, "asset-*")
if err == nil {
body = io.TeeReader(tr, tempFile)
} else {
log.Printf("Warning: failed to create temp file for caching: %v", err)
}
}
n, err := io.Copy(w, body)
if tempFile != nil {
tempFile.Close()
if err == nil {
if renameErr := os.Rename(tempFile.Name(), cachePath); renameErr != nil {
log.Printf("Warning: failed to rename temp file to cache: %v", renameErr)
_ = os.Remove(tempFile.Name())
}
} else {
_ = os.Remove(tempFile.Name())
}
}
if err != nil {
log.Printf("Error copying proxy response: %v", err)
}
@@ -389,6 +456,7 @@ func (s *Server) DownloadProxyHandler(w http.ResponseWriter, r *http.Request) {
func (s *Server) LegalHandler(w http.ResponseWriter, r *http.Request) {
doc := r.URL.Query().Get("doc")
lang := r.URL.Query().Get("lang")
var filename string
switch doc {
@@ -404,6 +472,15 @@ func (s *Server) LegalHandler(w http.ResponseWriter, r *http.Request) {
}
path := filepath.Join(LegalDir, filename)
if lang != "" && lang != "en" {
ext := filepath.Ext(filename)
base := strings.TrimSuffix(filename, ext)
langPath := filepath.Join(LegalDir, fmt.Sprintf("%s.%s%s", base, lang, ext))
if _, err := os.Stat(langPath); err == nil {
path = langPath
}
}
data, err := os.ReadFile(path) // #nosec G304
if err != nil {
http.Error(w, "Document not found", http.StatusNotFound)

View File

@@ -5,10 +5,12 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"software-station/internal/models"
"software-station/internal/stats"
"strings"
"testing"
"time"
)
func TestHandlers(t *testing.T) {
@@ -34,7 +36,7 @@ func TestHandlers(t *testing.T) {
AvatarURL: "http://example.com/logo.png",
},
}
server := NewServer("token", initialSoftware, statsService)
server := NewServer("token", initialSoftware, statsService, true)
t.Run("APISoftwareHandler", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/software", nil)
@@ -94,6 +96,78 @@ func TestHandlers(t *testing.T) {
}
})
t.Run("AssetCaching", func(t *testing.T) {
content := []byte("cache-me-if-you-can")
callCount := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
w.Write(content)
}))
defer upstream.Close()
hash := server.RegisterURL(upstream.URL)
// First call - should go to upstream
req := httptest.NewRequest("GET", "/api/download?id="+hash, nil)
rr := httptest.NewRecorder()
server.DownloadProxyHandler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("first call: expected 200, got %d", rr.Code)
}
if callCount != 1 {
t.Errorf("first call: expected call count 1, got %d", callCount)
}
// Verify file exists in cache
cachePath := filepath.Join(AssetCacheDir, hash)
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
t.Error("asset was not cached to disk")
}
// Second call - should be served from cache
req = httptest.NewRequest("GET", "/api/download?id="+hash, nil)
rr = httptest.NewRecorder()
server.DownloadProxyHandler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("second call: expected 200, got %d", rr.Code)
}
if callCount != 1 {
t.Errorf("second call: expected call count 1 (from cache), got %d", callCount)
}
if rr.Body.String() != string(content) {
t.Errorf("second call: expected content %q, got %q", string(content), rr.Body.String())
}
})
t.Run("CacheCleanup", func(t *testing.T) {
cacheDir := "test_cleanup_cache"
os.MkdirAll(cacheDir, 0750)
defer os.RemoveAll(cacheDir)
// Create some files
f1 := filepath.Join(cacheDir, "old")
f2 := filepath.Join(cacheDir, "new")
os.WriteFile(f1, make([]byte, 100), 0600)
time.Sleep(10 * time.Millisecond) // Ensure different mod times
os.WriteFile(f2, make([]byte, 100), 0600)
// Set mod times explicitly
now := time.Now()
os.Chtimes(f1, now.Add(-1*time.Hour), now.Add(-1*time.Hour))
os.Chtimes(f2, now, now)
// Clean up with limit that only allows one file
server.cleanupCacheDir(cacheDir, 150)
if _, err := os.Stat(f1); err == nil {
t.Error("expected old file to be removed")
}
if _, err := os.Stat(f2); err != nil {
t.Error("expected new file to be kept")
}
})
t.Run("RSSHandler", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/rss", nil)
rr := httptest.NewRecorder()

View File

@@ -13,6 +13,14 @@ import (
"software-station/internal/models"
)
func checkDocsExist(repo string) bool {
// Check for .svx or .md files in the frontend docs directory
docsDir := "frontend/src/lib/docs/software"
_, errSvx := os.Stat(docsDir + "/" + repo + ".svx")
_, errMd := os.Stat(docsDir + "/" + repo + ".md")
return errSvx == nil || errMd == nil
}
func LoadSoftware(path, server, token string) []models.Software {
return LoadSoftwareExtended(path, server, token, true)
}
@@ -74,6 +82,7 @@ func LoadSoftwareExtended(path, server, token string, useCache bool) []models.So
License: license,
IsPrivate: isPrivate,
AvatarURL: avatarURL,
HasDocs: checkDocsExist(repo),
}
softwareList = append(softwareList, sw)
if err := cache.SaveToCache(owner, repo, sw); err != nil {

View File

@@ -131,6 +131,38 @@ func FetchContributors(server, token, owner, repo string) ([]models.Contributor,
func DetectOS(filename string) string {
lower := strings.ToLower(filename)
if strings.HasSuffix(lower, ".whl") {
if strings.Contains(lower, "win") || strings.Contains(lower, "windows") {
return models.OSWindows
}
if strings.Contains(lower, "macosx") || strings.Contains(lower, "darwin") {
return models.OSMacOS
}
if strings.Contains(lower, "linux") {
return models.OSLinux
}
return models.OSUnknown
}
if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") || strings.HasSuffix(lower, ".zip") {
if strings.Contains(lower, "win") || strings.Contains(lower, "windows") {
return models.OSWindows
}
if strings.Contains(lower, "mac") || strings.Contains(lower, "darwin") || strings.Contains(lower, "osx") {
return models.OSMacOS
}
if strings.Contains(lower, "linux") {
return models.OSLinux
}
if strings.Contains(lower, "freebsd") {
return models.OSFreeBSD
}
if strings.Contains(lower, "openbsd") {
return models.OSOpenBSD
}
return models.OSUnknown
}
osMap := []struct {
patterns []string
suffixes []string

View File

@@ -41,6 +41,7 @@ type Software struct {
License string `json:"license,omitempty"`
IsPrivate bool `json:"is_private"`
AvatarURL string `json:"avatar_url,omitempty"`
HasDocs bool `json:"has_docs"`
}
type FingerprintData struct {

10
legal/disclaimer.de.txt Normal file
View File

@@ -0,0 +1,10 @@
Haftungsausschluss
Version: 2.1
Zuletzt aktualisiert: 27. Dezember 2025
1. Keine Gewährleistung: Die Software und der Dienst werden "wie besehen" (as-is) zur Verfügung gestellt, ohne ausdrückliche oder stillschweigende Gewährleistungen jeglicher Art, einschließlich, aber nicht beschränkt auf die stillschweigende Gewährleistung der Marktgängigkeit oder der Eignung für einen bestimmten Zweck.
2. Haftung: Quad4 und seine Mitwirkenden haften nicht für Schäden (einschließlich Datenverlust, Hardwarefehler oder finanzielle Verluste), die durch die Nutzung des Dienstes oder der von dieser Station heruntergeladenen Software entstehen.
3. Integrität und Verifizierung: Wir bieten eine clientseitige Verifizierungs-Engine an, die auf WebAssembly (WASM) basiert. Während dieses System SHA256-Prüfsummen in Echtzeit in Ihrem Browser neu berechnet und sie mit kryptografisch signierten Manifesten abgleicht, garantiert eine erfolgreiche Verifizierung nicht, dass die Software frei von Fehlern, Schwachstellen oder bösartiger Logik ist, die vom ursprünglichen Autor beabsichtigt wurde. Sie stellt lediglich sicher, dass die Datei mit dem vom Entwickler beabsichtigten Release übereinstimmt.
4. Sicherheitsmerkmale: Wir verwenden Subresource Integrity (SRI) für alle kritischen Frontend-Assets. Dies stellt sicher, dass der Code, auf dem der Verifizierer selbst läuft, nicht manipuliert wurde.
5. Upstream-Inhalte: Diese Station leitet Inhalte von externen Software-Repositories und Speicherquellen (z. B. Gitea, S3, SFTP) weiter, die vom Betreiber ausgewählt wurden. Obwohl der Betreiber diese Quellen auswählt, haben wir keine Kontrolle über und sind nicht verantwortlich für den Inhalt, die Sicherheits- oder Datenschutzpraktiken der ursprünglichen Entwickler oder Hosting-Anbieter.

32
legal/privacy.de.txt Normal file
View File

@@ -0,0 +1,32 @@
Datenschutzerklärung
Version: 2.1
Zuletzt aktualisiert: 27. Dezember 2025
Wir schätzen Ihre Privatsphäre und arbeiten in Übereinstimmung mit der Datenschutz-Grundverordnung (DSGVO). Dieser Dienst wurde nach dem Prinzip "Privacy by Design" entwickelt, um eine minimale Datenerhebung zu gewährleisten.
Clientseitige Verifizierung:
Software Station verwendet ein "Zero-Trust"-Verifizierungsmodell. Alle Integritätsprüfungen (SHA256-Hashing) werden lokal in Ihrem Browser mit einer WebAssembly (WASM)-Engine durchgeführt. Kein Teil der heruntergeladenen Softwaredatei wird während des Verifizierungsprozesses jemals an unsere Server zurückübertragen. Die einzigen Daten, die während eines Downloads an unsere Server gesendet werden, sind die Standardanfrage für das Asset selbst.
Datenverarbeitung und Anonymisierung:
Um unsere Infrastruktur zu schützen und eine faire Ressourcenverteilung zu gewährleisten, verarbeiten wir bestimmte technische Metadaten. Entscheidend ist, dass wir keine rohen IP-Adressen oder personenbezogene Daten (PII) speichern.
Die folgenden Daten werden im flüchtigen Speicher verarbeitet, um einen eindeutigen, kryptografischen Einweg-Hash (Fingerabdruck) zu erstellen:
- IP-Adresse
- User Agent und Client Hints (z. B. Plattform, Mobilstatus)
- TLS-Metadaten (Cipher Suites und Handshake-Versionen)
- Eine persistente Kennung, die in einem First-Party-Cookie (_ss_uid) gespeichert ist
Unmittelbar nach der Erstellung des Fingerabdrucks werden die rohen Eingabedaten (einschließlich Ihrer IP-Adresse) aus dem Speicher verworfen. Wir speichern nur den resultierenden anonymen Hash, um Ratenschutzlimits durchzusetzen und aggregierte Download-Statistiken zu erstellen.
Rechtsgrundlage für die Verarbeitung:
Unsere Verarbeitung technischer Metadaten basiert auf "Berechtigtem Interesse" (Art. 6(1)(f) DSGVO). Dies ist notwendig, um:
1. Denial-of-Service (DoS)-Angriffe und automatisiertes Scraping zu verhindern.
2. Die Stabilität und Sicherheit der Softwareverteilungsplattform zu gewährleisten.
3. Fair-Use-Download-Limits für alle Benutzer durchzusetzen.
Cookies:
Wir verwenden ein einziges First-Party-Cookie (_ss_uid) zu Sicherheitszwecken. Es enthält keine persönlichen Informationen und dient ausschließlich dazu, einen konsistenten Sicherheitskontext für Ihr Gerät aufrechtzuerhalten. Wir verwenden keine Tracker von Drittanbietern, Analysen oder Marketing-Cookies.
Ihre Rechte:
Nach der DSGVO haben Sie das Recht auf Auskunft, Berichtigung oder Löschung Ihrer Daten. Aufgrund unseres Anonymisierungsprozesses können wir einen bestimmten Hash in der Regel nicht ohne zusätzliche identifizierende Informationen einer physischen Person zuordnen. Wenn Sie jedoch Ihre Rechte ausüben möchten, wenden Sie sich bitte an den Administrator.

14
legal/terms.de.txt Normal file
View File

@@ -0,0 +1,14 @@
Nutzungsbedingungen
Version: 2.1
Zuletzt aktualisiert: 27. Dezember 2025
Durch die Nutzung dieser Softwarestation stimmen Sie den folgenden Bedingungen zu und erkennen unsere Datenschutzerklärung an:
1. Fair Use: Sie werden nicht versuchen, Ratenschutzlimits zu umgehen, Fingerabdrücke zu fälschen oder die Station übermäßig zu scrapen. Der automatisierte Zugriff muss die angegebenen Ratenschutzlimits respektieren.
2. Anti-Abuse: Wir setzen fortschrittliches Fingerprinting und zustandsbehaftetes Tracking (über Cookies) ein, um bösartige Aktivitäten zu verhindern. Wir behalten uns das Recht vor, jeden anonymen Fingerabdruck oder IP-Bereich zu sperren, der missbräuchlich verwendet wird.
3. Verifizierungssystem: Sie erkennen an, dass das clientseitige Verifizierungssystem erfordert, dass WebAssembly (WASM) in Ihrem Browser aktiviert ist. Der Versuch, den Verifizierer, seine Manifeste oder die Subresource Integrity (SRI)-Hashes zu manipulieren, ist strengstens untersagt.
4. Verteilung: Die hier bereitgestellte Software wird von verschiedenen Upstream-Quellen gespiegelt. Die ursprünglichen Lizenzen für die einzelnen Projekte gelten und müssen respektiert werden.
5. Haftungsausschluss: DER DIENST UND DIE SOFTWARE WERDEN "WIE BESEHEN" ZUR VERFÜGUNG GESTELLT, OHNE GEWÄHRLEISTUNG JEGLICHER ART, WEDER AUSDRÜCKLICH NOCH STILLSCHWEIGEND.
Ein Missbrauch des Dienstes kann zur dauerhaften Sperrung Ihrer Sicherheitskennung führen.

View File

@@ -43,6 +43,7 @@ func main() {
port := flag.String("p", getEnv("PORT", "8080"), "Server port")
isProd := flag.Bool("prod", os.Getenv("NODE_ENV") == "production", "Run in production mode")
disableVerifier := flag.Bool("disable-verifier", getEnv("DISABLE_VERIFIER", "false") == "true", "Completely disable the verifier UI and logic")
cacheAssets := flag.Bool("cache-assets", getEnv("CACHE_ASSETS", "false") == "true", "Cache popular software assets locally")
updateInterval := flag.Duration("u", 1*time.Hour, "Software update interval")
flag.Parse()
@@ -54,7 +55,7 @@ func main() {
botBlocker := security.NewBotBlocker(*uaBlocklistPath)
initialSoftware := config.LoadSoftware(configPath, giteaServer, giteaToken)
apiServer := api.NewServer(giteaToken, initialSoftware, statsService)
apiServer := api.NewServer(giteaToken, initialSoftware, statsService, *cacheAssets)
config.StartBackgroundUpdater(configPath, giteaServer, giteaToken, *updateInterval, apiServer.UpdateSoftwareList)
r := chi.NewRouter()
@@ -131,8 +132,8 @@ func main() {
if strings.HasPrefix(path, "_app/immutable/") {
// SvelteKit immutable assets (fingerprinted)
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
} else if strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") ||
strings.HasSuffix(path, ".png") || strings.HasSuffix(path, ".webp") ||
} else if strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") ||
strings.HasSuffix(path, ".png") || strings.HasSuffix(path, ".webp") ||
strings.HasSuffix(path, ".svg") || strings.HasSuffix(path, ".wasm") {
// Other static assets (1 week)
w.Header().Set("Cache-Control", "public, max-age=604800")

View File

@@ -48,7 +48,7 @@ func TestMainHandlers(t *testing.T) {
statsService := stats.NewService("test-hashes.json")
botBlocker := security.NewBotBlocker("")
initialSoftware := config.LoadSoftware(configPath, giteaServer, "")
apiServer := api.NewServer("", initialSoftware, statsService)
apiServer := api.NewServer("", initialSoftware, statsService, true)
r := chi.NewRouter()
r.Use(security.SecurityMiddleware(statsService, botBlocker))
@@ -193,6 +193,41 @@ func TestMainHandlers(t *testing.T) {
}
})
t.Run("Asset Caching Integration", func(t *testing.T) {
content := []byte("integration-cache-test")
callCount := 0
assetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
w.Write(content)
}))
defer assetServer.Close()
hash := apiServer.RegisterURL(assetServer.URL)
// First call
req := httptest.NewRequest("GET", fmt.Sprintf("/api/download?id=%s", hash), nil)
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("first call: expected 200, got %d", rr.Code)
}
// Second call (should be cached)
req = httptest.NewRequest("GET", fmt.Sprintf("/api/download?id=%s", hash), nil)
rr = httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("second call: expected 200, got %d", rr.Code)
}
if callCount != 1 {
t.Errorf("expected 1 upstream call, got %d", callCount)
}
if rr.Body.String() != string(content) {
t.Errorf("expected content %q, got %q", string(content), rr.Body.String())
}
})
t.Run("Security - Path Traversal", func(t *testing.T) {
patterns := []string{"/.git/config", "/etc/passwd"}
for _, p := range patterns {
@@ -208,7 +243,7 @@ func TestMainHandlers(t *testing.T) {
t.Run("Security - XSS in API", func(t *testing.T) {
malicious := []models.Software{{Name: "<script>alert(1)</script>"}}
testStatsService := stats.NewService("test-hashes.json")
srv := api.NewServer("", malicious, testStatsService)
srv := api.NewServer("", malicious, testStatsService, true)
req := httptest.NewRequest("GET", "/api/software", nil)
rr := httptest.NewRecorder()
srv.APISoftwareHandler(rr, req)