Update asset verification and documentation features

- Added a flag to disable the verifier UI and logic for user preferences.
- Implemented Cache-Control headers for static assets in production.
- Updated the SoftwareCard component to include a copy hash feature and display release dates.
- Introduced a Markdown component for rendering documentation content.
- Enhanced the verification process with speed updates during asset downloads.
- Improved the user interface for verification toasts and modals.
- Updated legal documents with new versions and additional privacy features.
- Added new API documentation and routes for better user guidance.
This commit is contained in:
2025-12-27 18:07:12 -06:00
parent 8f94411747
commit 5b8daa638d
31 changed files with 1311 additions and 161 deletions

View File

@@ -25,7 +25,6 @@ A software distribution platform for assets built and hosted on Gitea. Built wit
- Software dependencies page and licenses information.
- SBOM and SPDX viewer.
- CDN support
- GPG signatures verification
- OSV integration for vulnerability scanning.
- Container scanning
- Authentication for certain software/containers

View File

@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.2.0",
"version": "0.3.0",
"type": "module",
"scripts": {
"dev": "vite dev",
@@ -19,12 +19,16 @@
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.49.2",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/typography": "^0.5.19",
"@types/marked": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.50.1",
"@typescript-eslint/parser": "^8.50.1",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-plugin-svelte": "^3.13.1",
"lucide-svelte": "^0.562.0",
"marked": "^17.0.1",
"mdsvex": "^0.12.6",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.1",

163
frontend/pnpm-lock.yaml generated
View File

@@ -22,6 +22,12 @@ importers:
'@sveltejs/vite-plugin-svelte':
specifier: ^6.2.1
version: 6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7))
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@3.4.19)
'@types/marked':
specifier: ^6.0.0
version: 6.0.0
'@typescript-eslint/eslint-plugin':
specifier: ^8.50.1
version: 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
@@ -40,6 +46,12 @@ importers:
lucide-svelte:
specifier: ^0.562.0
version: 0.562.0(svelte@5.46.1)
marked:
specifier: ^17.0.1
version: 17.0.1
mdsvex:
specifier: ^0.12.6
version: 0.12.6(svelte@5.46.1)
postcss:
specifier: ^8.5.6
version: 8.5.6
@@ -944,6 +956,14 @@ packages:
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@tailwindcss/typography@0.5.19':
resolution:
{
integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==,
}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@types/cookie@0.6.0':
resolution:
{
@@ -962,6 +982,25 @@ packages:
integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==,
}
'@types/marked@6.0.0':
resolution:
{
integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==,
}
deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed.
'@types/mdast@4.0.4':
resolution:
{
integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==,
}
'@types/unist@2.0.11':
resolution:
{
integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==,
}
'@typescript-eslint/eslint-plugin@8.50.1':
resolution:
{
@@ -1871,6 +1910,22 @@ packages:
integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==,
}
marked@17.0.1:
resolution:
{
integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==,
}
engines: { node: '>= 20' }
hasBin: true
mdsvex@0.12.6:
resolution:
{
integrity: sha512-pupx2gzWh3hDtm/iDW4WuCpljmyHbHi34r7ktOqpPGvyiM4MyfNgdJ3qMizXdgCErmvYC9Nn/qyjePy+4ss9Wg==,
}
peerDependencies:
svelte: ^3.56.0 || ^4.0.0 || ^5.0.0-next.120
memoizee@0.4.17:
resolution:
{
@@ -2141,6 +2196,13 @@ packages:
peerDependencies:
postcss: ^8.4.29
postcss-selector-parser@6.0.10:
resolution:
{
integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==,
}
engines: { node: '>=4' }
postcss-selector-parser@6.1.2:
resolution:
{
@@ -2192,6 +2254,19 @@ packages:
engines: { node: '>=14' }
hasBin: true
prism-svelte@0.4.7:
resolution:
{
integrity: sha512-yABh19CYbM24V7aS7TuPYRNMqthxwbvx6FF/Rw920YbyBWO3tnyPIqRMgHuSVsLmuHkkBS1Akyof463FVdkeDQ==,
}
prismjs@1.30.0:
resolution:
{
integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==,
}
engines: { node: '>=6' }
punycode@2.3.1:
resolution:
{
@@ -2476,6 +2551,30 @@ packages:
engines: { node: '>=14.17' }
hasBin: true
unist-util-is@4.1.0:
resolution:
{
integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==,
}
unist-util-stringify-position@2.0.3:
resolution:
{
integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==,
}
unist-util-visit-parents@3.1.1:
resolution:
{
integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==,
}
unist-util-visit@2.0.3:
resolution:
{
integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==,
}
update-browserslist-db@1.2.3:
resolution:
{
@@ -2497,6 +2596,12 @@ packages:
integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==,
}
vfile-message@2.0.4:
resolution:
{
integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==,
}
vite@7.3.0:
resolution:
{
@@ -2972,12 +3077,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.19)':
dependencies:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.19
'@types/cookie@0.6.0': {}
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
'@types/marked@6.0.0':
dependencies:
marked: 17.0.1
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 2.0.11
'@types/unist@2.0.11': {}
'@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -3575,6 +3695,18 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
marked@17.0.1: {}
mdsvex@0.12.6(svelte@5.46.1):
dependencies:
'@types/mdast': 4.0.4
'@types/unist': 2.0.11
prism-svelte: 0.4.7
prismjs: 1.30.0
svelte: 5.46.1
unist-util-visit: 2.0.3
vfile-message: 2.0.4
memoizee@0.4.17:
dependencies:
d: 1.0.2
@@ -3703,6 +3835,11 @@ snapshots:
dependencies:
postcss: 8.5.6
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
@@ -3730,6 +3867,10 @@ snapshots:
prettier@3.7.4: {}
prism-svelte@0.4.7: {}
prismjs@1.30.0: {}
punycode@2.3.1: {}
queue-microtask@1.2.3: {}
@@ -3951,6 +4092,23 @@ snapshots:
typescript@5.9.3: {}
unist-util-is@4.1.0: {}
unist-util-stringify-position@2.0.3:
dependencies:
'@types/unist': 2.0.11
unist-util-visit-parents@3.1.1:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 4.1.0
unist-util-visit@2.0.3:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 4.1.0
unist-util-visit-parents: 3.1.1
update-browserslist-db@1.2.3(browserslist@4.28.1):
dependencies:
browserslist: 4.28.1
@@ -3963,6 +4121,11 @@ snapshots:
util-deprecate@1.0.2: {}
vfile-message@2.0.4:
dependencies:
'@types/unist': 2.0.11
unist-util-stringify-position: 2.0.3
vite@7.3.0(jiti@1.21.7):
dependencies:
esbuild: 0.27.2

View File

@@ -15,10 +15,10 @@
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--muted-foreground: 215.4 16.3% 40%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive: 0 84.2% 50%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;

View File

@@ -4,31 +4,41 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="/logo.png" />
<!-- Primary Meta Tags -->
<title>Software Station - Secure & Transparent Software Distribution</title>
<meta name="title" content="Software Station - Secure & Transparent Software Distribution" />
<meta name="description" content="A privacy-focused platform for secure software distribution with built-in client-side WebAssembly verification and cryptographic integrity checks." />
<meta
name="description"
content="A privacy-focused platform for secure software distribution with built-in client-side WebAssembly verification and cryptographic integrity checks."
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://software.quad4.io/" />
<meta property="og:title" content="Software Station - Secure & Transparent Software Distribution" />
<meta property="og:description" content="A privacy-focused platform for secure software distribution with built-in client-side WebAssembly verification and cryptographic integrity checks." />
<meta
property="og:title"
content="Software Station - Secure & Transparent Software Distribution"
/>
<meta
property="og:description"
content="A privacy-focused platform for secure software distribution with built-in client-side WebAssembly verification and cryptographic integrity checks."
/>
<meta property="og:image" content="/logo.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://software.quad4.io/" />
<meta property="twitter:title" content="Software Station - Secure & Transparent Software Distribution" />
<meta property="twitter:description" content="A privacy-focused platform for secure software distribution with built-in client-side WebAssembly verification and cryptographic integrity checks." />
<meta
property="twitter:title"
content="Software Station - Secure & Transparent Software Distribution"
/>
<meta
property="twitter:description"
content="A privacy-focused platform for secure software distribution with built-in client-side WebAssembly verification and cryptographic integrity checks."
/>
<meta property="twitter:image" content="/logo.png" />
<script
src="/verifier/wasm_exec.js"
integrity="sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce"
crossorigin="anonymous"
></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@@ -50,7 +50,7 @@
// Highlights strings and comments first but uses a more complex regex to avoid matching inside tags
// Actually, a better way is to do it in one pass or use non-capturing groups
return escaped
.replace(/\/\/.*/g, '<span class="text-zinc-500">$&</span>')
.replace(/\/\/.*/g, '<span class="text-zinc-400">$&</span>')
.replace(/(&quot;[^&]*&quot;)/g, '<span class="text-green-400">$1</span>')
.replace(
/\b(func|package|import|var|const|if|else|return|range|type|struct|interface)\b/g,
@@ -64,7 +64,7 @@
if (lang === 'python') {
return escaped
.replace(/#.*/g, '<span class="text-zinc-500">$&</span>')
.replace(/#.*/g, '<span class="text-zinc-400">$&</span>')
.replace(/(&quot;[^&]*&quot;|&#039;[^&]*&#039;)/g, '<span class="text-green-400">$1</span>')
.replace(
/\b(def|import|from|as|if|else|return|for|in|class|with|try|except)\b/g,
@@ -74,7 +74,7 @@
if (lang === 'javascript') {
return escaped
.replace(/\/\/.*/g, '<span class="text-zinc-500">$&</span>')
.replace(/\/\/.*/g, '<span class="text-zinc-400">$&</span>')
.replace(
/(&quot;[^&]*&quot;|&#039;[^&]*&#039;|`[^`]*`)/g,
'<span class="text-green-400">$1</span>'

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { marked } from 'marked';
let { content = '' } = $props();
let html = $derived(marked.parse(content));
</script>
<div class="prose dark:prose-invert max-w-none">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html html}
</div>
<style>
:global(.prose strong) {
color: hsl(var(--primary));
font-weight: 800;
}
</style>

View File

@@ -18,12 +18,16 @@
ChevronDown,
ChevronUp,
Rss,
Copy,
Check,
Calendar,
} from 'lucide-svelte';
import type { Software, Asset } from '$lib/types';
import VerificationModal from './VerificationModal.svelte';
import { backgroundVerify } from '$lib/verificationStore';
import { verifierDisabled } from '$lib/settingsStore';
import { get } from 'svelte/store';
import { onMount } from 'svelte';
interface Props {
software: Software;
@@ -41,9 +45,26 @@
contributors?: any[];
security?: any;
} | null>(null);
let copiedHash = $state<string | null>(null);
const theme = getContext<{ isDark: boolean }>('theme');
onMount(() => {
const ua = navigator.userAgent.toLowerCase();
let detectedOS = '';
if (ua.includes('win')) detectedOS = 'windows';
else if (ua.includes('mac')) detectedOS = 'macos';
else if (ua.includes('linux')) detectedOS = 'linux';
else if (ua.includes('android')) detectedOS = 'android';
else if (ua.includes('freebsd')) detectedOS = 'freebsd';
else if (ua.includes('openbsd')) detectedOS = 'openbsd';
// Only auto-filter if binaries exist for that OS
if (detectedOS && availableOSs.includes(detectedOS)) {
toggleOSFilter(detectedOS);
}
});
const availableOSs = $derived.by(() => {
const oss = new Set<string>();
software.releases?.forEach((release) => {
@@ -108,6 +129,21 @@
}
}
function copyHash(hash: string) {
navigator.clipboard.writeText(hash);
copiedHash = hash;
setTimeout(() => (copiedHash = null), 2000);
}
function formatDate(dateStr: string) {
const date = new Date(dateStr);
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
function handleDownload(e: MouseEvent, asset: Asset, release: any) {
if (asset.sha256) {
if (get(verifierDisabled)) {
@@ -139,7 +175,7 @@
</script>
<div
class="flex flex-col rounded-xl border border-border bg-card hover:shadow-xl hover:border-primary/30 transition-all duration-300 overflow-hidden group"
class="flex flex-col rounded-xl border border-border bg-card hover:shadow-xl hover:border-primary/50 transition-all duration-300 overflow-hidden group"
>
<div class="p-6 flex-1">
<div class="flex items-start justify-between mb-4">
@@ -148,6 +184,8 @@
<img
src={software.avatar_url}
alt={software.name}
width="24"
height="24"
class="w-6 h-6 rounded-md object-cover"
/>
{:else}
@@ -157,7 +195,7 @@
<div class="flex items-center gap-2">
<a
href="/api/rss?software={software.name}"
class="p-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-orange-500"
class="p-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-orange-700 dark:hover:text-orange-500"
title="RSS Feed"
target="_blank"
>
@@ -241,9 +279,11 @@
{@const brand = getOSIcon(os)}
<button
onclick={() => toggleOSFilter(os)}
class="p-1 rounded-md transition-all duration-200 {selectedOSs.includes(os)
class="p-1 rounded-md transition-all duration-200 relative {selectedOSs.includes(
os
)
? 'bg-primary text-primary-foreground shadow-sm scale-110'
: 'hover:bg-primary/10 text-muted-foreground/60 hover:text-primary'}"
: 'hover:bg-primary/10 text-muted-foreground/80 hover:text-primary'}"
title={$t(`os.${os}`, { default: os })}
>
{#if brand}
@@ -283,7 +323,7 @@
)}
{#if filteredAssets.length > 0}
<div class="space-y-2">
<div class="flex items-center justify-between text-[10px] font-bold opacity-60">
<div class="flex items-center justify-between text-[10px] font-bold opacity-80">
<span>{release.tag_name} {i === 0 ? `(${$t('common.latest')})` : ''}</span>
</div>
<div class="space-y-1">
@@ -294,6 +334,7 @@
<a
href={asset.url}
onclick={(e) => handleDownload(e, asset, release)}
data-sveltekit-reload
class="flex-1 flex items-center justify-between p-1.5 rounded-md hover:bg-accent transition-colors text-[11px] border border-transparent hover:border-border min-w-0"
>
<div class="flex items-center gap-2 min-w-0">
@@ -312,17 +353,30 @@
>
{#if asset.is_sbom}
<span
class="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400 text-[8px] font-bold uppercase tracking-tight border border-blue-500/20"
class="px-1 py-0.5 rounded bg-blue-500/10 text-blue-700 dark:text-blue-400 text-[8px] font-bold uppercase tracking-tight border border-blue-500/20"
>
{$t('common.sbom')}
</span>
{/if}
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-[9px] opacity-60">{formatSize(asset.size)}</span>
<span class="text-[9px] opacity-80">{formatSize(asset.size)}</span>
<Download class="w-3 h-3 text-primary" />
</div>
</a>
{#if asset.sha256}
<button
onclick={() => copyHash(asset.sha256!)}
class="p-1 rounded hover:bg-muted text-muted-foreground transition-colors"
title="Copy SHA256: {asset.sha256}"
>
{#if copiedHash === asset.sha256}
<Check class="w-3 h-3 text-green-500" />
{:else}
<Copy class="w-3 h-3" />
{/if}
</button>
{/if}
</div>
{/each}
</div>
@@ -345,6 +399,7 @@
<a
href={asset.url}
onclick={(e) => handleDownload(e, asset, latest)}
data-sveltekit-reload
class="flex-1 flex items-center justify-between p-2 rounded-md hover:bg-accent transition-colors text-sm border border-transparent hover:border-border min-w-0"
>
<div class="flex items-center gap-3 min-w-0">
@@ -363,7 +418,7 @@
</span>
{#if asset.is_sbom}
<span
class="px-1 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400 text-[9px] font-bold uppercase tracking-tight border border-blue-500/20"
class="px-1 py-0.5 rounded bg-blue-500/10 text-blue-700 dark:text-blue-400 text-[9px] font-bold uppercase tracking-tight border border-blue-500/20"
>
{$t('common.sbom')}
</span>
@@ -377,31 +432,55 @@
/>
</div>
</a>
{#if asset.sha256}
<div
class="p-1.5 rounded-full bg-green-500/10 text-green-600 dark:text-green-400 shrink-0"
title="{$t('common.checksumVerified')}: {asset.sha256}"
>
<FileCheck class="w-3.5 h-3.5" />
</div>
{:else}
<div
class="p-1.5 rounded-full bg-orange-500/10 text-orange-600 dark:text-orange-400 shrink-0"
title={$t('common.checksumMissing')}
>
<FileQuestion class="w-3.5 h-3.5" />
</div>
{/if}
<div class="flex items-center gap-1">
{#if asset.sha256}
<button
onclick={() => copyHash(asset.sha256!)}
class="p-1.5 rounded-md hover:bg-muted text-muted-foreground transition-colors group/copy"
title="Copy SHA256: {asset.sha256}"
>
{#if copiedHash === asset.sha256}
<Check class="w-3.5 h-3.5 text-green-500" />
{:else}
<Copy
class="w-3.5 h-3.5 group-hover/copy:text-primary transition-colors"
/>
{/if}
</button>
<div
class="p-1.5 rounded-full bg-green-500/10 text-green-600 dark:text-green-400 shrink-0"
title="{$t('common.checksumVerified')}: {asset.sha256}"
>
<FileCheck class="w-3.5 h-3.5" />
</div>
{:else}
<div
class="p-1.5 rounded-full bg-orange-500/10 text-orange-600 dark:text-orange-400 shrink-0"
title={$t('common.checksumMissing')}
>
<FileQuestion class="w-3.5 h-3.5" />
</div>
{/if}
</div>
</div>
{/each}
{:else}
<div class="text-center py-4 text-xs text-muted-foreground opacity-60">
<div class="text-center py-4 text-xs text-muted-foreground opacity-80">
{$t('common.noMatchingAssets')}
</div>
{/if}
</div>
{/if}
</div>
<div class="px-4 pb-4 pt-2 flex items-center justify-between border-t border-border/50">
<div class="flex items-center gap-1.5 text-[10px] text-muted-foreground font-medium">
<Calendar class="w-3 h-3" />
<span>Released {formatDate(latest.created_at)}</span>
</div>
<div class="text-[10px] text-muted-foreground/80 font-bold uppercase tracking-tighter">
{latest.assets.length} Assets
</div>
</div>
{:else}
<div class="text-sm text-center text-muted-foreground py-6">
{$t('common.noReleases')}

View File

@@ -142,7 +142,7 @@
<div>
<h3 class="text-xl font-bold leading-none">Verify Download</h3>
<a
href="https://git.quad4.io/Quad4-Software/software-station/software-verifier"
href="https://git.quad4.io/Quad4-Software/software-station/src/branch/master/software-verifier"
target="_blank"
class="text-[10px] text-muted-foreground hover:text-primary transition-colors flex items-center gap-1 mt-1"
>
@@ -168,7 +168,9 @@
<div class="flex items-center gap-4 mt-3 pt-3 border-t border-border/50">
<div class="flex items-center gap-1.5" title="TLS Certificate Validation">
<Lock
class="w-3.5 h-3.5 {security.tls_valid ? 'text-green-500' : 'text-destructive'}"
class="w-3.5 h-3.5 {security.tls_valid
? 'text-green-700 dark:text-green-400'
: 'text-destructive'}"
/>
<span class="text-[10px] font-bold uppercase tracking-tight">TLS</span>
</div>
@@ -226,7 +228,7 @@
Public GPG Keys
</p>
<span
class="text-[9px] px-1.5 py-0.5 rounded bg-orange-500/10 text-orange-600 font-bold"
class="text-[9px] px-1.5 py-0.5 rounded bg-orange-500/10 text-orange-700 dark:text-orange-400 font-bold"
>{selectedContributor.gpg_keys?.length || 0} Keys</span
>
</div>
@@ -328,7 +330,7 @@
</p>
<p class="text-[10px] text-muted-foreground mt-1">
{status === 'downloading'
? 'Fetching raw binary from source'
? `Fetching raw binary from source${progress > 0 ? ` • ${progress}%` : ''}`
: 'Computing SHA256 in sandbox'}
</p>
</div>
@@ -336,7 +338,7 @@
{:else if status === 'success'}
<div class="space-y-4">
<div
class="py-8 bg-green-500/5 rounded-xl border border-green-500/20 flex flex-col items-center justify-center gap-4 text-green-500"
class="py-8 bg-green-500/5 rounded-xl border border-green-500/20 flex flex-col items-center justify-center gap-4 text-green-700 dark:text-green-400"
>
<div class="p-4 rounded-full bg-green-500/10">
<ShieldCheck class="w-12 h-12" />
@@ -352,7 +354,7 @@
<div
class="px-4 py-2 bg-muted/50 border-b border-border/50 flex items-center gap-2"
>
<CheckCircle2 class="w-3 h-3 text-green-500" />
<CheckCircle2 class="w-3 h-3 text-green-700 dark:text-green-400" />
<span class="text-[10px] font-bold uppercase tracking-wider"
>Verification Audit</span
>
@@ -362,7 +364,7 @@
<div class="flex gap-3">
<div class="mt-0.5">
{#if step.status === 'success'}
<CheckCircle2 class="w-3 h-3 text-green-500" />
<CheckCircle2 class="w-3 h-3 text-green-700 dark:text-green-400" />
{:else if step.status === 'error'}
<ShieldAlert class="w-3 h-3 text-destructive" />
{:else}
@@ -383,7 +385,7 @@
<button
onclick={onClose}
class="w-full py-3 px-4 bg-green-500 text-white rounded-xl font-semibold hover:bg-green-600 transition-colors flex items-center justify-center gap-2"
class="w-full py-3 px-4 bg-green-700 dark:bg-green-600 text-white rounded-xl font-semibold hover:opacity-90 transition-opacity flex items-center justify-center gap-2"
>
Start Using Software
</button>
@@ -417,7 +419,7 @@
<div class="flex gap-3">
<div class="mt-0.5">
{#if step.status === 'success'}
<CheckCircle2 class="w-3 h-3 text-green-500" />
<CheckCircle2 class="w-3 h-3 text-green-700 dark:text-green-400" />
{:else if step.status === 'error'}
<ShieldAlert class="w-3 h-3 text-destructive" />
{:else}

View File

@@ -39,7 +39,7 @@
<div class="flex items-center gap-3 min-w-0">
<div
class="p-2 rounded-lg {toast.status === 'success'
? 'bg-green-500/10 text-green-500'
? 'bg-green-500/10 text-green-700 dark:text-green-400'
: toast.status === 'error'
? 'bg-destructive/10 text-destructive'
: 'bg-primary/10 text-primary'}"
@@ -56,17 +56,24 @@
</div>
<div class="min-w-0">
<p class="text-xs font-bold truncate">{toast.assetName}</p>
<p class="text-[10px] text-muted-foreground">
{#if toast.status === 'downloading'}
Downloading ({toast.progress}%)
{:else if toast.status === 'verifying'}
Verifying Checksum...
{:else if toast.status === 'success'}
Verified & Downloading
{:else if toast.status === 'error'}
Verification Failed
<div class="flex items-center gap-2">
<p class="text-[10px] text-muted-foreground">
{#if toast.status === 'downloading'}
Downloading ({toast.progress}%)
{:else if toast.status === 'verifying'}
Verifying Checksum...
{:else if toast.status === 'success'}
Verified & Downloading
{:else if toast.status === 'error'}
Verification Failed
{/if}
</p>
{#if toast.status === 'downloading' && toast.speed}
<span class="text-[9px] px-1 rounded bg-muted text-muted-foreground font-medium">
{toast.speed}
</span>
{/if}
</p>
</div>
</div>
</div>
<div class="flex items-center gap-1">
@@ -112,7 +119,7 @@
<div class="flex gap-2">
<div class="mt-0.5">
{#if step.status === 'success'}
<CheckCircle2 class="w-2.5 h-2.5 text-green-500" />
<CheckCircle2 class="w-2.5 h-2.5 text-green-700 dark:text-green-400" />
{:else if step.status === 'error'}
<ShieldAlert class="w-2.5 h-2.5 text-destructive" />
{:else}

View File

@@ -0,0 +1,217 @@
<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" />
Documentation
</div>
<h1 class="text-4xl font-black tracking-tight sm:text-5xl">How Verification Works</h1>
<p class="text-xl text-muted-foreground max-w-2xl mx-auto">
Software Station uses cutting-edge browser technology to ensure your downloads are 100%
authentic and untampered.
</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">The Simple Explanation</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">It's a Digital Lock</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Every file has a unique "digital fingerprint" (SHA256). Before you save a file, our
verifier recalculates that fingerprint inside your browser to make sure it matches the
developer's original version.
</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">Runs in a Sandbox</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
The verification happens in a "sandbox" a safe, isolated area in your browser. This means
the file is checked for safety before it ever touches your computer's permanent storage.
</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">Source Verification</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
We verify the connection to the source using industrial-grade security (TLS). This ensures
you are talking directly to the real repository and not an impostor.
</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">Know the Authors</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
Every release shows the actual contributors from the Gitea repository. You can see exactly
who wrote the code you are about to download.
</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">Technical Deep-Dive</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">
The verifier is written in **Go** and compiled to **WebAssembly**. By using WASM instead
of standard JavaScript, we leverage the Go standard library's <code>crypto/sha256</code>
implementation, ensuring high-performance cryptographic operations that are resistant to
JS-level prototype pollution or environment manipulation.
</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 Architecture
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
Software Station operates on a zero-trust model regarding the server's response. The
browser fetches the raw binary stream into a <code>Uint8Array</code>. This buffer is then passed
directly into the WASM memory space. The hash calculation is performed **client-side**,
meaning even if the server were compromised and serving malicious binaries, the verifier
would detect the hash mismatch immediately.
</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" />
Verification 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">
Binary is streamed from the source (Gitea/S3) via a secure TLS tunnel.
</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 engine reads the byte stream and computes the SHA256 digest locally.
</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">
The calculated digest is compared against the cryptographically signed manifest.
</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">
Only upon successful match is the <code>Blob</code> object created and the download triggered.
</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" />
Coming Soon
</h2>
<p class="text-muted-foreground">
We are constantly working to improve transparency. Our next milestones include:
</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">Automatic SBOM Generation</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 Storage Verification</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-Signature Verification</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">Reproducible Build Checks</span>
</div>
</div>
</div>
</div>

View File

@@ -8,6 +8,10 @@ const storedVerifierDisabled = isBrowser
export const verifierDisabled = writable<boolean>(storedVerifierDisabled);
export const verifierGloballyDisabled = isBrowser
? (window as any).VERIFIER_GLOBALLY_DISABLED === true
: false;
if (isBrowser) {
verifierDisabled.subscribe((value) => {
localStorage.setItem('verifier_disabled', String(value));

View File

@@ -21,6 +21,7 @@ export interface SourceSecurity {
export interface Release {
tag_name: string;
body?: string;
created_at: string;
assets: Asset[];
contributors?: Contributor[];
security?: SourceSecurity;

View File

@@ -12,6 +12,7 @@ export interface VerificationToast {
steps: VerificationStep[];
errorMessage?: string;
expanded?: boolean;
speed?: string;
}
export const verificationToasts = writable<VerificationToast[]>([]);
@@ -65,14 +66,34 @@ export async function backgroundVerify(assetName: string, assetUrl: string, expe
let receivedLength = 0;
const chunks = [];
let startTime = Date.now();
let lastUpdate = startTime;
let lastLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
if (total) {
updateToast(id, { progress: Math.round((receivedLength / total) * 100) });
const now = Date.now();
if (now - lastUpdate > 500) {
// Update speed every 500ms
const duration = (now - lastUpdate) / 1000;
const bytesSinceLast = receivedLength - lastLength;
const speedBytesPerSec = bytesSinceLast / duration;
const speedMBps = (speedBytesPerSec / (1024 * 1024)).toFixed(1);
const updates: Partial<VerificationToast> = {
speed: `${speedMBps} MB/s`,
};
if (total) {
updates.progress = Math.round((receivedLength / total) * 100);
}
updateToast(id, updates);
lastUpdate = now;
lastLength = receivedLength;
}
}

View File

@@ -4,6 +4,19 @@ export async function loadVerifier() {
if (typeof window === 'undefined') return null;
if ((window as any).verifySHA256) return (window as any).verifySHA256;
// Dynamically load wasm_exec.js if Go is not defined
if (!(window as any).Go) {
await new Promise<void>((resolve, reject) => {
const script = document.createElement('script');
script.src = '/verifier/wasm_exec.js';
script.integrity = 'sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce';
script.crossOrigin = 'anonymous';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load WASM executor script'));
document.head.appendChild(script);
});
}
const go = new (window as any).Go();
const result = await WebAssembly.instantiateStreaming(
fetch('/verifier/verifier.wasm'),

View File

@@ -4,9 +4,10 @@
import { onMount, setContext } from 'svelte';
import { Moon, Sun, Languages, Rss, ShieldOff, Shield } from 'lucide-svelte';
import { locale, waitLocale, locales, t } from 'svelte-i18n';
import { page } from '$app/state';
import { version } from '../../package.json';
import VerificationToasts from '$lib/components/VerificationToasts.svelte';
import { verifierDisabled } from '$lib/settingsStore';
import { verifierDisabled, verifierGloballyDisabled } from '$lib/settingsStore';
let { children } = $props();
@@ -54,7 +55,7 @@
class="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"
>
<div class="flex items-center gap-3">
<img src="/logo.png" alt="Quad4 Logo" class="w-8 h-8 rounded-md" />
<img src="/logo.png" alt="Quad4 Logo" width="32" height="32" class="w-8 h-8 rounded-md" />
<a href="/" class="text-xl font-bold tracking-tight">Software Station</a>
<span class="text-muted-foreground mx-1">|</span>
<a
@@ -63,24 +64,29 @@
class="text-sm font-medium hover:text-primary transition-colors">Quad4</a
>
<span class="text-muted-foreground mx-1">|</span>
<a href="/api-docs" class="text-sm font-medium hover:text-primary transition-colors"
>API</a
<a
href="/docs"
class="text-sm font-medium {page.url.pathname.startsWith('/docs')
? 'text-primary'
: 'hover:text-primary'} transition-colors">Docs</a
>
</div>
<div class="flex items-center gap-4">
<button
onclick={() => ($verifierDisabled = !$verifierDisabled)}
class="p-2 rounded-lg hover:bg-accent transition-colors {$verifierDisabled
? 'text-destructive'
: 'text-muted-foreground'}"
title={$verifierDisabled ? 'Verifier Disabled' : 'Verifier Enabled'}
>
{#if $verifierDisabled}
<ShieldOff class="w-5 h-5" />
{:else}
<Shield class="w-5 h-5" />
{/if}
</button>
{#if !verifierGloballyDisabled}
<button
onclick={() => ($verifierDisabled = !$verifierDisabled)}
class="hidden sm:flex p-2 rounded-lg hover:bg-accent transition-colors {$verifierDisabled
? 'text-destructive'
: 'text-muted-foreground'}"
title={$verifierDisabled ? 'Verifier Disabled' : 'Verifier Enabled'}
>
{#if $verifierDisabled}
<ShieldOff class="w-5 h-5" />
{:else}
<Shield class="w-5 h-5" />
{/if}
</button>
{/if}
<div class="relative group flex items-center">
<Languages
class="w-5 h-5 text-muted-foreground group-hover:text-primary transition-colors cursor-pointer pointer-events-none"
@@ -113,70 +119,96 @@
</div>
</nav>
<main class="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 py-6">
<main
class="mx-auto {page.url.pathname.startsWith('/docs')
? 'w-full h-[calc(100vh-64px)] overflow-hidden'
: 'max-w-[1600px] px-4 sm:px-6 lg:px-8 py-6'}"
>
{@render children()}
</main>
<VerificationToasts />
<footer class="border-t border-border mt-auto pt-6 pb-4">
<div class="max-w-[1600px] mx-auto px-4 text-center space-y-3">
<div class="flex flex-wrap justify-center gap-x-6 gap-y-2 text-sm font-medium">
<a
href="/legal/privacy"
class="text-muted-foreground hover:text-primary transition-colors"
>
{$t('common.privacy')}
</a>
<a href="/legal/terms" class="text-muted-foreground hover:text-primary transition-colors">
{$t('common.terms')}
</a>
<a
href="/legal/disclaimer"
class="text-muted-foreground hover:text-primary transition-colors"
>
{$t('common.disclaimer')}
</a>
<a href="/api-docs" class="text-muted-foreground hover:text-primary transition-colors">
API Docs
</a>
<a
href="/api/rss"
target="_blank"
class="flex items-center gap-1 text-muted-foreground transition-colors hover:text-orange-500"
title="Global RSS Feed"
>
<Rss class="h-3.5 w-3.5" />
<span>RSS</span>
</a>
{#if !verifierGloballyDisabled}
<button
onclick={() => ($verifierDisabled = !$verifierDisabled)}
class="sm:hidden fixed bottom-6 right-6 z-[100] p-4 rounded-full bg-primary text-primary-foreground shadow-2xl shadow-primary/30 transition-all active:scale-95 {$verifierDisabled
? 'bg-destructive shadow-destructive/30'
: ''}"
aria-label={$verifierDisabled ? 'Enable Verifier' : 'Disable Verifier'}
>
{#if $verifierDisabled}
<ShieldOff class="w-6 h-6" />
{:else}
<Shield class="w-6 h-6" />
{/if}
</button>
{/if}
{#if !page.url.pathname.startsWith('/docs')}
<footer class="border-t border-border mt-auto pt-6 pb-4">
<div class="max-w-[1600px] mx-auto px-4 text-center space-y-3">
<div class="flex flex-wrap justify-center gap-x-6 gap-y-2 text-sm font-medium">
<a
href="/legal/privacy"
class="text-muted-foreground hover:text-primary transition-colors"
>
{$t('common.privacy')}
</a>
<a
href="/legal/terms"
class="text-muted-foreground hover:text-primary transition-colors"
>
{$t('common.terms')}
</a>
<a
href="/legal/disclaimer"
class="text-muted-foreground hover:text-primary transition-colors"
>
{$t('common.disclaimer')}
</a>
<a href="/docs" class="text-muted-foreground hover:text-primary transition-colors">
Docs
</a>
<a
href="/api/rss"
target="_blank"
class="flex items-center gap-1 text-muted-foreground transition-colors hover:text-orange-700 dark:hover:text-orange-500"
title="Global RSS Feed"
>
<Rss class="h-3.5 w-3.5" />
<span>RSS</span>
</a>
</div>
<div class="text-sm text-muted-foreground">
&copy; {new Date().getFullYear()}
<a href="https://quad4.io" class="hover:text-primary underline decoration-dotted"
>Quad4</a
>.
{$t('common.rightsReserved')}
</div>
<div class="text-[10px] text-muted-foreground/80">
<a
href="https://git.quad4.io/Quad4-Software/software-station"
target="_blank"
class="hover:text-primary transition-colors"
>
{$t('common.openSource')}
</a>
<span class="mx-1">-</span>
<a
href="https://git.quad4.io/Quad4-Software/software-station"
target="_blank"
class="hover:text-primary transition-colors"
>
{$t('common.mit')}
</a>
<span class="mx-1">-</span>
v{version}
</div>
</div>
<div class="text-sm text-muted-foreground">
&copy; {new Date().getFullYear()}
<a href="https://quad4.io" class="hover:text-primary underline decoration-dotted">Quad4</a
>.
{$t('common.rightsReserved')}
</div>
<div class="text-[10px] text-muted-foreground/60">
<a
href="https://git.quad4.io/Quad4-Software/software-station"
target="_blank"
class="hover:text-primary transition-colors"
>
{$t('common.openSource')}
</a>
<span class="mx-1">-</span>
<a
href="https://git.quad4.io/Quad4-Software/software-station"
target="_blank"
class="hover:text-primary transition-colors"
>
{$t('common.mit')}
</a>
<span class="mx-1">-</span>
v{version}
</div>
</div>
</footer>
</footer>
{/if}
</div>
{:else}
<div class="min-h-screen bg-background flex items-center justify-center">

View File

@@ -42,13 +42,18 @@
);
</script>
<div class="space-y-8">
<SearchBar bind:searchQuery />
<div class="space-y-8 min-h-[600px]">
<div class="h-[56px]">
<!-- Reserve space for SearchBar -->
<SearchBar bind:searchQuery />
</div>
{#if loading}
<div class="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-6">
{#each Array(6) as _}
<div class="flex flex-col rounded-xl border border-border bg-card overflow-hidden">
<div
class="flex flex-col h-[400px] rounded-xl border border-border bg-card overflow-hidden"
>
<div class="p-6 flex-1 space-y-4">
<div class="flex items-start justify-between">
<div class="w-10 h-10 rounded-lg bg-muted animate-pulse"></div>
@@ -74,12 +79,16 @@
<div class="h-10 w-full bg-muted rounded-md animate-pulse"></div>
</div>
</div>
<div class="px-4 pb-4 pt-2 flex items-center justify-between border-t border-border/50">
<div class="h-3 w-32 bg-muted rounded animate-pulse"></div>
<div class="h-3 w-12 bg-muted rounded animate-pulse"></div>
</div>
</div>
{/each}
</div>
{:else if error}
<div class="text-center py-20 bg-destructive/5 rounded-2xl border border-destructive/10">
<AlertCircle class="w-16 h-16 mx-auto text-destructive opacity-50 mb-4" />
<AlertCircle class="w-16 h-16 mx-auto text-destructive opacity-70 mb-4" />
<h3 class="text-xl font-bold text-destructive">{$t('common.errorTitle')}</h3>
<p class="text-muted-foreground mt-2 max-w-md mx-auto">{$t('common.errorMessage')}</p>
<button
@@ -92,7 +101,7 @@
</div>
{:else if filteredSoftware.length === 0}
<div class="text-center py-20">
<Package class="w-12 h-12 mx-auto text-muted-foreground opacity-50 mb-4" />
<Package class="w-12 h-12 mx-auto text-muted-foreground opacity-70 mb-4" />
<h3 class="text-lg font-medium">{$t('common.noSoftware')}</h3>
<p class="text-muted-foreground">{$t('common.tryAdjusting')}</p>
</div>

View File

@@ -209,7 +209,7 @@
>{endpoint.method}</span
>
<code
class="px-2 py-1 rounded bg-muted text-muted-foreground text-xs font-mono border border-border/50"
class="px-2 py-1 rounded bg-muted text-foreground text-xs font-mono border border-border/50"
>{endpoint.path}</code
>
</div>
@@ -230,7 +230,7 @@
endpoint.id
) === lang
? 'text-primary'
: 'text-zinc-500 hover:text-zinc-300'}"
: 'text-zinc-400 hover:text-zinc-200'}"
>
{lang}
</button>
@@ -263,7 +263,7 @@
<div
class="px-4 py-2 bg-zinc-900 border-b border-white/5 flex items-center justify-between"
>
<span class="text-[10px] text-zinc-500 font-bold uppercase tracking-widest"
<span class="text-[10px] text-zinc-400 font-bold uppercase tracking-widest"
>Live Response</span
>
<div class="flex gap-1">

View File

@@ -0,0 +1,171 @@
<script lang="ts">
import { page } from '$app/state';
import { BookOpen, Shield, Terminal, Menu, X, Search } from 'lucide-svelte';
import { slide } from 'svelte/transition';
import { t } from 'svelte-i18n';
let { children } = $props();
const docs = [
{
title: 'Verification',
slug: 'verification',
icon: Shield,
description: 'How verification works',
comingSoon: false,
},
{
title: 'API Reference',
slug: 'api',
icon: Terminal,
description: 'Developer documentation',
comingSoon: false,
},
];
let isMobileMenuOpen = $state(false);
let searchQuery = $state('');
const filteredDocs = $derived(
docs.filter(
(doc) =>
doc.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
doc.description.toLowerCase().includes(searchQuery.toLowerCase())
)
);
function toggleMobileMenu() {
isMobileMenuOpen = !isMobileMenuOpen;
}
</script>
<div class="h-full bg-background flex flex-col lg:flex-row overflow-hidden">
<!-- Sidebar -->
<aside
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">
<div class="p-2 rounded-xl bg-primary/10 text-primary">
<BookOpen class="w-6 h-6" />
</div>
<div>
<h2 class="font-black tracking-tight text-xl">Docs</h2>
<p class="text-[10px] text-muted-foreground uppercase font-bold tracking-widest">
Software Station
</p>
</div>
</div>
<div class="relative mb-6">
<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>
<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}
<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')
? '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}
<p class="text-center py-8 text-xs text-muted-foreground">No documentation found.</p>
{/if}
</nav>
</div>
</aside>
<!-- Mobile Header -->
<div
class="lg:hidden flex items-center justify-between p-4 border-b border-border bg-card/50 backdrop-blur-xl sticky top-0 z-50"
>
<div class="flex items-center gap-2">
<BookOpen class="w-5 h-5 text-primary" />
<span class="font-bold">Docs</span>
</div>
<button
onclick={toggleMobileMenu}
class="p-2 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
>
{#if isMobileMenuOpen}
<X class="w-5 h-5" />
{:else}
<Menu class="w-5 h-5" />
{/if}
</button>
</div>
<!-- Mobile Navigation -->
{#if isMobileMenuOpen}
<div
transition:slide
class="lg:hidden border-b border-border bg-card/50 backdrop-blur-xl z-40 overflow-hidden"
>
<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}
<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">{doc.title}</span>
</a>
{/if}
{/each}
</nav>
</div>
{/if}
<!-- Main Content -->
<main class="flex-1 overflow-y-auto">
<div class="max-w-4xl mx-auto py-12 px-6 lg:px-12">
{@render children()}
</div>
</main>
</div>
<style>
:global(.prose) {
max-width: none;
}
:global(.prose strong) {
color: hsl(var(--primary));
font-weight: 800;
}
</style>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
onMount(() => {
goto('/docs/verification', { replaceState: true });
});
</script>
<div class="flex items-center justify-center min-h-[50vh]">
<div class="animate-pulse text-muted-foreground font-medium">Loading documentation...</div>
</div>

View File

@@ -0,0 +1,10 @@
<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}
</div>

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,322 @@
<script lang="ts">
import {
Terminal,
Code2,
Globe,
Cpu,
Database,
Play,
Copy,
Check,
Shield,
Loader2,
Download as DownloadIcon,
Box,
Zap,
} from 'lucide-svelte';
import CodeBlock from '$lib/components/CodeBlock.svelte';
let responseData = $state<Record<string, any>>({});
let loadingPaths = $state<Record<string, boolean>>({});
let copied = $state(false);
let selectedLanguages = $state<Record<string, string>>({});
const endpoints = [
{
id: 'software',
name: 'Get Software List',
path: '/api/software',
method: 'GET',
description: 'Fetches the complete list of available software, releases, and assets.',
examples: {
bash: 'curl https://software.quad4.io/api/software',
go: `package main\n\nimport (\n\t"fmt"\n\t"net/http"\n\t"io/io"\n)\n\nfunc main() {\n\tresp, _ := http.Get("https://software.quad4.io/api/software")\n\tbody, _ := io.ReadAll(resp.Body)\n\tfmt.Println(string(body))\n}`,
python: `import requests\n\nresponse = requests.get("https://software.quad4.io/api/software")\nprint(response.json())`,
javascript: `const response = await fetch("https://software.quad4.io/api/software");\nconst data = await response.json();\nconsole.log(data);`,
},
},
{
id: 'download',
name: 'Download Asset',
path: '/api/download',
method: 'GET',
description:
'Proxy a download from Gitea. Requires repository and asset name query parameters.',
examples: {
bash: 'curl -L "https://software.quad4.io/api/download?repo=webnews&asset=web-news-linux-amd64"',
go: `package main\n\nimport "net/http"\n\nfunc main() {\n\t// Use query params to specify repo and asset\n\thttp.Get("https://software.quad4.io/api/download?repo=webnews&asset=web-news-linux-amd64")\n}`,
python: `import requests\n\nparams = {"repo": "webnews", "asset": "web-news-linux-amd64"}\nresponse = requests.get("https://software.quad4.io/api/download", params=params)`,
javascript: `const url = new URL("https://software.quad4.io/api/download");\nurl.searchParams.set("repo", "webnews");\nurl.searchParams.set("asset", "web-news-linux-amd64");\nwindow.location.href = url.toString();`,
},
},
{
id: 'stats',
name: 'Get Global Stats',
path: '/api/stats',
method: 'GET',
description:
'Retrieves global platform statistics including unique users and data transferred.',
examples: {
bash: 'curl https://software.quad4.io/api/stats',
go: `package main\n\nimport (\n\t"fmt"\n\t"net/http"\n)\n\nfunc main() {\n\tresp, _ := http.Get("https://software.quad4.io/api/stats")\n\tfmt.Println(resp.Status)\n}`,
python: `import requests\n\nresponse = requests.get("https://software.quad4.io/api/stats")\nprint(response.status_code)`,
javascript: `fetch("https://software.quad4.io/api/stats")\n .then(res => res.json())\n .then(console.log);`,
},
},
{
id: 'rss',
name: 'RSS Feed',
path: '/api/rss',
method: 'GET',
description: 'Global RSS feed for all software updates.',
examples: {
bash: 'curl https://software.quad4.io/api/rss',
go: `// Fetch RSS XML\nresp, _ := http.Get("https://software.quad4.io/api/rss")`,
python: `import requests\nxml_data = requests.get("https://software.quad4.io/api/rss").text`,
javascript: `const xml = await fetch("https://software.quad4.io/api/rss").then(r => r.text());`,
},
},
];
async function runExample(id: string, path: string) {
loadingPaths[id] = true;
try {
const res = await fetch(path);
const contentType = res.headers.get('content-type');
if (contentType?.includes('application/json')) {
responseData[id] = await res.json();
} else {
// Handle non-JSON (like XML for RSS)
const text = await res.text();
responseData[id] = text;
}
} catch {
responseData[id] = {
error:
'Failed to fetch. This endpoint might return non-JSON data or require specific parameters.',
};
} finally {
loadingPaths[id] = false;
}
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
copied = true;
setTimeout(() => (copied = false), 2000);
}
function getLanguage(id: string) {
return selectedLanguages[id] || 'bash';
}
function setLanguage(id: string, lang: string) {
selectedLanguages[id] = lang;
}
</script>
<div class="space-y-12">
<div class="space-y-4 text-center">
<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"
>
<Code2 class="w-3 h-3" />
Developer API
</div>
<h1 class="text-4xl font-black tracking-tight sm:text-5xl">Integrate with Software Station</h1>
<p class="text-xl text-muted-foreground max-w-2xl mx-auto">
Simple, powerful, and transparent APIs to fetch software metadata and platform statistics.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="p-6 rounded-2xl bg-card border border-border space-y-3 shadow-sm">
<div class="p-2 w-fit rounded-lg bg-blue-500/10 text-blue-500">
<Globe class="w-5 h-5" />
</div>
<h3 class="font-bold">CORS Enabled</h3>
<p class="text-sm text-muted-foreground">
All endpoints are CORS-enabled, allowing you to build frontend integrations directly.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-3 shadow-sm">
<div class="p-2 w-fit rounded-lg bg-green-500/10 text-green-500">
<Cpu class="w-5 h-5" />
</div>
<h3 class="font-bold">Real-time Data</h3>
<p class="text-sm text-muted-foreground">
Data is fetched directly from source and cached for optimal performance.
</p>
</div>
<div class="p-6 rounded-2xl bg-card border border-border space-y-3 shadow-sm">
<div class="p-2 w-fit rounded-lg bg-purple-500/10 text-purple-500">
<Shield class="w-5 h-5" />
</div>
<h3 class="font-bold">Transparent</h3>
<p class="text-sm text-muted-foreground">
Every asset includes verified SHA256 checksums for programmatic verification.
</p>
</div>
</div>
<div class="space-y-8">
<h2 class="text-2xl font-bold flex items-center gap-2">
<Terminal class="w-6 h-6 text-primary" />
API Reference
</h2>
<div class="space-y-6">
{#each endpoints as endpoint}
<div
class="group bg-card border border-border rounded-2xl overflow-hidden hover:border-primary/30 transition-all duration-300 shadow-sm"
>
<div class="p-6">
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-6">
<div class="space-y-1">
<h3 class="text-xl font-bold flex items-center gap-2">
{#if endpoint.id === 'download'}
<DownloadIcon class="w-5 h-5 text-primary" />
{:else if endpoint.id === 'software'}
<Box class="w-5 h-5 text-primary" />
{:else if endpoint.id === 'stats'}
<Zap class="w-5 h-5 text-primary" />
{:else}
<Code2 class="w-5 h-5 text-primary" />
{/if}
{endpoint.name}
</h3>
<p class="text-sm text-muted-foreground leading-relaxed">{endpoint.description}</p>
</div>
<div class="flex items-center gap-2 shrink-0">
{#if endpoint.id !== 'download'}
<button
onclick={() => runExample(endpoint.id, endpoint.path)}
disabled={loadingPaths[endpoint.id]}
class="flex items-center gap-2 px-3 py-1.5 bg-primary/10 text-primary hover:bg-primary hover:text-white rounded-lg text-xs font-bold transition-all disabled:opacity-50"
>
{#if loadingPaths[endpoint.id]}
<Loader2 class="w-3 h-3 animate-spin" />
{:else}
<Play class="w-3 h-3" />
{/if}
Run Live
</button>
{/if}
<span
class="px-2 py-1 rounded bg-green-500/10 text-green-600 text-[10px] font-black uppercase border border-green-500/20"
>{endpoint.method}</span
>
<code
class="px-2 py-1 rounded bg-muted text-foreground text-xs font-mono border border-border/50"
>{endpoint.path}</code
>
</div>
</div>
<div class="space-y-4">
<div
class="bg-zinc-950 dark:bg-zinc-950 rounded-xl overflow-hidden border border-zinc-800 shadow-inner"
>
<div
class="flex items-center justify-between px-4 py-2 bg-zinc-900/50 dark:bg-zinc-900/80 border-b border-white/5"
>
<div class="flex gap-4">
{#each Object.keys(endpoint.examples) as lang}
<button
onclick={() => setLanguage(endpoint.id, lang)}
class="text-[10px] font-bold uppercase tracking-widest transition-colors {getLanguage(
endpoint.id
) === lang
? 'text-primary'
: 'text-zinc-400 hover:text-zinc-200'}"
>
{lang}
</button>
{/each}
</div>
<button
onclick={() =>
copyToClipboard((endpoint.examples as any)[getLanguage(endpoint.id)])}
class="p-1.5 hover:bg-white/10 rounded-md transition-colors text-zinc-400"
title="Copy to clipboard"
>
{#if copied}
<Check class="w-3 h-3 text-green-500" />
{:else}
<Copy class="w-3 h-3" />
{/if}
</button>
</div>
<div class="p-4 overflow-x-auto min-h-[80px]">
<CodeBlock
code={(endpoint.examples as any)[getLanguage(endpoint.id)]}
language={getLanguage(endpoint.id) as any}
/>
</div>
</div>
{#if responseData[endpoint.id]}
<div class="animate-in fade-in slide-in-from-top-2 duration-300">
<div
class="bg-zinc-900/50 dark:bg-zinc-900/50 rounded-xl overflow-hidden border border-zinc-800"
>
<div
class="px-4 py-2 bg-zinc-900 dark:bg-zinc-950 border-b border-white/5 flex items-center justify-between"
>
<span class="text-[10px] text-zinc-400 font-bold uppercase tracking-widest"
>Live Response</span
>
<div class="flex gap-1">
<div class="w-2 h-2 rounded-full bg-red-500/20"></div>
<div class="w-2 h-2 rounded-full bg-yellow-500/20"></div>
<div class="w-2 h-2 rounded-full bg-green-500/20"></div>
</div>
</div>
<div
class="p-4 max-h-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-white/10"
>
<CodeBlock
code={typeof responseData[endpoint.id] === 'string'
? responseData[endpoint.id]
: JSON.stringify(responseData[endpoint.id], null, 2)}
language={typeof responseData[endpoint.id] === 'string' &&
responseData[endpoint.id].includes('<?xml')
? 'bash'
: 'json'}
/>
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
{/each}
</div>
</div>
<div class="p-8 rounded-3xl bg-primary/5 border border-primary/20 space-y-4 shadow-sm">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-primary/10 text-primary">
<Database class="w-6 h-6" />
</div>
<h2 class="text-2xl font-bold">Self-Hosting</h2>
</div>
<p class="text-muted-foreground">
Software Station is open source and can be self-hosted with ease. All API features are
available out of the box.
</p>
<div class="flex gap-4 pt-2">
<a
href="https://git.quad4.io/Quad4-Software/software-station"
target="_blank"
rel="noopener noreferrer"
class="px-6 py-2 bg-primary text-primary-foreground rounded-xl font-bold hover:opacity-90 transition-opacity flex items-center gap-2 shadow-lg shadow-primary/20"
>
<Code2 class="w-4 h-4" />
Gitea repository
</a>
</div>
</div>
</div>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { page } from '$app/state';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { FileText, Download, ArrowLeft } from 'lucide-svelte';
@@ -10,7 +9,15 @@
const docType = $derived(page.params.doc);
onMount(async () => {
$effect(() => {
if (docType) {
fetchDoc();
}
});
async function fetchDoc() {
loading = true;
error = false;
try {
const res = await fetch(`/api/legal?doc=${docType}`);
if (!res.ok) throw new Error('Failed to fetch');
@@ -20,7 +27,7 @@
} finally {
loading = false;
}
});
}
</script>
<div class="max-w-4xl mx-auto space-y-8 animate-in fade-in duration-500">

View File

Binary file not shown.

View File

@@ -1,11 +1,18 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { mdsvex } from 'mdsvex';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
extensions: ['.svelte', '.svx', '.md'],
preprocess: [
vitePreprocess(),
mdsvex({
extensions: ['.svx', '.md'],
}),
],
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.

View File

@@ -1,3 +1,5 @@
import typography from '@tailwindcss/typography';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
@@ -46,5 +48,5 @@ export default {
},
},
},
plugins: [],
plugins: [typography],
};

View File

@@ -1,9 +1,10 @@
Legal Disclaimer
Version: 2.0
Version: 2.1
Last Updated: December 27, 2025
1. No Warranty: The software and service are provided "as-is" without any express or implied warranties, including but not limited to the implied warranties of merchantability or fitness for a particular purpose.
2. Liability: Quad4 and its contributors shall not be held liable for any damages (including data loss, hardware failure, or financial loss) arising from the use of the service or software downloaded from this station.
3. Integrity: While we proxy assets from upstream sources and provide SHA256 checksums for verification, users are responsible for verifying the integrity of any downloaded file before execution.
4. Upstream Content: This station proxies content from external software repositories and storage sources (e.g., Gitea, S3, SFTP) selected by the operator. While the operator selects these sources, we do not control and are not responsible for the content, security, or privacy practices of the original developers or hosting providers.
3. Integrity & Verification: We provide a client-side verification engine powered by WebAssembly (WASM). While this system recalculates SHA256 checksums in real-time within your browser and checks them against cryptographically signed manifests, successful verification does not guarantee the software is free of bugs, vulnerabilities, or malicious logic intended by the original author. It only ensures the file matches the developer's intended release.
4. Security Features: We utilize Subresource Integrity (SRI) for all critical frontend assets. This ensures that the code running the verifier itself has not been tampered with.
5. Upstream Content: This station proxies content from external software repositories and storage sources (e.g., Gitea, S3, SFTP) selected by the operator. While the operator selects these sources, we do not control and are not responsible for the content, security, or privacy practices of the original developers or hosting providers.

View File

@@ -1,9 +1,12 @@
Privacy Policy
Version: 2.0
Version: 2.1
Last Updated: December 27, 2025
We value your privacy and operate in compliance with the General Data Protection Regulation (GDPR). This service is designed with "privacy by design" principles, ensuring minimal data collection.
Client-Side Verification:
Software Station employs a "Zero-Trust" verification model. All file integrity checks (SHA256 hashing) are performed locally in your browser using a WebAssembly (WASM) engine. No part of the downloaded software file is ever transmitted back to our servers during the verification process. The only data sent to our servers during a download is the standard request for the asset itself.
Data Processing & Anonymization:
To protect our infrastructure and ensure fair resource distribution, we process certain technical metadata. Crucially, we do not store raw IP addresses or personally identifiable information (PII).

View File

@@ -1,13 +1,14 @@
Terms of Service
Version: 2.0
Version: 2.1
Last Updated: December 27, 2025
By using this software station, you agree to the following terms and acknowledge our Privacy Policy:
1. Fair Use: You will not attempt to bypass rate limits, spoof fingerprints, or scrape the station excessively. Automated access must respect the rate limits provided.
2. Anti-Abuse: We employ advanced fingerprinting and stateful tracking (via cookies) to prevent malicious activity. We reserve the right to block any anonymous fingerprint or IP range that engages in abuse.
3. Distribution: The software provided here is mirrored from various upstream sources. The original licenses for individual projects apply and must be respected.
4. Disclaimer: THE SERVICE AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
3. Verification System: You acknowledge that the client-side verification system requires WebAssembly (WASM) to be enabled in your browser. Attempting to tamper with the verifier, its manifests, or the Subresource Integrity (SRI) hashes is strictly prohibited.
4. Distribution: The software provided here is mirrored from various upstream sources. The original licenses for individual projects apply and must be respected.
5. Disclaimer: THE SERVICE AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
Misuse of the service may lead to permanent blocking of your security identifier.

22
main.go
View File

@@ -42,6 +42,7 @@ func main() {
uaBlocklistPath := flag.String("ua-blocklist", getEnv("UA_BLOCKLIST_PATH", "ua-blocklist.txt"), "Path to ua-blocklist.txt (optional)")
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")
updateInterval := flag.Duration("u", 1*time.Hour, "Software update interval")
flag.Parse()
@@ -125,6 +126,19 @@ func main() {
path = strings.TrimPrefix(path, "/")
}
// Set Cache-Control headers for static assets
if *isProd {
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") ||
strings.HasSuffix(path, ".svg") || strings.HasSuffix(path, ".wasm") {
// Other static assets (1 week)
w.Header().Set("Cache-Control", "public, max-age=604800")
}
}
f, err := contentStatic.Open(path)
if err != nil {
if strings.HasPrefix(r.URL.Path, "/api") {
@@ -137,7 +151,13 @@ func main() {
http.Error(w, "Index not found", http.StatusInternalServerError)
return
}
http.ServeContent(w, r, "index.html", time.Unix(0, 0), strings.NewReader(string(indexData)))
// Inject global configuration
html := string(indexData)
configJS := fmt.Sprintf("<script>window.VERIFIER_GLOBALLY_DISABLED = %v;</script>", *disableVerifier)
html = strings.Replace(html, "</head>", configJS+"</head>", 1)
http.ServeContent(w, r, "index.html", time.Unix(0, 0), strings.NewReader(html))
return
}
if err := f.Close(); err != nil {