fix(nostr-client): reject non-ws(s):// relays in the spire seed
The npubs in the seed are bech32-checksummed, so a mis-scanned character is caught — but the relay strings are raw inside the base64. A QR misread silently turned `ws://192.168.0.32:5001/...` into `As://192.168.0.32:5001/...`, which parsed fine and then crash-looped the machine on an unreachable NIP-46 relay. Validate every `relays[]` entry (and `bunker_relay`) is a `ws://`/`wss://` URL at parse time, so a garbled scan is rejected as an invalid seed instead of persisted. Part of bitspire-#70 pairing robustness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e57868020b
commit
1e2a653ad1
2 changed files with 19 additions and 0 deletions
|
|
@ -80,7 +80,10 @@ describe('parseSpireSeed', () => {
|
||||||
['missing bunker_secret', { ...VALID, bunker_secret: undefined }],
|
['missing bunker_secret', { ...VALID, bunker_secret: undefined }],
|
||||||
['empty relays', { ...VALID, relays: [] }],
|
['empty relays', { ...VALID, relays: [] }],
|
||||||
['non-string relay', { ...VALID, relays: [123] }],
|
['non-string relay', { ...VALID, relays: [123] }],
|
||||||
|
['non-ws relay (scan corruption ws://→As://)', { ...VALID, relays: ['As://events.relay/'] }],
|
||||||
|
['non-ws relay (http)', { ...VALID, relays: ['http://events.relay/'] }],
|
||||||
['empty bunker_relay', { ...VALID, bunker_relay: '' }],
|
['empty bunker_relay', { ...VALID, bunker_relay: '' }],
|
||||||
|
['non-ws bunker_relay', { ...VALID, bunker_relay: 'As://bunker.relay/' }],
|
||||||
])('rejects %s', (_label, json) => {
|
])('rejects %s', (_label, json) => {
|
||||||
expect(() => parseSpireSeed(makeSeed(json))).toThrow()
|
expect(() => parseSpireSeed(makeSeed(json))).toThrow()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,20 @@ export interface SpireSeed {
|
||||||
|
|
||||||
const HEX64 = /^[0-9a-f]{64}$/
|
const HEX64 = /^[0-9a-f]{64}$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A relay must be a `ws://` or `wss://` URL. Unlike the npubs (bech32-checksummed,
|
||||||
|
* so a mis-scanned character is caught), the relay strings are raw inside the
|
||||||
|
* seed's base64 — a QR misread can silently corrupt `ws://` into e.g. `As://`
|
||||||
|
* and the pairing then crash-loops on an unreachable relay. Reject at parse time
|
||||||
|
* so the wizard refuses a garbled scan instead of persisting it (bitspire#70).
|
||||||
|
*/
|
||||||
|
const WS_URL = /^wss?:\/\/[^\s]+$/
|
||||||
|
function assertRelayUrl(value: string, field: string): void {
|
||||||
|
if (!WS_URL.test(value)) {
|
||||||
|
throw new Error(`parseSpireSeed: ${field} must be a ws:// or wss:// URL (got "${value}")`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Decode an unpadded base64url string in both browser and Node. */
|
/** Decode an unpadded base64url string in both browser and Node. */
|
||||||
function base64urlDecode(input: string): string {
|
function base64urlDecode(input: string): string {
|
||||||
const padded = input.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(input.length / 4) * 4, '=')
|
const padded = input.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(input.length / 4) * 4, '=')
|
||||||
|
|
@ -118,6 +132,7 @@ export function parseSpireSeed(seedUrl: string): SpireSeed {
|
||||||
if (!Array.isArray(relays) || relays.length === 0 || !relays.every((r) => typeof r === 'string')) {
|
if (!Array.isArray(relays) || relays.length === 0 || !relays.every((r) => typeof r === 'string')) {
|
||||||
throw new Error('parseSpireSeed: relays must be a non-empty string array')
|
throw new Error('parseSpireSeed: relays must be a non-empty string array')
|
||||||
}
|
}
|
||||||
|
relays.forEach((r, i) => assertRelayUrl(r as string, `relays[${i}]`))
|
||||||
|
|
||||||
// Optional bunker relay; default to the first event relay. Keeps the common
|
// Optional bunker relay; default to the first event relay. Keeps the common
|
||||||
// case (bunker on the same relay) one field lighter, while still allowing a
|
// case (bunker on the same relay) one field lighter, while still allowing a
|
||||||
|
|
@ -127,6 +142,7 @@ export function parseSpireSeed(seedUrl: string): SpireSeed {
|
||||||
if (typeof obj.bunker_relay !== 'string' || obj.bunker_relay.length === 0) {
|
if (typeof obj.bunker_relay !== 'string' || obj.bunker_relay.length === 0) {
|
||||||
throw new Error('parseSpireSeed: bunker_relay, if present, must be a non-empty string')
|
throw new Error('parseSpireSeed: bunker_relay, if present, must be a non-empty string')
|
||||||
}
|
}
|
||||||
|
assertRelayUrl(obj.bunker_relay, 'bunker_relay')
|
||||||
bunkerRelay = obj.bunker_relay
|
bunkerRelay = obj.bunker_relay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue