diff --git a/apps/blog/app/api/avatar/route.ts b/apps/blog/app/api/avatar/route.ts index de92ebd92fc95d710980f000ff2cf35cebd03137..838c1380ac29ab4bb62745b098223407ff9668c2 100644 --- a/apps/blog/app/api/avatar/route.ts +++ b/apps/blog/app/api/avatar/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { configuredImagesEndpoint } from '@hive/ui/config/public-vars'; +import { isHiveAccountNameValid } from '@hive/transaction'; /** * Proxy endpoint for user avatars that prevents caching @@ -18,6 +19,10 @@ export async function GET(req: NextRequest): Promise { return NextResponse.json({ error: 'Username is required' }, { status: 400 }); } + if (!(await isHiveAccountNameValid(username))) { + return NextResponse.json({ error: 'Invalid username' }, { status: 400 }); + } + // Build the image hoster URL let imageUrl: string; if (size && ['small', 'medium', 'large'].includes(size)) { diff --git a/apps/blog/utils/validate-links.ts b/apps/blog/utils/validate-links.ts index 68b701654db10a8455499a4fdceb3459fe750d19..60d7e6253ef43cc1b28ec01b3aba87d5b2fb83d2 100644 --- a/apps/blog/utils/validate-links.ts +++ b/apps/blog/utils/validate-links.ts @@ -1,74 +1,9 @@ -import { getChain } from '@hive/transaction/lib/chain'; -import { getLogger } from '@ui/lib/logging'; - -const logger = getLogger('app'); +import { isHiveAccountNameValid } from '@hive/transaction'; export function isPermlinkValid(permlink: string): boolean { if (typeof permlink !== 'string') return false; return /^[a-z0-9-]{1,255}$/.test(permlink); } -/** - * Validates Hive account names using wax library with regex fallback. - * - * TEMPORARY WORKAROUND: The wax WASM module isn't thread-safe - concurrent - * Server Component requests cause "memory access out of bounds" errors. - * We catch WASM errors and fall back to regex validation until wax is fixed. - * - * @see https://gitlab.syncad.com/hive/denser/-/issues/758 - * @see https://gitlab.syncad.com/hive/wax/-/issues/140 - * - * TODO: Remove regex fallback once wax thread-safety is resolved - */ -export async function isUsernameValid(accountName: string): Promise { - if (typeof accountName !== 'string') return false; - - try { - const chain = await getChain(); - return chain.isValidAccountName(accountName); - } catch (error) { - // TEMPORARY: Log WASM failure and fall back to regex validation - // See: https://gitlab.syncad.com/hive/wax/-/issues/140 - logger.warn({ err: error, accountName }, 'WASM isValidAccountName failed, using regex fallback'); - return isUsernameValidRegex(accountName); - } -} - -/** - * Regex fallback for Hive account name validation. - * Used when wax WASM fails due to concurrent access. - * - * Rules based on Hive blockchain consensus: - * - 3-16 characters - * - Lowercase letters, numbers, dots, hyphens only - * - Must start with a letter - * - Cannot end with hyphen or dot - * - Each segment (separated by dots) must start with a letter - * - No consecutive dots or hyphens - * - * TODO: Remove once wax thread-safety is resolved (see wax#140) - */ -function isUsernameValidRegex(accountName: string): boolean { - if (accountName.length < 3 || accountName.length > 16) return false; - - // Must contain only lowercase letters, numbers, dots, and hyphens - if (!/^[a-z0-9.-]+$/.test(accountName)) return false; - - // Must start with a letter - if (!/^[a-z]/.test(accountName)) return false; - - // Cannot end with hyphen or dot - if (/[.-]$/.test(accountName)) return false; - - // No consecutive dots or hyphens - if (/[.-]{2}/.test(accountName)) return false; - - // Each segment must start with a letter - const segments = accountName.split('.'); - for (const segment of segments) { - if (segment.length === 0) return false; - if (!/^[a-z]/.test(segment)) return false; - } - - return true; -} +// Re-export from @hive/transaction for backwards compatibility +export { isHiveAccountNameValid as isUsernameValid }; diff --git a/apps/wallet/pages/api/avatar.ts b/apps/wallet/pages/api/avatar.ts index fd3eeddafb921f0e3c36c25daa591083bf8c1cba..f017c938b3cadcbde703aff09c103fb92f13f128 100644 --- a/apps/wallet/pages/api/avatar.ts +++ b/apps/wallet/pages/api/avatar.ts @@ -1,5 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { configuredImagesEndpoint } from '@hive/ui/config/public-vars'; +import { isHiveAccountNameValid } from '@hive/transaction'; type ResponseData = { error?: string; @@ -25,6 +26,10 @@ export default async function handler( return res.status(400).json({ error: 'Username is required' }); } + if (!(await isHiveAccountNameValid(username))) { + return res.status(400).json({ error: 'Invalid username' }); + } + // Build the image hoster URL let imageUrl: string; if (size && typeof size === 'string' && ['small', 'medium', 'large'].includes(size)) { diff --git a/packages/transaction/index.ts b/packages/transaction/index.ts index 903b975330bb1be2d88fe757061ff80bca281e11..c058c5d1327e5384ebed3b2b9859e22354155e23 100644 --- a/packages/transaction/index.ts +++ b/packages/transaction/index.ts @@ -1158,3 +1158,5 @@ export class TransactionService { } } export const transactionService = new TransactionService(); + +export { isHiveAccountNameValid, isHiveAccountNameValidFallback } from './lib/validate-hive-account'; diff --git a/packages/transaction/lib/validate-hive-account.ts b/packages/transaction/lib/validate-hive-account.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3a485eb9f4c6ac8f87f6950a9d0abb15b293315 --- /dev/null +++ b/packages/transaction/lib/validate-hive-account.ts @@ -0,0 +1,65 @@ +import { getChain } from './chain'; +import { getLogger } from '@hive/ui/lib/logging'; + +const logger = getLogger('validate-hive-account'); + +/** + * Regex fallback for Hive account name validation. + * Used when wax WASM fails due to concurrent access. + * + * NOTE: This regex is NOT an accurate implementation of Hive account name + * validation rules. It's intentionally permissive as a fallback when wax + * is unavailable. Some technically invalid names may pass this check, but + * they will be safely rejected at a later stage (e.g., by the Hive API or + * image host returning 404). This is just cheap/fast input sanitization. + * + * @see https://gitlab.syncad.com/hive/wax/-/issues/140 + */ +export function isHiveAccountNameValidFallback(accountName: string): boolean { + if (accountName.length < 3 || accountName.length > 16) return false; + + // Must contain only lowercase letters, numbers, dots, and hyphens + if (!/^[a-z0-9.-]+$/.test(accountName)) return false; + + // Must start with a letter + if (!/^[a-z]/.test(accountName)) return false; + + // Cannot end with hyphen or dot + if (/[.-]$/.test(accountName)) return false; + + // No consecutive dots (consecutive hyphens ARE valid, e.g., 'a--a') + if (/\.\./.test(accountName)) return false; + + // Each segment (separated by dots) must start with a letter + const segments = accountName.split('.'); + for (const segment of segments) { + if (segment.length === 0) return false; + if (!/^[a-z]/.test(segment)) return false; + } + + return true; +} + +/** + * Validates Hive account names using wax library with regex fallback. + * + * TEMPORARY WORKAROUND: The wax WASM module isn't thread-safe - concurrent + * Server Component requests cause "memory access out of bounds" errors. + * We catch WASM errors and fall back to regex validation until wax is fixed. + * + * @see https://gitlab.syncad.com/hive/denser/-/issues/758 + * @see https://gitlab.syncad.com/hive/wax/-/issues/140 + * + * TODO: Remove regex fallback once wax thread-safety is resolved + */ +export async function isHiveAccountNameValid(accountName: string): Promise { + if (typeof accountName !== 'string') return false; + + try { + const chain = await getChain(); + return chain.isValidAccountName(accountName); + } catch (error) { + logger.warn({ err: error, accountName }, 'WASM isValidAccountName failed, using regex fallback'); + return isHiveAccountNameValidFallback(accountName); + } +}