e857bf4ec6
- grammY bot: /start, /unlock, /tip, /contact, /claim, /settings, /wallet - AES-256-GCM mnemonic encryption with scrypt key derivation - In-memory unlock sessions with background sweep - Atomic claim handling (TOCTOU-safe) - PIN rate limiting (5 attempts → 15 min lockout) - Fastify API server + Telegram Mini App (setup, unlock, send, receive, history) - One-time seed reveal via Mini App or auto-deleted DM message - Federated registry client - Docker Compose deployment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50 lines
1.3 KiB
TypeScript
50 lines
1.3 KiB
TypeScript
/**
|
|
* Simple in-memory rate limiter for PIN attempts.
|
|
*
|
|
* After MAX_FAILURES consecutive wrong PINs the account is locked for
|
|
* LOCKOUT_MS. The counter resets on a successful unlock.
|
|
*/
|
|
|
|
const MAX_FAILURES = 5;
|
|
const LOCKOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
|
|
interface Entry {
|
|
failures: number;
|
|
lockedUntil: number | null;
|
|
}
|
|
|
|
const store = new Map<number, Entry>();
|
|
|
|
function entry(userId: number): Entry {
|
|
if (!store.has(userId)) store.set(userId, { failures: 0, lockedUntil: null });
|
|
return store.get(userId)!;
|
|
}
|
|
|
|
export function checkPinRateLimit(userId: number): void {
|
|
const e = entry(userId);
|
|
if (e.lockedUntil && Date.now() < e.lockedUntil) {
|
|
const secondsLeft = Math.ceil((e.lockedUntil - Date.now()) / 1000);
|
|
throw new RateLimitError(`Too many incorrect PINs. Try again in ${secondsLeft}s.`);
|
|
}
|
|
}
|
|
|
|
export function recordPinFailure(userId: number): void {
|
|
const e = entry(userId);
|
|
e.failures += 1;
|
|
if (e.failures >= MAX_FAILURES) {
|
|
e.lockedUntil = Date.now() + LOCKOUT_MS;
|
|
e.failures = 0;
|
|
}
|
|
}
|
|
|
|
export function recordPinSuccess(userId: number): void {
|
|
store.delete(userId); // reset counter on success
|
|
}
|
|
|
|
export class RateLimitError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "RateLimitError";
|
|
}
|
|
}
|