86 lines
3.2 KiB
TypeScript
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);
|
|
}
|
|
}
|