You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
6.1 KiB
193 lines
6.1 KiB
/** |
|
* API endpoint for verifying user level (relay write access) |
|
* This must be done server-side for security - client-side checks can be bypassed |
|
* |
|
* User only needs to be able to write to ONE of the default relays, not all. |
|
* This is a trust mechanism - users who can write to trusted default relays |
|
* get unlimited access, limiting access to trusted npubs. |
|
*/ |
|
|
|
import { json, error } from '@sveltejs/kit'; |
|
import type { RequestHandler } from './$types'; |
|
import { verifyRelayWriteProof } from '$lib/services/nostr/relay-write-proof.js'; |
|
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; |
|
import { auditLogger } from '$lib/services/security/audit-logger.js'; |
|
import { getCachedUserLevel, cacheUserLevel } from '$lib/services/security/user-level-cache.js'; |
|
import { extractRequestContext } from '$lib/utils/api-context.js'; |
|
import { sanitizeError } from '$lib/utils/security.js'; |
|
import { verifyEvent } from 'nostr-tools'; |
|
import logger from '$lib/services/logger.js'; |
|
|
|
export const POST: RequestHandler = async (event) => { |
|
const requestContext = extractRequestContext(event); |
|
const clientIp = requestContext.clientIp || 'unknown'; |
|
|
|
try { |
|
const body = await event.request.json(); |
|
const { proofEvent, userPubkeyHex } = body; |
|
|
|
// Validate input |
|
if (!proofEvent || !userPubkeyHex) { |
|
auditLogger.logAuth( |
|
null, |
|
clientIp, |
|
'NIP-98', |
|
'failure', |
|
'Missing proof event or user pubkey' |
|
); |
|
return error(400, 'Missing required fields: proofEvent and userPubkeyHex'); |
|
} |
|
|
|
// Validate pubkey format (should be hex, 64 characters) |
|
if (typeof userPubkeyHex !== 'string' || !/^[0-9a-f]{64}$/i.test(userPubkeyHex)) { |
|
auditLogger.logAuth( |
|
null, |
|
clientIp, |
|
'NIP-98', |
|
'failure', |
|
'Invalid pubkey format' |
|
); |
|
return error(400, 'Invalid pubkey format'); |
|
} |
|
|
|
// Validate proof event structure |
|
if (!proofEvent.kind || !proofEvent.pubkey || !proofEvent.created_at || !proofEvent.id) { |
|
auditLogger.logAuth( |
|
userPubkeyHex, |
|
clientIp, |
|
'NIP-98', |
|
'failure', |
|
'Invalid proof event structure' |
|
); |
|
return error(400, 'Invalid proof event structure'); |
|
} |
|
|
|
// Validate proof event signature first (even if using cache, user must prove they have private key) |
|
if (!verifyEvent(proofEvent)) { |
|
auditLogger.logAuth( |
|
userPubkeyHex, |
|
clientIp, |
|
'NIP-98', |
|
'failure', |
|
'Invalid proof event signature' |
|
); |
|
return error(400, 'Invalid proof event signature'); |
|
} |
|
|
|
// Verify pubkey matches |
|
if (proofEvent.pubkey !== userPubkeyHex) { |
|
auditLogger.logAuth( |
|
userPubkeyHex, |
|
clientIp, |
|
'NIP-98', |
|
'failure', |
|
'Proof event pubkey does not match user pubkey' |
|
); |
|
return error(400, 'Proof event pubkey does not match user pubkey'); |
|
} |
|
|
|
// Check cache (if relays are down, use cached value) |
|
// But user must still provide valid proof event signed with their private key |
|
const cached = getCachedUserLevel(userPubkeyHex); |
|
if (cached) { |
|
logger.info({ userPubkeyHex, level: cached.level }, '[API] Using cached user level (proof event signature validated)'); |
|
return json({ |
|
level: cached.level, |
|
verified: true, |
|
cached: true |
|
// SECURITY: Removed userPubkeyHex - client already knows their own pubkey |
|
}); |
|
} |
|
|
|
// Verify relay write proof server-side |
|
const verification = await verifyRelayWriteProof( |
|
proofEvent, |
|
userPubkeyHex, |
|
DEFAULT_NOSTR_RELAYS |
|
); |
|
|
|
// If relays are down, check cache again (might have been cached from previous request) |
|
if (verification.relayDown) { |
|
const cachedOnRelayDown = getCachedUserLevel(userPubkeyHex); |
|
if (cachedOnRelayDown) { |
|
logger.info({ userPubkeyHex, level: cachedOnRelayDown.level }, '[API] Relays down, using cached user level'); |
|
auditLogger.logAuth( |
|
userPubkeyHex, |
|
clientIp, |
|
'NIP-98', |
|
'success', |
|
'Relays down, using cached access level' |
|
); |
|
return json({ |
|
level: cachedOnRelayDown.level, |
|
verified: true, |
|
cached: true, |
|
relayDown: true |
|
// SECURITY: Removed userPubkeyHex - client already knows their own pubkey |
|
}); |
|
} |
|
|
|
// No cache available and relays are down - return error |
|
auditLogger.logAuth( |
|
userPubkeyHex, |
|
clientIp, |
|
'NIP-98', |
|
'failure', |
|
'Relays unavailable and no cached access level' |
|
); |
|
return error(503, 'Relays are temporarily unavailable. Please try again later.'); |
|
} |
|
|
|
if (verification.valid) { |
|
// User has write access - unlimited level |
|
// Cache the successful verification |
|
cacheUserLevel(userPubkeyHex, 'unlimited'); |
|
|
|
auditLogger.logAuth( |
|
userPubkeyHex, |
|
clientIp, |
|
'NIP-98', |
|
'success', |
|
'Relay write access verified' |
|
); |
|
|
|
return json({ |
|
level: 'unlimited', |
|
verified: true |
|
// SECURITY: Removed userPubkeyHex - client already knows their own pubkey |
|
}); |
|
} else { |
|
// User is logged in but no write access - rate limited |
|
// Cache this level too (so they don't lose access if relays go down) |
|
cacheUserLevel(userPubkeyHex, 'rate_limited'); |
|
|
|
auditLogger.logAuth( |
|
userPubkeyHex, |
|
clientIp, |
|
'NIP-98', |
|
'success', |
|
'Authenticated but no relay write access' |
|
); |
|
|
|
return json({ |
|
level: 'rate_limited', |
|
verified: true, |
|
error: verification.error |
|
// SECURITY: Removed userPubkeyHex - client already knows their own pubkey |
|
}); |
|
} |
|
} catch (err) { |
|
const errorMessage = err instanceof Error ? err.message : String(err); |
|
logger.error({ error: err, clientIp }, '[API] Error verifying user level'); |
|
|
|
auditLogger.logAuth( |
|
null, |
|
clientIp, |
|
'NIP-98', |
|
'failure', |
|
sanitizeError(errorMessage) |
|
); |
|
|
|
return error(500, 'Failed to verify user level'); |
|
} |
|
};
|
|
|