mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 05:33:12 +02:00
Address review feedback on Relay Admin app
- sanitizeIconUrl() now only allows http:// for local relay hostnames (localhost/127.0.0.1/::1/*.local), matching its docstring and error message instead of accepting arbitrary http:// URLs. - Scope the discovery-time cache clear to the previous session's own relay URL instead of removing every ['nip86'] query, so connecting in one Relay Admin window no longer disrupts other open windows. - Use the AllowedPubkey type (not BannedPubkey) when mapping allowed- pubkey entries, since the structural overlap today made a real type mismatch invisible. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
parsePubkeyInput,
|
||||
validateIpInput,
|
||||
validateReason,
|
||||
type AllowedPubkey,
|
||||
type BannedPubkey,
|
||||
type BlockedIp,
|
||||
type Nip86CoreMethod,
|
||||
@@ -400,7 +401,7 @@ export function AllowedPubkeysSection({
|
||||
/>
|
||||
) : (
|
||||
<ul className="divide-y divide-border border-t border-border">
|
||||
{filtered.map((entry: BannedPubkey) => (
|
||||
{filtered.map((entry: AllowedPubkey) => (
|
||||
<PolicyRow
|
||||
key={entry.pubkey}
|
||||
id={entry.pubkey}
|
||||
|
||||
@@ -191,6 +191,12 @@ export function useNip86Connection(): Nip86Connection {
|
||||
}
|
||||
|
||||
const { core, extensions } = partitionMethods(methodsResult);
|
||||
// A different relay must never show the previous relay's policy lists
|
||||
// — but query keys are already scoped by URL, so only this session's
|
||||
// own prior relay (not every open Relay Admin window) needs clearing.
|
||||
if (session && session.url !== url) {
|
||||
queryClient.removeQueries({ queryKey: ['nip86', session.url] });
|
||||
}
|
||||
setSession({
|
||||
url,
|
||||
info,
|
||||
@@ -198,8 +204,6 @@ export function useNip86Connection(): Nip86Connection {
|
||||
extensions,
|
||||
canListRoles: methodsResult.includes('listroles'),
|
||||
});
|
||||
// A different relay must never show the previous relay's policy lists.
|
||||
queryClient.removeQueries({ queryKey: ['nip86'] });
|
||||
record({
|
||||
method: 'supportedmethods',
|
||||
target: 'discovery',
|
||||
@@ -223,7 +227,7 @@ export function useNip86Connection(): Nip86Connection {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
},
|
||||
[user, queryClient, record],
|
||||
[user, queryClient, record, session],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -182,6 +182,12 @@ describe('input validation', () => {
|
||||
expect(sanitizeIconUrl('not a url')).toHaveProperty('error');
|
||||
});
|
||||
|
||||
it('only allows http:// for local relays', () => {
|
||||
expect(sanitizeIconUrl('http://localhost:4869/icon.png')).toBe('http://localhost:4869/icon.png');
|
||||
expect(sanitizeIconUrl('http://127.0.0.1/icon.png')).toBe('http://127.0.0.1/icon.png');
|
||||
expect(sanitizeIconUrl('http://relay.example.com/icon.png')).toHaveProperty('error');
|
||||
});
|
||||
|
||||
it('caps reason length', () => {
|
||||
expect(validateReason('spam')).toBeUndefined();
|
||||
expect(validateReason('x'.repeat(501))).toBeTruthy();
|
||||
|
||||
@@ -549,6 +549,12 @@ export function validateRoleColor(input: string): string | undefined {
|
||||
: 'Use a hex color like #8b5cf6, or leave it empty.';
|
||||
}
|
||||
|
||||
/** Loopback/`.local` hostnames — the only ones http:// is trusted for below. */
|
||||
function isLocalHostname(hostname: string): boolean {
|
||||
const host = hostname.toLowerCase();
|
||||
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host.endsWith('.local');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize a relay icon URL before it is shown or submitted.
|
||||
* Only https (and http for local relays) URLs survive — anything else could
|
||||
@@ -559,8 +565,8 @@ export function sanitizeIconUrl(input: string): string | { error: string } {
|
||||
if (!value) return { error: 'Enter an icon URL.' };
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
||||
return { error: 'Only https:// icon URLs are allowed.' };
|
||||
if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && isLocalHostname(parsed.hostname))) {
|
||||
return { error: 'Only https:// icon URLs are allowed (http:// only for local relays).' };
|
||||
}
|
||||
return parsed.href;
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user