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:
@@ -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
|
||||
|
||||
@@ -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
163
frontend/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(/("[^&]*")/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(/("[^&]*"|'[^&]*')/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(
|
||||
/("[^&]*"|'[^&]*'|`[^`]*`)/g,
|
||||
'<span class="text-green-400">$1</span>'
|
||||
|
||||
19
frontend/src/lib/components/Markdown.svelte
Normal file
19
frontend/src/lib/components/Markdown.svelte
Normal 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>
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
217
frontend/src/lib/docs/verification.svx
Normal file
217
frontend/src/lib/docs/verification.svx
Normal 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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface SourceSecurity {
|
||||
export interface Release {
|
||||
tag_name: string;
|
||||
body?: string;
|
||||
created_at: string;
|
||||
assets: Asset[];
|
||||
contributors?: Contributor[];
|
||||
security?: SourceSecurity;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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">
|
||||
© {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">
|
||||
© {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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
171
frontend/src/routes/docs/+layout.svelte
Normal file
171
frontend/src/routes/docs/+layout.svelte
Normal 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>
|
||||
12
frontend/src/routes/docs/+page.svelte
Normal file
12
frontend/src/routes/docs/+page.svelte
Normal 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>
|
||||
10
frontend/src/routes/docs/[slug]/+page.svelte
Normal file
10
frontend/src/routes/docs/[slug]/+page.svelte
Normal 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>
|
||||
14
frontend/src/routes/docs/[slug]/+page.ts
Normal file
14
frontend/src/routes/docs/[slug]/+page.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
322
frontend/src/routes/docs/api/+page.svelte
Normal file
322
frontend/src/routes/docs/api/+page.svelte
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
Binary file not shown.
@@ -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.
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
22
main.go
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user