Add Swagger docs at /docs and /openapi.json; frontend and backend updates
Made-with: Cursor
This commit is contained in:
@@ -96,7 +96,10 @@ export async function checkEligibility(pubkey: string, ipHash: string): Promise<
|
||||
const 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) {
|
||||
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}`);
|
||||
return {
|
||||
eligible: false,
|
||||
denialCode: "account_too_new",
|
||||
|
||||
@@ -19,6 +19,27 @@ 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.
|
||||
*/
|
||||
async function probeOldEvent(pubkey: string, untilTimestamp: number): Promise<number | null> {
|
||||
try {
|
||||
const ev = await withTimeout(
|
||||
pool.get(config.nostrRelays, {
|
||||
kinds: [0, 1, 3],
|
||||
authors: [pubkey],
|
||||
until: untilTimestamp,
|
||||
limit: 1,
|
||||
}),
|
||||
config.relayTimeoutMs
|
||||
);
|
||||
return ev ? ev.created_at : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch events from relays in parallel (kinds 0, 1, 3), compute metrics, optionally cache.
|
||||
* When forceRefreshProfile is true, always fetch from relays (skip cache) so kind 0 is parsed and lightning_address/name updated.
|
||||
@@ -40,15 +61,22 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
||||
};
|
||||
}
|
||||
|
||||
let events: { kind: number; created_at: number; content?: string; tags: string[][] }[] = [];
|
||||
try {
|
||||
const result = await withTimeout(
|
||||
pool.querySync(config.nostrRelays, { kinds: [0, 1, 3], authors: [pubkey], limit: config.maxEventsFetch }),
|
||||
config.relayTimeoutMs
|
||||
);
|
||||
events = Array.isArray(result) ? result : [];
|
||||
} catch (_) {
|
||||
// Timeout or relay error: use cache if any; otherwise upsert minimal user so /refresh-profile returns a row
|
||||
// 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;
|
||||
|
||||
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 [events, oldEventTimestamp] = await Promise.all([mainQueryPromise, ageProbePromise]);
|
||||
|
||||
if (events.length === 0 && oldEventTimestamp === null) {
|
||||
// Both queries failed or returned nothing
|
||||
if (cached) {
|
||||
return {
|
||||
nostrFirstSeenAt: cached.nostr_first_seen_at,
|
||||
@@ -83,10 +111,24 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
||||
const kind1 = events.filter((e) => e.kind === 1);
|
||||
const kind3 = events.filter((e) => e.kind === 3);
|
||||
|
||||
const earliestCreatedAt = events.length
|
||||
// Determine earliest known timestamp from all sources
|
||||
let earliestCreatedAt: number | null = events.length
|
||||
? Math.min(...events.map((e) => e.created_at))
|
||||
: null;
|
||||
|
||||
if (oldEventTimestamp !== null) {
|
||||
earliestCreatedAt = earliestCreatedAt === null
|
||||
? oldEventTimestamp
|
||||
: Math.min(earliestCreatedAt, oldEventTimestamp);
|
||||
}
|
||||
|
||||
// Never overwrite a known-older timestamp with a newer one
|
||||
if (cached?.nostr_first_seen_at != null) {
|
||||
earliestCreatedAt = earliestCreatedAt === null
|
||||
? cached.nostr_first_seen_at
|
||||
: Math.min(earliestCreatedAt, cached.nostr_first_seen_at);
|
||||
}
|
||||
|
||||
const lookbackSince = nowSec - config.activityLookbackDays * 86400;
|
||||
const notesInLookback = kind1.filter((e) => e.created_at >= lookbackSince).length;
|
||||
|
||||
@@ -110,7 +152,6 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
||||
if (kind0.length > 0 && kind0[0].content) {
|
||||
try {
|
||||
const meta = JSON.parse(kind0[0].content) as Record<string, unknown>;
|
||||
// NIP-19 / common: lud16 is the Lightning address (user@domain). Fallbacks for other clients.
|
||||
for (const key of ["lud16", "lightning", "ln_address", "nip05"] as const) {
|
||||
const v = meta[key];
|
||||
if (typeof v === "string") {
|
||||
@@ -129,6 +170,8 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
||||
const nostrFirstSeenAt = earliestCreatedAt;
|
||||
const lastMetadataFetchAt = Math.floor(Date.now() / 1000);
|
||||
|
||||
console.log(`[nostr] pubkey=${pubkey.slice(0, 8)}… events=${events.length} ageProbe=${oldEventTimestamp} firstSeen=${nostrFirstSeenAt} score=${score}`);
|
||||
|
||||
await db.upsertUser({
|
||||
pubkey,
|
||||
nostr_first_seen_at: nostrFirstSeenAt,
|
||||
|
||||
Reference in New Issue
Block a user