Add Swagger docs at /docs and /openapi.json; frontend and backend updates

Made-with: Cursor
This commit is contained in:
SatsFaucet
2026-03-01 01:24:51 +01:00
parent bdb4892014
commit 381597c96f
20 changed files with 1214 additions and 98 deletions

View File

@@ -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",

View File

@@ -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,