Fix account_too_new false positive for old Nostr accounts
- Force refresh profile when denying account_too_new, then re-check - Use querySync for age probe instead of pool.get (all relays, more reliable) - Add default relays: nostr.info, snort.social, mostr.pub - Skip cache when cached first-seen is borderline (10-14 days) Made-with: Cursor
This commit is contained in:
@@ -63,8 +63,8 @@ export const config = {
|
|||||||
ipCooldownDays: envInt("IP_COOLDOWN_DAYS", 7),
|
ipCooldownDays: envInt("IP_COOLDOWN_DAYS", 7),
|
||||||
maxClaimsPerIpPerPeriod: envInt("MAX_CLAIMS_PER_IP_PER_PERIOD", 1),
|
maxClaimsPerIpPerPeriod: envInt("MAX_CLAIMS_PER_IP_PER_PERIOD", 1),
|
||||||
|
|
||||||
// Nostr (defaults include relays common for remote signers / NIP-05)
|
// Nostr (defaults include relays common for remote signers, snort, iris, and NIP-05)
|
||||||
nostrRelays: (process.env.NOSTR_RELAYS ?? "wss://relay.damus.io,wss://relay.nostr.band,wss://relay.getalby.com,wss://nos.lol").split(",").map((s) => s.trim()),
|
nostrRelays: (process.env.NOSTR_RELAYS ?? "wss://relay.damus.io,wss://relay.nostr.band,wss://relay.getalby.com,wss://nos.lol,wss://relay.nostr.info,wss://relay.snort.social,wss://relay.mostr.pub").split(",").map((s) => s.trim()),
|
||||||
relayTimeoutMs: envInt("RELAY_TIMEOUT_MS", 5000),
|
relayTimeoutMs: envInt("RELAY_TIMEOUT_MS", 5000),
|
||||||
maxEventsFetch: envInt("MAX_EVENTS_FETCH", 500),
|
maxEventsFetch: envInt("MAX_EVENTS_FETCH", 500),
|
||||||
metadataCacheHours: envInt("METADATA_CACHE_HOURS", 24),
|
metadataCacheHours: envInt("METADATA_CACHE_HOURS", 24),
|
||||||
|
|||||||
@@ -93,10 +93,14 @@ export async function checkEligibility(pubkey: string, ipHash: string): Promise<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await fetchAndScorePubkey(pubkey);
|
let profile = await fetchAndScorePubkey(pubkey);
|
||||||
const minAgeSec = config.minAccountAgeDays * SECONDS_PER_DAY;
|
const minAgeSec = config.minAccountAgeDays * SECONDS_PER_DAY;
|
||||||
const cutoff = now - minAgeSec;
|
const cutoff = now - minAgeSec;
|
||||||
console.log(`[eligibility] pubkey=${pubkey.slice(0, 8)}… firstSeen=${profile.nostrFirstSeenAt} cutoff=${cutoff} score=${profile.activityScore}`);
|
console.log(`[eligibility] pubkey=${pubkey.slice(0, 8)}… firstSeen=${profile.nostrFirstSeenAt} cutoff=${cutoff} score=${profile.activityScore}`);
|
||||||
|
if (profile.nostrFirstSeenAt === null || profile.nostrFirstSeenAt > cutoff) {
|
||||||
|
profile = await fetchAndScorePubkey(pubkey, true);
|
||||||
|
console.log(`[eligibility] force-refresh after account_too_new: firstSeen=${profile.nostrFirstSeenAt}`);
|
||||||
|
}
|
||||||
if (profile.nostrFirstSeenAt === null || profile.nostrFirstSeenAt > cutoff) {
|
if (profile.nostrFirstSeenAt === null || profile.nostrFirstSeenAt > cutoff) {
|
||||||
const ageDays = profile.nostrFirstSeenAt ? Math.floor((now - profile.nostrFirstSeenAt) / SECONDS_PER_DAY) : "null";
|
const ageDays = profile.nostrFirstSeenAt ? Math.floor((now - profile.nostrFirstSeenAt) / SECONDS_PER_DAY) : "null";
|
||||||
console.log(`[eligibility] DENIED account_too_new: firstSeen age=${ageDays} days, required=${config.minAccountAgeDays}`);
|
console.log(`[eligibility] DENIED account_too_new: firstSeen age=${ageDays} days, required=${config.minAccountAgeDays}`);
|
||||||
|
|||||||
@@ -20,21 +20,23 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pool.get resolves as soon as ONE relay responds (or null if none do).
|
* Query all relays for old events (created_at <= untilTimestamp). Uses querySync to wait
|
||||||
* Much faster than querySync which waits for EOSE from ALL relays.
|
* for all relays and merge results, improving reliability vs pool.get (first relay only).
|
||||||
*/
|
*/
|
||||||
async function probeOldEvent(pubkey: string, untilTimestamp: number): Promise<number | null> {
|
async function probeOldEvent(pubkey: string, untilTimestamp: number): Promise<number | null> {
|
||||||
try {
|
try {
|
||||||
const ev = await withTimeout(
|
const evs = await withTimeout(
|
||||||
pool.get(config.nostrRelays, {
|
pool.querySync(config.nostrRelays, {
|
||||||
kinds: [0, 1, 3],
|
kinds: [0, 1, 3],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
until: untilTimestamp,
|
until: untilTimestamp,
|
||||||
limit: 1,
|
limit: 10,
|
||||||
}),
|
}),
|
||||||
config.relayTimeoutMs
|
config.relayTimeoutMs
|
||||||
);
|
);
|
||||||
return ev ? ev.created_at : null;
|
const arr = Array.isArray(evs) ? evs : [];
|
||||||
|
if (arr.length === 0) return null;
|
||||||
|
return Math.min(...arr.map((e) => e.created_at));
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -50,8 +52,14 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
|||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
const cacheHours = config.metadataCacheHours;
|
const cacheHours = config.metadataCacheHours;
|
||||||
const cacheValidUntil = (cached?.last_metadata_fetch_at ?? 0) + cacheHours * 3600;
|
const cacheValidUntil = (cached?.last_metadata_fetch_at ?? 0) + cacheHours * 3600;
|
||||||
|
const ageCutoffSec = nowSec - config.minAccountAgeDays * 86400;
|
||||||
|
const borderlineWindowSec = 4 * 86400;
|
||||||
|
const isBorderline =
|
||||||
|
cached?.nostr_first_seen_at != null &&
|
||||||
|
cached.nostr_first_seen_at >= ageCutoffSec &&
|
||||||
|
cached.nostr_first_seen_at <= ageCutoffSec + borderlineWindowSec;
|
||||||
|
|
||||||
if (!forceRefreshProfile && cached && cacheValidUntil > nowSec) {
|
if (!forceRefreshProfile && cached && cacheValidUntil > nowSec && !isBorderline) {
|
||||||
return {
|
return {
|
||||||
nostrFirstSeenAt: cached.nostr_first_seen_at,
|
nostrFirstSeenAt: cached.nostr_first_seen_at,
|
||||||
notesCount: cached.notes_count,
|
notesCount: cached.notes_count,
|
||||||
@@ -62,16 +70,14 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the main activity query and the account-age probe in parallel.
|
// Run the main activity query and the account-age probe in parallel.
|
||||||
// The age probe uses pool.get (resolves on first relay hit) so it's fast.
|
// The age probe uses querySync to wait for all relays and merge results for reliability.
|
||||||
const ageCutoff = nowSec - config.minAccountAgeDays * 86400;
|
|
||||||
|
|
||||||
const mainQueryPromise = withTimeout(
|
const mainQueryPromise = withTimeout(
|
||||||
pool.querySync(config.nostrRelays, { kinds: [0, 1, 3], authors: [pubkey], limit: config.maxEventsFetch }),
|
pool.querySync(config.nostrRelays, { kinds: [0, 1, 3], authors: [pubkey], limit: config.maxEventsFetch }),
|
||||||
config.relayTimeoutMs
|
config.relayTimeoutMs
|
||||||
).then((r) => (Array.isArray(r) ? r : []))
|
).then((r) => (Array.isArray(r) ? r : []))
|
||||||
.catch((): { kind: number; created_at: number; content?: string; tags: string[][] }[] => []);
|
.catch((): { kind: number; created_at: number; content?: string; tags: string[][] }[] => []);
|
||||||
|
|
||||||
const ageProbePromise = probeOldEvent(pubkey, ageCutoff);
|
const ageProbePromise = probeOldEvent(pubkey, ageCutoffSec);
|
||||||
|
|
||||||
const [events, oldEventTimestamp] = await Promise.all([mainQueryPromise, ageProbePromise]);
|
const [events, oldEventTimestamp] = await Promise.all([mainQueryPromise, ageProbePromise]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user