feat: seed-driven pairing over the LNbits nostr-transport (#70) #73
2 changed files with 19 additions and 0 deletions
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>
commit
5179a21da6
|
|
@ -80,7 +80,10 @@ describe('parseSpireSeed', () => {
|
|||
['missing bunker_secret', { ...VALID, bunker_secret: undefined }],
|
||||
['empty relays', { ...VALID, relays: [] }],
|
||||
['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: '' }],
|
||||
['non-ws bunker_relay', { ...VALID, bunker_relay: 'As://bunker.relay/' }],
|
||||
])('rejects %s', (_label, json) => {
|
||||
expect(() => parseSpireSeed(makeSeed(json))).toThrow()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -53,6 +53,20 @@ export interface SpireSeed {
|
|||
|
||||
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. */
|
||||
function base64urlDecode(input: string): string {
|
||||
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')) {
|
||||
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
|
||||
// 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) {
|
||||
throw new Error('parseSpireSeed: bunker_relay, if present, must be a non-empty string')
|
||||
}
|
||||
assertRelayUrl(obj.bunker_relay, 'bunker_relay')
|
||||
bunkerRelay = obj.bunker_relay
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue