Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions packages/backend/src/routes/webhooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Express, Request, Response } from 'express'
import { timingSafeEqual } from 'node:crypto'
import { timingSafeKeyCompare } from '../utils/timingSafeKeyCompare'
import { writeLimiter } from '../middleware/rateLimit'
import { asyncHandler } from '../middleware/asyncHandler'
import { AppError } from '../errors/AppError'
Expand All @@ -18,13 +18,6 @@ const VOTE_TTL_MILLISECONDS = 60 * 60 * 12 * 1000
// doesn't lose their streak due to timezone or minor scheduling drift.
const STREAK_TTL_MILLISECONDS = 60 * 60 * 36 * 1000

function safeEqualString(a: string, b: string): boolean {
const ab = Buffer.from(a)
const bb = Buffer.from(b)
if (ab.length !== bb.length) return false
return timingSafeEqual(ab, bb)
}

type TopggVotePayload = {
bot?: string
user?: string
Expand All @@ -43,15 +36,15 @@ function verifyTopggAuth(req: Request): void {
throw new AppError(503, 'TOPGG_AUTH_TOKEN not configured')
}
const provided = req.header('authorization')?.trim()
if (!provided || !safeEqualString(provided, expected)) {
if (!provided || !timingSafeKeyCompare(provided, expected)) {
throw AppError.unauthorized('invalid top.gg webhook token')
}
}

function verifyInternalKey(req: Request): void {
const expected = process.env.LUCKY_NOTIFY_API_KEY
const provided = req.header('x-notify-key')?.trim()
if (!expected || !provided || !safeEqualString(provided, expected)) {
if (!expected || !provided || !timingSafeKeyCompare(provided, expected)) {
throw AppError.unauthorized('invalid internal key')
}
}
Expand Down Expand Up @@ -80,7 +73,8 @@ async function readVoteState(
)

// Streak expires after 36h with no vote, matching the original Redis EXPIRE behavior
const expiredStreak = timeSinceVote > STREAK_TTL_MILLISECONDS ? 0 : vote.streak
const expiredStreak =
timeSinceVote > STREAK_TTL_MILLISECONDS ? 0 : vote.streak

return {
hasVoted: timeSinceVote < VOTE_TTL_MILLISECONDS,
Expand Down Expand Up @@ -113,9 +107,10 @@ async function recordVote(userId: string): Promise<'recorded' | 'duplicate'> {
if (existing) {
const timeSinceVote = nowMs - existing.lastVoteAt.getTime()
// Preserve streak if within 36h, otherwise reset to 1
newStreak = timeSinceVote <= STREAK_TTL_MILLISECONDS
? existing.streak + 1
: 1
newStreak =
timeSinceVote <= STREAK_TTL_MILLISECONDS
? existing.streak + 1
: 1
}

// Upsert: create or update vote record
Expand Down
Loading