refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {ICaptchaProvider} from '@fluxer/captcha/src/ICaptchaProvider';
import {HcaptchaProvider} from '@fluxer/captcha/src/providers/HcaptchaProvider';
import type {HttpCaptchaProviderOptions} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
import type {RecaptchaProviderOptions} from '@fluxer/captcha/src/providers/RecaptchaProvider';
import {RecaptchaProvider} from '@fluxer/captcha/src/providers/RecaptchaProvider';
import {TestCaptchaProvider} from '@fluxer/captcha/src/providers/TestProvider';
import {TurnstileProvider} from '@fluxer/captcha/src/providers/TurnstileProvider';
import {UnavailableCaptchaProvider} from '@fluxer/captcha/src/providers/UnavailableCaptchaProvider';
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
interface BaseCaptchaProviderFactoryParams {
logger?: LoggerInterface;
}
interface CreateUnavailableCaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
mode: 'unavailable';
}
interface CreateTestCaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
mode: 'test';
}
interface CreateHcaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
mode: 'hcaptcha';
secretKey: string;
timeoutMs?: number;
userAgent?: string;
fetchFn?: typeof fetch;
}
interface CreateTurnstileProviderParams extends BaseCaptchaProviderFactoryParams {
mode: 'turnstile';
secretKey: string;
timeoutMs?: number;
userAgent?: string;
fetchFn?: typeof fetch;
}
interface CreateRecaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
mode: 'recaptcha';
secretKey: string;
minimumScore?: number;
timeoutMs?: number;
userAgent?: string;
fetchFn?: typeof fetch;
}
export type CreateCaptchaProviderParams =
| CreateUnavailableCaptchaProviderParams
| CreateTestCaptchaProviderParams
| CreateHcaptchaProviderParams
| CreateTurnstileProviderParams
| CreateRecaptchaProviderParams;
function buildHttpOptions(
params: CreateHcaptchaProviderParams | CreateTurnstileProviderParams | CreateRecaptchaProviderParams,
): HttpCaptchaProviderOptions {
return {
secretKey: params.secretKey,
logger: params.logger,
timeoutMs: params.timeoutMs,
userAgent: params.userAgent,
fetchFn: params.fetchFn,
};
}
export function createCaptchaProvider(params: CreateCaptchaProviderParams): ICaptchaProvider {
if (params.mode === 'test') {
return new TestCaptchaProvider();
}
if (params.mode === 'hcaptcha') {
return new HcaptchaProvider(buildHttpOptions(params));
}
if (params.mode === 'turnstile') {
return new TurnstileProvider(buildHttpOptions(params));
}
if (params.mode === 'recaptcha') {
const options: RecaptchaProviderOptions = {
...buildHttpOptions(params),
minimumScore: params.minimumScore,
};
return new RecaptchaProvider(options);
}
return new UnavailableCaptchaProvider();
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
export interface VerifyCaptchaParams {
token: string;
remoteIp?: string;
}
export type CaptchaProviderType = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'test' | 'unavailable';
export interface ICaptchaProvider {
readonly type: CaptchaProviderType;
verify(params: VerifyCaptchaParams): Promise<boolean>;
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {CaptchaProviderType} from '@fluxer/captcha/src/ICaptchaProvider';
import {HttpCaptchaProvider} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
export class HcaptchaProvider extends HttpCaptchaProvider {
readonly type: CaptchaProviderType = 'hcaptcha';
protected readonly verifyUrl = 'https://api.hcaptcha.com/siteverify';
protected readonly providerName = 'hCaptcha';
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {CaptchaProviderType, ICaptchaProvider, VerifyCaptchaParams} from '@fluxer/captcha/src/ICaptchaProvider';
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
import {ms} from 'itty-time';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; Fluxerbot/1.0; +https://fluxer.app)';
const DEFAULT_TIMEOUT = ms('10 seconds');
export interface HttpCaptchaProviderOptions {
secretKey: string;
logger?: LoggerInterface;
timeoutMs?: number;
userAgent?: string;
fetchFn?: typeof fetch;
}
interface CaptchaVerifyResponse {
success: boolean;
'error-codes'?: Array<string>;
hostname?: string;
challenge_ts?: string;
}
export abstract class HttpCaptchaProvider implements ICaptchaProvider {
abstract readonly type: CaptchaProviderType;
protected readonly secretKey: string;
protected readonly logger: LoggerInterface | undefined;
protected readonly timeoutMs: number;
protected readonly userAgent: string;
protected readonly fetchFn: typeof fetch;
protected abstract readonly verifyUrl: string;
protected abstract readonly providerName: string;
constructor(options: HttpCaptchaProviderOptions) {
this.secretKey = options.secretKey;
this.logger = options.logger;
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
this.fetchFn = options.fetchFn ?? fetch;
}
async verify({token, remoteIp}: VerifyCaptchaParams): Promise<boolean> {
try {
const body = new URLSearchParams();
body.append('secret', this.secretKey);
body.append('response', token);
if (remoteIp) {
body.append('remoteip', remoteIp);
}
const response = await this.fetchFn(this.verifyUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': this.userAgent,
},
body: body.toString(),
signal: AbortSignal.timeout(this.timeoutMs),
});
if (!response.ok) {
this.logger?.error({status: response.status}, `${this.providerName} verify request failed`);
return false;
}
const data = (await response.json()) as CaptchaVerifyResponse;
if (!data.success) {
this.logger?.warn({errorCodes: data['error-codes']}, `${this.providerName} verification failed`);
return false;
}
return this.validateResponse(data);
} catch (error) {
if (error instanceof Error && error.name === 'TimeoutError') {
this.logger?.error({}, `${this.providerName} verification timed out after ${this.timeoutMs}ms`);
} else {
this.logger?.error({error}, `Error verifying ${this.providerName} token`);
}
return false;
}
}
protected validateResponse(_data: CaptchaVerifyResponse): boolean {
return true;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {CaptchaProviderType} from '@fluxer/captcha/src/ICaptchaProvider';
import type {HttpCaptchaProviderOptions} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
import {HttpCaptchaProvider} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
const DEFAULT_MINIMUM_SCORE = 0.5;
interface RecaptchaVerifyResponse {
success: boolean;
'error-codes'?: Array<string>;
score?: number;
}
export interface RecaptchaProviderOptions extends HttpCaptchaProviderOptions {
minimumScore?: number;
}
export class RecaptchaProvider extends HttpCaptchaProvider {
readonly type: CaptchaProviderType = 'recaptcha';
protected readonly verifyUrl = 'https://www.google.com/recaptcha/api/siteverify';
protected readonly providerName = 'reCAPTCHA';
private readonly minimumScore: number;
constructor(options: RecaptchaProviderOptions) {
super(options);
this.minimumScore = options.minimumScore ?? DEFAULT_MINIMUM_SCORE;
}
protected override validateResponse(data: RecaptchaVerifyResponse): boolean {
if (data.score !== undefined && data.score < this.minimumScore) {
this.logger?.warn(
{score: data.score, minimumScore: this.minimumScore},
'reCAPTCHA score below minimum threshold',
);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {CaptchaProviderType, ICaptchaProvider, VerifyCaptchaParams} from '@fluxer/captcha/src/ICaptchaProvider';
export class TestCaptchaProvider implements ICaptchaProvider {
readonly type: CaptchaProviderType = 'test';
async verify(_params: VerifyCaptchaParams): Promise<boolean> {
return true;
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {CaptchaProviderType} from '@fluxer/captcha/src/ICaptchaProvider';
import {HttpCaptchaProvider} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
export class TurnstileProvider extends HttpCaptchaProvider {
readonly type: CaptchaProviderType = 'turnstile';
protected readonly verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
protected readonly providerName = 'Turnstile';
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {CaptchaProviderType, ICaptchaProvider, VerifyCaptchaParams} from '@fluxer/captcha/src/ICaptchaProvider';
export class UnavailableCaptchaProvider implements ICaptchaProvider {
readonly type: CaptchaProviderType = 'unavailable';
async verify(_params: VerifyCaptchaParams): Promise<boolean> {
return true;
}
}