randomayzer/src/app/api/giveaways/[id]/verify/route.ts

86 lines
3.2 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { GiveawayStore } from '@/lib/giveaway-store';
import { verifyDrawResult } from '@/core/randomizer/deterministic';
import { handleApiError, NotFoundError, ConflictError } from '@/core/errors/http-errors';
import { expensiveApiRateLimiter } from '@/lib/rate-limiter';
import { resolveClientIp } from '@/lib/client-ip';
export const dynamic = 'force-dynamic';
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> | { id: string } }
) {
try {
const { id } = await params;
const clientIp = resolveClientIp(req);
expensiveApiRateLimiter.assertAllowed(`verify-get:${clientIp}:${id}`);
const giveaway = await GiveawayStore.getById(id);
if (!giveaway) {
throw new NotFoundError(`Giveaway with id "${id}" not found`);
}
const drawResult = giveaway.drawResult;
if (!drawResult) {
throw new ConflictError('Giveaway has not been drawn yet. Nothing to verify.');
}
// Strict snapshot lookup: DO NOT fallback to latestSnapshot
const snapshot = giveaway.snapshots.find(s => s.id === drawResult.snapshotId);
if (!snapshot) {
return NextResponse.json({
success: false,
error: {
code: 'INTEGRITY_ERROR',
message: `Participant snapshot "${drawResult.snapshotId}" referenced by draw does not exist in storage`,
},
verified: false,
snapshotFound: false,
}, { status: 404 });
}
const claimedWinnersCount = drawResult.winners.length;
const claimedReserveCount = drawResult.reserveWinners.length;
// Run independent cryptographic replay verification
const verification = verifyDrawResult({
giveawayId: id,
drawId: drawResult.drawId,
drawnAt: drawResult.drawnAt,
snapshot,
seed: drawResult.seedUsed,
claimedWinnersCount,
claimedReserveCount,
claimedWinnerIds: drawResult.winnerIds,
claimedReserveWinnerIds: drawResult.reserveWinnerIds,
claimedDeterministicProofHash: drawResult.deterministicProofHash,
claimedAuditEventHash: drawResult.auditEventHash,
algorithmVersion: drawResult.algorithmVersion,
});
return NextResponse.json({
success: true,
verified: verification.verified,
giveawayId: id,
drawId: drawResult.drawId,
snapshotId: snapshot.id,
algorithmVersion: verification.algorithmVersion,
algorithmSupported: verification.algorithmSupported,
participantsSnapshotIntegrity: verification.participantsSnapshotIntegrity,
conditionsIntegrity: verification.conditionsIntegrity,
winnersMatch: verification.winnersMatch,
reserveWinnersMatch: verification.reserveWinnersMatch,
deterministicProofHashMatch: verification.deterministicProofHashMatch,
auditEventHashMatch: verification.auditEventHashMatch,
expectedWinnerIds: verification.expectedWinnerIds,
expectedReserveWinnerIds: verification.expectedReserveWinnerIds,
deterministicProofHash: verification.expectedDeterministicProofHash,
auditEventHash: verification.expectedAuditEventHash,
drawnAt: drawResult.drawnAt,
});
} catch (error: any) {
return handleApiError(error);
}
}