first commit

Made-with: Cursor
This commit is contained in:
Michilis
2026-04-01 02:46:53 +00:00
commit 76210db03d
126 changed files with 20208 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { verifyEvent, type VerifiedEvent, nip19 } from 'nostr-tools';
import { prisma } from '../db/prisma';
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production';
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
interface StoredChallenge {
challenge: string;
expiresAt: number;
}
const challenges = new Map<string, StoredChallenge>();
// Periodically clean up expired challenges
setInterval(() => {
const now = Date.now();
for (const [key, value] of challenges) {
if (value.expiresAt < now) {
challenges.delete(key);
}
}
}, 60_000);
export const authService = {
createChallenge(pubkey: string): string {
const challenge = uuidv4();
challenges.set(pubkey, {
challenge,
expiresAt: Date.now() + CHALLENGE_TTL_MS,
});
return challenge;
},
verifySignature(pubkey: string, signedEvent: VerifiedEvent): boolean {
const stored = challenges.get(pubkey);
if (!stored) return false;
if (stored.expiresAt < Date.now()) {
challenges.delete(pubkey);
return false;
}
// Verify the event signature
if (!verifyEvent(signedEvent)) return false;
// Kind 22242 is the NIP-42 auth kind
if (signedEvent.kind !== 22242) return false;
if (signedEvent.pubkey !== pubkey) return false;
// Check that the challenge tag matches
const challengeTag = signedEvent.tags.find(
(t) => t[0] === 'challenge'
);
if (!challengeTag || challengeTag[1] !== stored.challenge) return false;
challenges.delete(pubkey);
return true;
},
generateToken(pubkey: string, role: string): string {
return jwt.sign({ pubkey, role }, JWT_SECRET, { expiresIn: '7d' });
},
async getRole(pubkey: string): Promise<string> {
const adminPubkeys = (process.env.ADMIN_PUBKEYS || '')
.split(',')
.map((p) => p.trim())
.filter(Boolean)
.map((p) => {
if (p.startsWith('npub1')) {
try {
const { data } = nip19.decode(p);
return data as string;
} catch {
return p;
}
}
return p;
});
if (adminPubkeys.includes(pubkey)) return 'ADMIN';
const user = await prisma.user.findUnique({ where: { pubkey } });
if (user?.role === 'MODERATOR') return 'MODERATOR';
if (user?.role === 'ADMIN') return 'ADMIN';
return 'USER';
},
};

View File

@@ -0,0 +1,146 @@
import { SimplePool, nip19 } from 'nostr-tools';
import { prisma } from '../db/prisma';
const pool = new SimplePool();
async function getRelayUrls(): Promise<string[]> {
const relays = await prisma.relay.findMany({
where: { active: true },
orderBy: { priority: 'asc' },
});
return relays.map((r) => r.url);
}
export const nostrService = {
async fetchEvent(eventId: string, skipCache = false) {
if (!skipCache) {
const cached = await prisma.nostrEventCache.findUnique({
where: { eventId },
});
if (cached) {
return {
id: cached.eventId,
kind: cached.kind,
pubkey: cached.pubkey,
content: cached.content,
tags: JSON.parse(cached.tags),
created_at: cached.createdAt,
};
}
}
const relays = await getRelayUrls();
if (relays.length === 0) return null;
try {
const event = await pool.get(relays, { ids: [eventId] });
if (!event) return null;
await prisma.nostrEventCache.upsert({
where: { eventId: event.id },
update: {
content: event.content,
tags: JSON.stringify(event.tags),
},
create: {
eventId: event.id,
kind: event.kind,
pubkey: event.pubkey,
content: event.content,
tags: JSON.stringify(event.tags),
createdAt: event.created_at,
},
});
return event;
} catch (err) {
console.error('Failed to fetch event:', err);
return null;
}
},
async fetchLongformEvent(naddrStr: string) {
let decoded: nip19.AddressPointer;
try {
const result = nip19.decode(naddrStr);
if (result.type !== 'naddr') return null;
decoded = result.data;
} catch {
return null;
}
const relays = decoded.relays?.length
? decoded.relays
: await getRelayUrls();
if (relays.length === 0) return null;
try {
const event = await pool.get(relays, {
kinds: [decoded.kind],
authors: [decoded.pubkey],
'#d': [decoded.identifier],
});
if (!event) return null;
await prisma.nostrEventCache.upsert({
where: { eventId: event.id },
update: {
content: event.content,
tags: JSON.stringify(event.tags),
},
create: {
eventId: event.id,
kind: event.kind,
pubkey: event.pubkey,
content: event.content,
tags: JSON.stringify(event.tags),
createdAt: event.created_at,
},
});
return event;
} catch (err) {
console.error('Failed to fetch longform event:', err);
return null;
}
},
async fetchReactions(eventId: string) {
const relays = await getRelayUrls();
if (relays.length === 0) return [];
try {
const events = await pool.querySync(relays, {
kinds: [7],
'#e': [eventId],
});
return events;
} catch (err) {
console.error('Failed to fetch reactions:', err);
return [];
}
},
async fetchReplies(eventId: string) {
const relays = await getRelayUrls();
if (relays.length === 0) return [];
try {
const events = await pool.querySync(relays, {
kinds: [1],
'#e': [eventId],
});
return events;
} catch (err) {
console.error('Failed to fetch replies:', err);
return [];
}
},
async getRelays() {
return prisma.relay.findMany({
where: { active: true },
orderBy: { priority: 'asc' },
});
},
};