This commit is contained in:
badblocks 2026-07-10 00:21:45 -07:00
parent 6e7977997e
commit e97ed713c5
Signed by: badblocks
SSH key fingerprint: SHA256:hEcM6BP4hKm9F7WsomNuXSBpPn2BSDnFzPSl3y1NerQ
21 changed files with 286 additions and 197 deletions

View file

@ -1,7 +1,7 @@
import Cap, { type ChallengeData } from "@cap.js/server";
import type { AstroSession } from "astro";
export function createCap(session: AstroSession<any> | null) {
export function createCap(session: AstroSession | null) {
if (!session) {
throw new Error("Session context is required");
}

View file

@ -1,16 +1,10 @@
import { ofetch } from "ofetch";
import { ProxyAgent } from "undici";
const wireguardDispatcher = new ProxyAgent("http://wireguard:8888");
const TIMEOUT = 5000;
const httpFetchClient = {
get: async (url: string, headers: Record<string, string>) => {
const response = await ofetch(url, {
method: "GET",
headers,
dispatcher: wireguardDispatcher,
timeout: TIMEOUT,
});
return response;
@ -20,8 +14,6 @@ const httpFetchClient = {
method: "POST",
headers,
body: JSON.stringify(body),
dispatcher: wireguardDispatcher,
timeout: TIMEOUT,
});
return response;
@ -31,8 +23,6 @@ const httpFetchClient = {
method: "PUT",
headers,
body: JSON.stringify(body),
dispatcher: wireguardDispatcher,
timeout: TIMEOUT,
});
return response;
@ -42,8 +32,6 @@ const httpFetchClient = {
method: "PATCH",
headers,
body: JSON.stringify(body),
dispatcher: wireguardDispatcher,
timeout: TIMEOUT,
});
return response;
@ -52,8 +40,6 @@ const httpFetchClient = {
const response = await ofetch(url, {
method: "DELETE",
headers,
dispatcher: wireguardDispatcher,
timeout: TIMEOUT,
});
return response;

View file

@ -1,16 +1,44 @@
import { authenticator } from "otplib";
import { createHash } from "crypto";
const submissionTimestamps = new Map();
const otpRequestTimestamps = new Map();
const submissionTimestamps = new Map<string, number[]>();
const otpRequestTimestamps = new Map<string, number[]>();
const ONE_WEEK_IN_MS: number = 7 * 24 * 60 * 60 * 1000;
const ONE_HOUR_IN_MS: number = 60 * 60 * 1000;
const MAX_OTP_REQUESTS_PER_HOUR: number = 3;
const MAX_MESSAGES_PER_WEEK: number = 3;
const OTP_STEP_IN_SEC: number = 300;
const VALID_PAST_OTP_STEPS: number = 1;
const VALID_FUTURE_OTP_STEPS: number = 1;
const VALID_FUTURE_OTP_STEPS: number = 0;
const OTP_NUM_DIGITS: number = 6;
const MAX_GLOBAL_OTP_SENDS_PER_HOUR: number = 10;
let globalOtpSendTimestamps: number[] = [];
export function isRateLimitedGlobally(): boolean {
const now = Date.now();
globalOtpSendTimestamps = globalOtpSendTimestamps.filter(
(t) => now - t < ONE_HOUR_IN_MS,
);
return globalOtpSendTimestamps.length >= MAX_GLOBAL_OTP_SENDS_PER_HOUR;
}
export function recordGlobalOtpSend() {
globalOtpSendTimestamps.push(Date.now());
}
function cleanupStaleEntries(map: Map<string, number[]>, windowMs: number) {
const now = Date.now();
for (const [key, timestamps] of map) {
const recent = timestamps.filter((t) => now - t < windowMs);
if (recent.length === 0) map.delete(key);
else if (recent.length !== timestamps.length) map.set(key, recent);
}
}
setInterval(() => {
cleanupStaleEntries(submissionTimestamps, ONE_WEEK_IN_MS);
cleanupStaleEntries(otpRequestTimestamps, ONE_HOUR_IN_MS);
}, ONE_HOUR_IN_MS).unref?.();
authenticator.options = {
step: OTP_STEP_IN_SEC,
@ -30,15 +58,14 @@ function getUserSecret(phoneNumber: string, salt: string): string {
}
export function normalizePhone(phone: string) {
const result = phone.replace(/[^\d]/g, "").trim().startsWith("1")
? phone.substring(1)
: phone;
if (result.length !== 10) {
let digits = phone.replace(/\D/g, "");
if (digits.length === 11 && digits.startsWith("1")) {
digits = digits.slice(1);
}
if (digits.length !== 10) {
throw new Error("Invalid phone number.");
}
return result;
return digits;
}
export function isValidPhone(phone: string): boolean {
@ -46,9 +73,13 @@ export function isValidPhone(phone: string): boolean {
const match = phone.match(/(\d{3})(\d{3})(\d{4})/);
const [, prefix, exchange, station] = match ?? [];
const isValidNANPFormat =
/^[2-7][0-8][0-9]$/.test(prefix) && /^[2-9][0-9]{2}$/.test(exchange);
/^[2-9][0-9]{2}$/.test(prefix) && /^[2-9][0-9]{2}$/.test(exchange);
const isNotAllSameDigit = !/^(.)\1{6}$/.test(exchange + station);
const isNot911Number = prefix !== "911" && exchange !== "911";
const isNotTollFreeNumber = !(
/^[8-9][0-9]{2}$/.test(prefix) &&
/^(99|88|77|66|55|44|33|22|11|00)$/.test(prefix.slice(1, 3))
);
const isNot555Number = prefix !== "555" && exchange !== "555";
const isNotPopSongNumber = exchange !== "867" && station !== "5309";
@ -57,6 +88,7 @@ export function isValidPhone(phone: string): boolean {
isNotAllSameDigit &&
isNot911Number &&
isNot555Number &&
isNotTollFreeNumber &&
isNotPopSongNumber
);
}
@ -153,4 +185,6 @@ export default {
recordMsgSubmission,
isRateLimitedForOtp,
isRateLimitedForMsgs,
isRateLimitedGlobally,
recordGlobalOtpSend,
};