import { error, json } from "@sveltejs/kit"; import { BASE_DOMAIN, MAX_SATS_RECEIVE, MIN_SATS_RECEIVE } from "$lib/config"; import { isNpub, userExists, userExistsOrNpub, validateLightningInvoiceAmount, } from "$lib"; import { getDb } from "$lib/database"; import { nip19 } from "nostr-tools"; interface LNURLPayRequest { callback: string; minSendable: number; maxSendable: number; metadata: string; tag: "payRequest"; } /** @type {import('./$types').RequestHandler} */ export async function GET({ params, url, request }) { const { username } = params; const amount = url.searchParams.get("amount"); if (!/^[a-z0-9\-_.]+$/.test(username)) { throw error(400, "Invalid username format"); } if (!(await userExistsOrNpub(username))) { throw error(404, "User not found"); } if ( amount && !validateLightningInvoiceAmount(amount, { isMillisats: true }) ) { throw error( 400, `Amount must be between ${MIN_SATS_RECEIVE} and ${MAX_SATS_RECEIVE} millisats`, ); } const domain = request.headers.get("host") || new URL(BASE_DOMAIN).host; const identifier = `${username}@${domain}`; const response: LNURLPayRequest = { callback: `https://${domain}/.well-known/lnurl-pay/callback/${ encodeURIComponent( username, ) }`, minSendable: MIN_SATS_RECEIVE * 1000, maxSendable: MAX_SATS_RECEIVE * 1000, metadata: JSON.stringify([ ["text/plain", `Pay ${username}`], ["text/identifier", identifier], ]), tag: "payRequest", }; return json(response); } /** @type {import('./$types').RequestHandler} */ export async function POST({ params, url, request }) { // TODO: creating a username should cost a small amount of sats // 105k sats for 1 character names // 63k sats for 2 character names // 42k sats for 3 character names // 21k sats for 4 character names // 10k sats otherwise const { username } = params; if (username.length > 32) { throw error(400, "Username is too long. Maximum length is 32 characters."); } if (username.length < 5) { throw error( 400, "Username is too short. Minimum length is 5 characters. Shorter usernames are reserved for future use.", ); } if (!/^[a-z0-9\-_]+$/.test(username)) { throw error( 400, "Invalid username format. Only lowercase letters, numbers, and hyphens are allowed.", ); } if (await userExists(username)) { throw error(400, "User already exists"); } if (isNpub(username)) { throw error( 400, "Username is a valid npub. Please use a username instead.", ); } const db = await getDb(); const body = await request.json(); const { npub }: { npub: `npub1${string}`; } = body; if (!npub) { throw error(400, "Missing npub"); } try { const decoded = nip19.decode(npub); if (decoded.type !== "npub") { throw error(400, "Invalid npub"); } } catch { throw error(400, "Invalid npub"); } await db.execute( "INSERT INTO users (username, npub) VALUES (?, ?)", [username, npub], ); return json({ success: true, }); } export async function OPTIONS() { return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", }, }); }