Initial commit — federated self-custodial Spark/Lightning tip bot

- 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>
This commit is contained in:
goyban
2026-05-03 13:21:43 +00:00
commit e857bf4ec6
40 changed files with 4689 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
/**
* 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";
}
}