From fe643d9831226226423d4a9179abc6a98ec86757 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Fri, 23 Oct 2026 18:34:09 +0200 Subject: [PATCH] Add client ALTCHA proof-of-work and Vite env flags for uploads. Expose enabled/max-number settings and wire altcha-lib for per-upload challenges. --- .env.example | 4 +- frontend/package-lock.json | 70 +++++++++++++++++++++++++++++++++++ frontend/package.json | 1 + frontend/src/config/index.ts | 6 +++ frontend/src/stores/altcha.ts | 70 +++++++++++++++++++++++++++++++++++ frontend/src/vite-env.d.ts | 2 + 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 frontend/src/stores/altcha.ts diff --git a/.env.example b/.env.example index 29a27b2..b342048 100644 --- a/.env.example +++ b/.env.example @@ -20,7 +20,7 @@ CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 ALTCHA_ENABLED=true ALTCHA_HMAC_KEY=change-me-use-long-random-secret -ALTCHA_MAX_NUMBER=50000 +ALTCHA_MAX_NUMBER=250000 ALTCHA_CHALLENGE_TTL_MS=600000 THROTTLE_TTL_MS=60000 @@ -35,6 +35,8 @@ VALIDATION_UPLOAD_JSON_BODY_PADDING_BYTES=2097152 # 2MiB VITE_API_BASE_URL=http://localhost:3000/api VITE_CLEARNET_DOMAIN=localhost:5173 VITE_ONION_HOSTNAME= +VITE_ALTCHA_ENABLED=true +VITE_ALTCHA_MAX_NUMBER=250000 VITE_UPLOAD_MAX_QUEUE_SIZE=10 VITE_VALIDATION_UPLOAD_MAX_VIEWS_MAX=1000000 VITE_VALIDATION_UPLOAD_MAX_IMAGE_BYTES=10485760 # 10MiB diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 496fac1..84a87e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@tailwindcss/vite": "^4.3.3", "@uswriting/exiftool": "1.0.9", + "altcha-lib": "^2.7.0", "axios": "^1.20.0", "dayjs": "^1.11.23", "hash-wasm": "^4.12.0", @@ -2071,6 +2072,75 @@ "dev": true, "license": "MIT" }, + "node_modules/altcha-lib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/altcha-lib/-/altcha-lib-2.7.0.tgz", + "integrity": "sha512-gnysjfIpeT6k3RLaWRd7xVvfs8U0a5DHIS95Xgwn8znKq5uIj6c/oTH8DtcdyJS/ZV3I5kYpyurvmJFb0eFWvA==", + "license": "MIT", + "bin": { + "altcha-lib": "bin/cli.mjs" + }, + "peerDependencies": { + "@fastify/cookie": "^11.0.0", + "@fastify/cors": "^11.0.0", + "@fastify/formbody": "^8.0.0 || ^9.0.0", + "@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0", + "@nestjs/platform-express": "^10.0.0 || ^11.0.0 || ^12.0.0", + "@nestjs/platform-fastify": "^10.0.0 || ^11.0.0 || ^12.0.0", + "@sveltejs/kit": "^2.0.0 || ^3.0.0", + "cookie-parser": "^1.0.0", + "cors": "^2.0.0", + "express": "^4.0.0 || ^5.0.0", + "fastify": "^5.0.0", + "h3": "^2.0.0", + "hono": "^4.0.0" + }, + "peerDependenciesMeta": { + "@fastify/cookie": { + "optional": true + }, + "@fastify/cors": { + "optional": true + }, + "@fastify/formbody": { + "optional": true + }, + "@nestjs/common": { + "optional": true + }, + "@nestjs/core": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/platform-fastify": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "cookie-parser": { + "optional": true + }, + "cors": { + "optional": true + }, + "express": { + "optional": true + }, + "fastify": { + "optional": true + }, + "h3": { + "optional": true + }, + "hono": { + "optional": true + } + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e5f88cb..906fa14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "dependencies": { "@tailwindcss/vite": "^4.3.3", "@uswriting/exiftool": "1.0.9", + "altcha-lib": "^2.7.0", "axios": "^1.20.0", "dayjs": "^1.11.23", "hash-wasm": "^4.12.0", diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts index 7a5ae00..4d3a652 100644 --- a/frontend/src/config/index.ts +++ b/frontend/src/config/index.ts @@ -10,10 +10,16 @@ const env = (key: string): string => { const envInt = (key: string): number => parseInt(env(key), 10); +const isEnabled = (key: string): boolean => env(key) === 'true'; + export const config = { baseUrl: env('VITE_API_BASE_URL'), clearnetDomain: env('VITE_CLEARNET_DOMAIN'), onionHostname: env('VITE_ONION_HOSTNAME'), + altcha: { + enabled: isEnabled('VITE_ALTCHA_ENABLED'), + maxNumber: envInt('VITE_ALTCHA_MAX_NUMBER') + }, validation: { upload: { maxQueueSize: envInt('VITE_UPLOAD_MAX_QUEUE_SIZE'), diff --git a/frontend/src/stores/altcha.ts b/frontend/src/stores/altcha.ts new file mode 100644 index 0000000..141df4f --- /dev/null +++ b/frontend/src/stores/altcha.ts @@ -0,0 +1,70 @@ +import { solveChallengeWorkers } from 'altcha-lib/v1'; +import type { Challenge, Payload } from 'altcha-lib/v1/types'; +import { defineStore } from 'pinia'; +import { config } from '@/config'; +import { api } from '@/plugins/axios'; +import { bytesToBase64 } from '@/utils/encoding/bytesToBase64'; +import AltchaSolveWorker from 'altcha-lib/v1/worker?worker'; + +export const useAltchaStore = defineStore('altcha', () => { + const runProofOfWork = async (): Promise => { + if (!config.altcha.enabled) { + return undefined; + } + + const challenge = await fetchChallenge(); + + return encodeSolvedChallenge(challenge); + }; + + const fetchChallenge = async (): Promise => { + const { data } = await api.get('/altcha/challenge'); + + return data; + }; + + const encodeSolvedChallenge = async ({ + algorithm, + challenge: challengeValue, + salt, + signature, + maxnumber + }: Challenge): Promise => { + const { maxNumber: fallbackMaxNumber } = config.altcha; + + const maxNumber = maxnumber ?? fallbackMaxNumber; + const cuncurrency = Math.min(8, Math.max(1, navigator.hardwareConcurrency ?? 4)); + + const solution = await solveChallengeWorkers( + () => new AltchaSolveWorker(), + cuncurrency, + challengeValue, + salt, + algorithm, + maxNumber + ); + + if (!solution) { + throw new Error('ALTCHA proof of work could not be solved'); + } + + const payload: Payload = { + algorithm, + challenge: challengeValue, + number: solution.number, + salt, + signature + }; + + const json = JSON.stringify(payload); + + const textEncoder = new TextEncoder(); + const bytes = textEncoder.encode(json); + + return bytesToBase64(bytes); + }; + + return { + runProofOfWork + }; +}); diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index aa316f0..806b5a2 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -4,6 +4,8 @@ interface ImportMetaEnv { readonly VITE_API_BASE_URL: string; readonly VITE_CLEARNET_DOMAIN: string; readonly VITE_ONION_HOSTNAME: string; + readonly VITE_ALTCHA_ENABLED: string; + readonly VITE_ALTCHA_MAX_NUMBER: string; readonly VITE_UPLOAD_MAX_QUEUE_SIZE: string; readonly VITE_VALIDATION_UPLOAD_MAX_VIEWS_MAX: string; readonly VITE_VALIDATION_UPLOAD_MAX_IMAGE_BYTES: string;