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.
 
 
 
 
 

142 lines
3.8 KiB

/**
* Service for determining user access level based on relay write capability
*
* SECURITY: User level verification is done server-side via API endpoint.
* Client-side checks are only for UI purposes and can be bypassed.
*
* Three tiers:
* - unlimited: Users with write access to default relays
* - rate_limited: Logged-in users without write access
* - strictly_rate_limited: Not logged-in users
*/
import { signEventWithNIP07, isNIP07Available } from './nip07-signer.js';
import { KIND } from '../../types/nostr.js';
import { createProofEvent } from './relay-write-proof.js';
import { nip19 } from 'nostr-tools';
export type UserLevel = 'unlimited' | 'rate_limited' | 'strictly_rate_limited';
export interface UserLevelResult {
level: UserLevel;
userPubkey: string | null;
userPubkeyHex: string | null;
error?: string;
}
/**
* Check if a user can write to default relays by creating and verifying a proof event
* SECURITY: This creates the proof event client-side, but verification is done server-side
*/
export async function checkRelayWriteAccess(
userPubkeyHex: string
): Promise<{ hasAccess: boolean; error?: string }> {
if (!isNIP07Available()) {
return { hasAccess: false, error: 'NIP-07 extension not available' };
}
try {
// Create a proof event (kind 1 text note)
const proofEventTemplate = createProofEvent(
userPubkeyHex,
`gitrepublic-write-proof-${Date.now()}`
);
// Sign the event with NIP-07
const signedEvent = await signEventWithNIP07(proofEventTemplate);
// Verify server-side via API endpoint (secure)
const response = await fetch('/api/user/level', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
proofEvent: signedEvent,
userPubkeyHex
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
return {
hasAccess: false,
error: errorData.error || `Server error: ${response.status}`
};
}
const result = await response.json();
return {
hasAccess: result.level === 'unlimited',
error: result.error
};
} catch (error) {
return {
hasAccess: false,
error: error instanceof Error ? error.message : 'Unknown error checking relay write access'
};
}
}
/**
* Determine user level based on authentication and relay write access
* This is the main function to call to get user level
*/
export async function determineUserLevel(
userPubkey: string | null,
userPubkeyHex: string | null
): Promise<UserLevelResult> {
// Not logged in
if (!userPubkey || !userPubkeyHex) {
return {
level: 'strictly_rate_limited',
userPubkey: null,
userPubkeyHex: null
};
}
// Check if user has write access to default relays
const writeAccess = await checkRelayWriteAccess(userPubkeyHex);
if (writeAccess.hasAccess) {
return {
level: 'unlimited',
userPubkey,
userPubkeyHex
};
}
// Logged in but no write access
return {
level: 'rate_limited',
userPubkey,
userPubkeyHex,
error: writeAccess.error
};
}
/**
* Helper to decode npub to hex if needed
* Handles both npub (bech32) and hex formats
*/
export function decodePubkey(pubkey: string): string | null {
if (!pubkey) return null;
// Check if it's already hex (64 characters, hex format)
if (/^[0-9a-f]{64}$/i.test(pubkey)) {
return pubkey.toLowerCase();
}
// Try to decode as npub (bech32)
try {
const decoded = nip19.decode(pubkey);
if (decoded.type === 'npub') {
return decoded.data as string;
}
return pubkey; // Unknown type, return as-is
} catch {
// Not a valid npub, assume it's already hex or return as-is
return pubkey;
}
}