Repository navigation
Expand file tree
/
Copy pathverification.js
More file actions
112 lines (97 loc) · 3.76 KB
/
Copy pathverification.js
File metadata and controls
112 lines (97 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const { createHash, randomBytes, randomInt, timingSafeEqual } = require('node:crypto');
function digestCode(code, salt) {
return createHash('sha256').update(salt).update(String(code)).digest();
}
function normalizeEmail(raw) {
const email = String(raw || '').trim().toLowerCase();
if (email.length < 3 || email.length > 254) return '';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return '';
return email;
}
class VerificationService {
constructor({ ttlMinutes = 10, maxAttempts = 5, maxSendsPerHour = 3 } = {}) {
this.ttlMinutes = ttlMinutes;
this.maxAttempts = maxAttempts;
this.maxSendsPerHour = maxSendsPerHour;
this.pending = new Map();
this.sends = new Map();
}
reserveAttempt(discordUserId, email) {
const now = Date.now();
const hourAgo = now - 60 * 60 * 1000;
const key = `${discordUserId}:${email}`;
const record = this.sends.get(key) || { timestamps: [] };
record.timestamps = record.timestamps.filter((t) => t > hourAgo);
if (record.timestamps.length >= this.maxSendsPerHour) {
this.sends.set(key, record);
return false;
}
record.timestamps.push(now);
this.sends.set(key, record);
return now;
}
releaseAttempt(discordUserId, email, reservedAt) {
const timestamps = this.sends.get(`${discordUserId}:${email}`)?.timestamps || [];
const index = timestamps.lastIndexOf(reservedAt);
if (index !== -1) timestamps.splice(index, 1);
}
create(discordUserId, email) {
const code = randomInt(0, 1_000_000).toString().padStart(6, '0');
const salt = randomBytes(16);
this.pending.set(String(discordUserId), {
email,
salt,
digest: digestCode(code, salt),
expiresAt: Date.now() + this.ttlMinutes * 60 * 1000,
attempts: 0,
});
return code;
}
verify(discordUserId, code) {
const item = this.pending.get(String(discordUserId));
if (!item) return { ok: false, reason: 'No verification is pending. Run /order again.' };
if (Date.now() > item.expiresAt) {
this.pending.delete(String(discordUserId));
return { ok: false, reason: 'That code expired. Run /order again.' };
}
item.attempts += 1;
if (item.attempts > this.maxAttempts) {
this.pending.delete(String(discordUserId));
return { ok: false, reason: 'Too many attempts. Run /order again.' };
}
const candidate = digestCode(String(code || '').trim(), item.salt);
if (candidate.length !== item.digest.length || !timingSafeEqual(candidate, item.digest)) {
return { ok: false, reason: 'Incorrect code.' };
}
this.pending.delete(String(discordUserId));
return { ok: true, email: item.email };
}
reset() {
this.pending.clear();
this.sends.clear();
}
}
class PersistentVerificationService {
constructor({ store, ttlMinutes = 10, maxAttempts = 5, maxSendsPerHour = 3 } = {}) {
this.storeProvider = store ? () => store : () => require('./supportIdentityStore').getStore();
this.ttlMinutes = ttlMinutes;
this.maxAttempts = maxAttempts;
this.maxSendsPerHour = maxSendsPerHour;
}
reserveAttempt(discordUserId, email) {
return this.storeProvider().reserveAttempt(discordUserId, email, this.maxSendsPerHour);
}
releaseAttempt(discordUserId, email, reservationId) {
return this.storeProvider().releaseAttempt(discordUserId, email, reservationId);
}
create(discordUserId, email) {
return this.storeProvider().createChallenge(discordUserId, email, { ttlMinutes: this.ttlMinutes, maxAttempts: this.maxAttempts });
}
verify(discordUserId, code) {
return this.storeProvider().verifyChallenge(discordUserId, code);
}
reset() {
if (!process.env.DATABASE_URL) this.storeProvider().resetMemory();
}
}
module.exports = { VerificationService, PersistentVerificationService, normalizeEmail, digestCode };