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),
|
||||
maxClaimsPerIpPerPeriod: envInt("MAX_CLAIMS_PER_IP_PER_PERIOD", 1),
|
||||
|
||||
// Nostr (defaults include relays common for remote signers / 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()),
|
||||
// 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,wss://relay.nostr.info,wss://relay.snort.social,wss://relay.mostr.pub").split(",").map((s) => s.trim()),
|
||||
relayTimeoutMs: envInt("RELAY_TIMEOUT_MS", 5000),
|
||||
maxEventsFetch: envInt("MAX_EVENTS_FETCH", 500),
|
||||
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 cutoff = now - minAgeSec;
|
||||
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) {
|
||||
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}`);
|
||||
|
||||
@@ -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).
|
||||
* Much faster than querySync which waits for EOSE from ALL relays.
|
||||
* Query all relays for old events (created_at <= untilTimestamp). Uses querySync to wait
|
||||
* for all relays and merge results, improving reliability vs pool.get (first relay only).
|
||||
*/
|
||||
async function probeOldEvent(pubkey: string, untilTimestamp: number): Promise<number | null> {
|
||||
try {
|
||||
const ev = await withTimeout(
|
||||
pool.get(config.nostrRelays, {
|
||||
const evs = await withTimeout(
|
||||
pool.querySync(config.nostrRelays, {
|
||||
kinds: [0, 1, 3],
|
||||
authors: [pubkey],
|
||||
until: untilTimestamp,
|
||||
limit: 1,
|
||||
limit: 10,
|
||||
}),
|
||||
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 {
|
||||
return null;
|
||||
}
|
||||
@@ -50,8 +52,14 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const cacheHours = config.metadataCacheHours;
|
||||
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 {
|
||||
nostrFirstSeenAt: cached.nostr_first_seen_at,
|
||||
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.
|
||||
// The age probe uses pool.get (resolves on first relay hit) so it's fast.
|
||||
const ageCutoff = nowSec - config.minAccountAgeDays * 86400;
|
||||
|
||||
// The age probe uses querySync to wait for all relays and merge results for reliability.
|
||||
const mainQueryPromise = withTimeout(
|
||||
pool.querySync(config.nostrRelays, { kinds: [0, 1, 3], authors: [pubkey], limit: config.maxEventsFetch }),
|
||||
config.relayTimeoutMs
|
||||
).then((r) => (Array.isArray(r) ? r : []))
|
||||
.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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user