mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
feat(captcha): make ALTCHA the only captcha (#3035)
This commit is contained in:
1 parent
d433a039b5
commit
4f968bbc47
209 files changed
+4097
-8692
No files matched your search
@@ -11,10 +11,7 @@ import {ADMIN_ACL_COUNT, AdminAclType} from '@fluxer/schema/src/domains/admin/Ad
|
||||
import {AdminArchiveResponseSchema} from '@fluxer/schema/src/domains/admin/AdminArchiveSchemas';
|
||||
import {GuildAdminResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
||||
import {UserAdminResponseSchema} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {
|
||||
AltchaCaptchaConfigResponse,
|
||||
AltchaCaptchaConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/AltchaCaptchaSchemas';
|
||||
import {CaptchaConfigResponse, CaptchaConfigUpdateRequest} from '@fluxer/schema/src/domains/admin/CaptchaSchemas';
|
||||
import {
|
||||
DomainMigrationConfigResponse,
|
||||
DomainMigrationConfigUpdateRequest,
|
||||
@@ -33,10 +30,7 @@ import {
|
||||
ExperimentDeliveryConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||
import {
|
||||
InstanceCaptchaProviderSchema,
|
||||
InstanceRegistrationModeSchema,
|
||||
} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
|
||||
import {InstanceRegistrationModeSchema} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
|
||||
import {MessageResponseSchema} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {GiftCodeDurationTypeSchema} from '@fluxer/schema/src/domains/premium/GiftCodeSchemas';
|
||||
import {ChannelTypeSchema} from '@fluxer/schema/src/primitives/ChannelValidators';
|
||||
@@ -608,15 +602,6 @@ const InstanceIntegrationsResponse = z.object({
|
||||
api_key_set: z.boolean(),
|
||||
effective_available: z.boolean(),
|
||||
}),
|
||||
captcha: z.object({
|
||||
provider: InstanceCaptchaProviderSchema.nullable(),
|
||||
effective_provider: InstanceCaptchaProviderSchema,
|
||||
hcaptcha_site_key: z.string().nullable(),
|
||||
hcaptcha_secret_key_set: z.boolean(),
|
||||
turnstile_site_key: z.string().nullable(),
|
||||
turnstile_secret_key_set: z.boolean(),
|
||||
effective_enabled: z.boolean(),
|
||||
}),
|
||||
email: z.object({
|
||||
enabled: z.boolean().nullable(),
|
||||
effective_enabled: z.boolean(),
|
||||
@@ -651,7 +636,7 @@ export const InstanceConfigResponse = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigResponse,
|
||||
push_relay: PushRelayConfigResponse,
|
||||
domain_migration: DomainMigrationConfigResponse,
|
||||
altcha_captcha: AltchaCaptchaConfigResponse,
|
||||
captcha: CaptchaConfigResponse,
|
||||
experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
registration: InstanceRegistrationResponse,
|
||||
self_hosted: z.boolean(),
|
||||
@@ -683,7 +668,7 @@ export const InstanceConfigUpdateRequest = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigUpdateRequest.nullish(),
|
||||
push_relay: PushRelayConfigUpdateRequest.nullish(),
|
||||
domain_migration: DomainMigrationConfigUpdateRequest.nullish(),
|
||||
altcha_captcha: AltchaCaptchaConfigUpdateRequest.nullish(),
|
||||
captcha: CaptchaConfigUpdateRequest.nullish(),
|
||||
experiment_delivery: ExperimentDeliveryConfigUpdateRequest.nullish(),
|
||||
registration: z
|
||||
.object({
|
||||
@@ -721,15 +706,6 @@ export const InstanceConfigUpdateRequest = z.object({
|
||||
api_key: z.string().trim().max(4096).nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
captcha: z
|
||||
.object({
|
||||
provider: InstanceCaptchaProviderSchema.nullish(),
|
||||
hcaptcha_site_key: z.string().trim().max(4096).nullish(),
|
||||
hcaptcha_secret_key: z.string().trim().max(4096).nullish(),
|
||||
turnstile_site_key: z.string().trim().max(4096).nullish(),
|
||||
turnstile_secret_key: z.string().trim().max(4096).nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
email: z
|
||||
.object({
|
||||
enabled: z.boolean().nullish(),
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
type AltchaCaptchaConfig,
|
||||
AltchaCaptchaConfigSchema,
|
||||
AltchaCaptchaConfigUpdateRequest,
|
||||
altchaCaptchaAppliesTo,
|
||||
DEFAULT_ALTCHA_CAPTCHA_CONFIG,
|
||||
resolveAltchaCaptchaAssignment,
|
||||
} from '@fluxer/schema/src/domains/admin/AltchaCaptchaSchemas';
|
||||
import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false};
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
|
||||
function createConfig(overrides: Partial<AltchaCaptchaConfig> = {}): AltchaCaptchaConfig {
|
||||
return {...DEFAULT_ALTCHA_CAPTCHA_CONFIG, included_user_ids: [], excluded_user_ids: [], ...overrides};
|
||||
}
|
||||
|
||||
function syntheticUserIds(count: number): Array<string> {
|
||||
return Array.from({length: count}, (_, index) => (1400000000000000000n + BigInt(index)).toString());
|
||||
}
|
||||
|
||||
describe('altcha captcha configuration', () => {
|
||||
test('defaults to disabled with no anonymous traffic', () => {
|
||||
expect(AltchaCaptchaConfigSchema.parse({})).toEqual({
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: 'altcha-captcha-v1',
|
||||
included_user_ids: [],
|
||||
excluded_user_ids: [],
|
||||
included_guild_ids: [],
|
||||
include_premium_users: false,
|
||||
anonymous_enabled: false,
|
||||
cost: 5000,
|
||||
max_counter: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects difficulty outside the supported range', () => {
|
||||
expect(AltchaCaptchaConfigUpdateRequest.safeParse({cost: 999}).success).toBe(false);
|
||||
expect(AltchaCaptchaConfigUpdateRequest.safeParse({cost: 100001}).success).toBe(false);
|
||||
expect(AltchaCaptchaConfigUpdateRequest.safeParse({max_counter: 99}).success).toBe(false);
|
||||
expect(AltchaCaptchaConfigUpdateRequest.safeParse({max_counter: 1000001}).success).toBe(false);
|
||||
expect(AltchaCaptchaConfigUpdateRequest.safeParse({config_version: 3}).data).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAltchaCaptchaAssignment', () => {
|
||||
test('serves nobody while disabled, even included users', () => {
|
||||
const config = createConfig({rollout_basis_points: 10000, included_user_ids: [TARGETED_USER_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('applies exclusions before inclusions', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('serves included users at zero rollout', () => {
|
||||
const config = createConfig({enabled: true, included_user_ids: [TARGETED_USER_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: true});
|
||||
});
|
||||
|
||||
test('buckets the rollout by salt and user id', () => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: 2500});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
expect(resolveAltchaCaptchaAssignment(config, userId, NO_TARGETING).enabled).toBe(
|
||||
experimentBucket(userId, config.rollout_salt) < 2500,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('altchaCaptchaAppliesTo', () => {
|
||||
test('serves anonymous requests only when anonymous_enabled is set', () => {
|
||||
expect(altchaCaptchaAppliesTo(createConfig({enabled: true}), null, NO_TARGETING)).toBe(false);
|
||||
expect(altchaCaptchaAppliesTo(createConfig({enabled: true, anonymous_enabled: true}), null, NO_TARGETING)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(altchaCaptchaAppliesTo(createConfig({anonymous_enabled: true}), null, NO_TARGETING)).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps signed-in users on their own bucket regardless of the anonymous switch', () => {
|
||||
const config = createConfig({enabled: true, anonymous_enabled: true, excluded_user_ids: [TARGETED_USER_ID]});
|
||||
expect(altchaCaptchaAppliesTo(config, TARGETED_USER_ID, NO_TARGETING)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAltchaCaptchaAssignment guild targeting', () => {
|
||||
const INCLUDED_GUILD_ID = '3000000000000000001';
|
||||
const MEMBER_GUILDS: ExperimentTargeting = {
|
||||
memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]),
|
||||
premium: false,
|
||||
};
|
||||
|
||||
test('serves members of an included guild at zero rollout', () => {
|
||||
const config = createConfig({enabled: true, included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: true});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of guild membership', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_guild_ids: [INCLUDED_GUILD_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('serves no guild members while disabled', () => {
|
||||
const config = createConfig({included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAltchaCaptchaAssignment premium targeting', () => {
|
||||
const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true};
|
||||
|
||||
test('serves premium users only when the switch is on', () => {
|
||||
const on = createConfig({enabled: true, include_premium_users: true});
|
||||
expect(resolveAltchaCaptchaAssignment(on, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: true});
|
||||
expect(resolveAltchaCaptchaAssignment(on, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
expect(resolveAltchaCaptchaAssignment(createConfig({enabled: true}), TARGETED_USER_ID, PREMIUM)).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of the premium switch', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
include_premium_users: true,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
EXPERIMENT_BUCKET_RESOLUTION,
|
||||
type ExperimentTargeting,
|
||||
experimentAudienceIncludes,
|
||||
experimentBucket,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
const ALTCHA_CAPTCHA_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
const ALTCHA_CAPTCHA_MAX_TARGETED_USERS = 1000;
|
||||
const DEFAULT_ALTCHA_CAPTCHA_SALT = 'altcha-captcha-v1';
|
||||
|
||||
export const ALTCHA_CAPTCHA_MIN_COST = 1000;
|
||||
export const ALTCHA_CAPTCHA_MAX_COST = 100000;
|
||||
export const ALTCHA_CAPTCHA_MIN_MAX_COUNTER = 100;
|
||||
export const ALTCHA_CAPTCHA_MAX_MAX_COUNTER = 1000000;
|
||||
|
||||
const ALTCHA_CAPTCHA_SALT_PATTERN = /^[\x20-\x7e]+$/u;
|
||||
|
||||
const AltchaCaptchaTargetIdSchema = z.string().regex(/^\d{1,20}$/u);
|
||||
const AltchaCaptchaTargetedUserIdsSchema = z.array(AltchaCaptchaTargetIdSchema).max(ALTCHA_CAPTCHA_MAX_TARGETED_USERS);
|
||||
|
||||
const altchaCaptchaConfigFields = {
|
||||
enabled: z.boolean(),
|
||||
config_version: z.number().int().min(0),
|
||||
rollout_basis_points: z.number().int().min(0).max(ALTCHA_CAPTCHA_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64).regex(ALTCHA_CAPTCHA_SALT_PATTERN),
|
||||
included_user_ids: AltchaCaptchaTargetedUserIdsSchema,
|
||||
included_guild_ids: AltchaCaptchaTargetedUserIdsSchema,
|
||||
include_premium_users: z.boolean(),
|
||||
excluded_user_ids: AltchaCaptchaTargetedUserIdsSchema,
|
||||
anonymous_enabled: z.boolean(),
|
||||
cost: z.number().int().min(ALTCHA_CAPTCHA_MIN_COST).max(ALTCHA_CAPTCHA_MAX_COST),
|
||||
max_counter: z.number().int().min(ALTCHA_CAPTCHA_MIN_MAX_COUNTER).max(ALTCHA_CAPTCHA_MAX_MAX_COUNTER),
|
||||
};
|
||||
|
||||
export const AltchaCaptchaConfigSchema = z.object({
|
||||
enabled: altchaCaptchaConfigFields.enabled.default(false),
|
||||
config_version: altchaCaptchaConfigFields.config_version.default(0),
|
||||
rollout_basis_points: altchaCaptchaConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: altchaCaptchaConfigFields.rollout_salt.default(DEFAULT_ALTCHA_CAPTCHA_SALT),
|
||||
included_user_ids: altchaCaptchaConfigFields.included_user_ids.default([]),
|
||||
included_guild_ids: altchaCaptchaConfigFields.included_guild_ids.default([]),
|
||||
include_premium_users: altchaCaptchaConfigFields.include_premium_users.default(false),
|
||||
excluded_user_ids: altchaCaptchaConfigFields.excluded_user_ids.default([]),
|
||||
anonymous_enabled: altchaCaptchaConfigFields.anonymous_enabled.default(false),
|
||||
cost: altchaCaptchaConfigFields.cost.default(5000),
|
||||
max_counter: altchaCaptchaConfigFields.max_counter.default(10000),
|
||||
});
|
||||
|
||||
export type AltchaCaptchaConfig = z.infer<typeof AltchaCaptchaConfigSchema>;
|
||||
|
||||
export const DEFAULT_ALTCHA_CAPTCHA_CONFIG: AltchaCaptchaConfig = AltchaCaptchaConfigSchema.parse({});
|
||||
|
||||
export const AltchaCaptchaConfigUpdateRequest = z
|
||||
.object(altchaCaptchaConfigFields)
|
||||
.omit({config_version: true})
|
||||
.partial();
|
||||
|
||||
export type AltchaCaptchaConfigUpdateRequest = z.infer<typeof AltchaCaptchaConfigUpdateRequest>;
|
||||
|
||||
export const AltchaCaptchaConfigResponse = AltchaCaptchaConfigSchema;
|
||||
|
||||
export type AltchaCaptchaConfigResponse = z.infer<typeof AltchaCaptchaConfigResponse>;
|
||||
|
||||
export const AltchaCaptchaAssignmentResponse = z.object({
|
||||
enabled: altchaCaptchaConfigFields.enabled,
|
||||
});
|
||||
|
||||
export type AltchaCaptchaAssignmentResponse = z.infer<typeof AltchaCaptchaAssignmentResponse>;
|
||||
|
||||
export const INERT_ALTCHA_CAPTCHA_ASSIGNMENT: AltchaCaptchaAssignmentResponse = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export function resolveAltchaCaptchaAssignment(
|
||||
config: AltchaCaptchaConfig,
|
||||
userId: string,
|
||||
targeting: ExperimentTargeting,
|
||||
): AltchaCaptchaAssignmentResponse {
|
||||
if (!config.enabled) return {...INERT_ALTCHA_CAPTCHA_ASSIGNMENT};
|
||||
if (config.excluded_user_ids.includes(userId)) return {...INERT_ALTCHA_CAPTCHA_ASSIGNMENT};
|
||||
if (config.included_user_ids.includes(userId)) return {enabled: true};
|
||||
if (experimentAudienceIncludes(config, targeting)) return {enabled: true};
|
||||
return {enabled: experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points};
|
||||
}
|
||||
|
||||
export function altchaCaptchaAppliesTo(
|
||||
config: AltchaCaptchaConfig,
|
||||
userId: string | null,
|
||||
targeting: ExperimentTargeting,
|
||||
): boolean {
|
||||
if (userId === null) return config.enabled && config.anonymous_enabled;
|
||||
return resolveAltchaCaptchaAssignment(config, userId, targeting).enabled;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
CaptchaConfigSchema,
|
||||
CaptchaConfigUpdateRequest,
|
||||
DEFAULT_CAPTCHA_CONFIG,
|
||||
} from '@fluxer/schema/src/domains/admin/CaptchaSchemas';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
describe('captcha configuration', () => {
|
||||
test('defaults to enabled at the phone-friendly difficulty', () => {
|
||||
expect(CaptchaConfigSchema.parse({})).toEqual({enabled: true, cost: 5000, max_counter: 1000});
|
||||
expect(DEFAULT_CAPTCHA_CONFIG).toEqual({enabled: true, cost: 5000, max_counter: 1000});
|
||||
});
|
||||
|
||||
test('rejects difficulty outside the supported range', () => {
|
||||
expect(CaptchaConfigUpdateRequest.safeParse({cost: 999}).success).toBe(false);
|
||||
expect(CaptchaConfigUpdateRequest.safeParse({cost: 20001}).success).toBe(false);
|
||||
expect(CaptchaConfigUpdateRequest.safeParse({max_counter: 99}).success).toBe(false);
|
||||
expect(CaptchaConfigUpdateRequest.safeParse({max_counter: 20001}).success).toBe(false);
|
||||
expect(CaptchaConfigSchema.safeParse({cost: 20001}).success).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps a partial update partial without filling defaults', () => {
|
||||
expect(CaptchaConfigUpdateRequest.parse({})).toEqual({});
|
||||
expect(CaptchaConfigUpdateRequest.parse({enabled: false})).toEqual({enabled: false});
|
||||
expect(CaptchaConfigUpdateRequest.parse({cost: 1000, max_counter: 100})).toEqual({cost: 1000, max_counter: 100});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {z} from 'zod';
|
||||
|
||||
export const CAPTCHA_MIN_COST = 1000;
|
||||
export const CAPTCHA_MAX_COST = 20000;
|
||||
export const CAPTCHA_MIN_MAX_COUNTER = 100;
|
||||
export const CAPTCHA_MAX_MAX_COUNTER = 20000;
|
||||
|
||||
const captchaConfigFields = {
|
||||
enabled: z.boolean(),
|
||||
cost: z.number().int().min(CAPTCHA_MIN_COST).max(CAPTCHA_MAX_COST),
|
||||
max_counter: z.number().int().min(CAPTCHA_MIN_MAX_COUNTER).max(CAPTCHA_MAX_MAX_COUNTER),
|
||||
};
|
||||
|
||||
export const CaptchaConfigSchema = z.object({
|
||||
enabled: captchaConfigFields.enabled.default(true),
|
||||
cost: captchaConfigFields.cost.default(5000),
|
||||
max_counter: captchaConfigFields.max_counter.default(1000),
|
||||
});
|
||||
|
||||
export type CaptchaConfig = z.infer<typeof CaptchaConfigSchema>;
|
||||
|
||||
export const DEFAULT_CAPTCHA_CONFIG: CaptchaConfig = CaptchaConfigSchema.parse({});
|
||||
|
||||
export const CaptchaConfigUpdateRequest = z.object(captchaConfigFields).partial();
|
||||
|
||||
export type CaptchaConfigUpdateRequest = z.infer<typeof CaptchaConfigUpdateRequest>;
|
||||
|
||||
export const CaptchaConfigResponse = CaptchaConfigSchema;
|
||||
|
||||
export type CaptchaConfigResponse = z.infer<typeof CaptchaConfigResponse>;
|
||||
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AltchaCaptchaAssignmentResponse} from '@fluxer/schema/src/domains/admin/AltchaCaptchaSchemas';
|
||||
import {
|
||||
DomainMigrationAssignmentResponse,
|
||||
INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
@@ -43,7 +42,6 @@ export type ExperimentDeliveryConfigResponse = z.infer<typeof ExperimentDelivery
|
||||
|
||||
const ExperimentAssignmentsSchema = z.object({
|
||||
domain_migration: DomainMigrationAssignmentResponse.optional(),
|
||||
altcha_captcha: AltchaCaptchaAssignmentResponse.optional(),
|
||||
});
|
||||
|
||||
export const ExperimentAssignmentsResponse = z.object({
|
||||
|
||||
@@ -74,8 +74,7 @@ export const InstanceAppPublicSchema = z.object({
|
||||
});
|
||||
export type InstanceAppPublic = z.infer<typeof InstanceAppPublicSchema>;
|
||||
|
||||
export const InstanceCaptchaProviderSchema = z.enum(['hcaptcha', 'turnstile', 'none']);
|
||||
export type InstanceCaptchaProvider = z.infer<typeof InstanceCaptchaProviderSchema>;
|
||||
export const InstanceCaptchaProviderSchema = z.enum(['altcha', 'none']);
|
||||
|
||||
export const InstanceEndpointsSchema = z
|
||||
.object({
|
||||
@@ -96,9 +95,7 @@ export type InstanceEndpoints = z.infer<typeof InstanceEndpointsSchema>;
|
||||
|
||||
export const InstanceCaptchaSchema = z
|
||||
.object({
|
||||
provider: InstanceCaptchaProviderSchema.describe('Captcha provider name (hcaptcha, turnstile, none)'),
|
||||
hcaptcha_site_key: z.string().nullable().describe('hCaptcha site key if using hCaptcha'),
|
||||
turnstile_site_key: z.string().nullable().describe('Cloudflare Turnstile site key if using Turnstile'),
|
||||
provider: InstanceCaptchaProviderSchema.describe('Captcha provider (altcha or none)'),
|
||||
})
|
||||
.describe('Captcha configuration');
|
||||
export type InstanceCaptcha = z.infer<typeof InstanceCaptchaSchema>;
|
||||
|
||||
Reference in new issue
Block a user