mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf7e00d32a | ||
|
|
9241089997 | ||
|
|
32c47e9bd8 | ||
|
|
6e58fe371c | ||
|
|
26e35d50e0 | ||
|
|
ef3184a6e0 | ||
|
|
56fe3dd5dd | ||
|
|
f1bb5030c8 | ||
|
|
ac212cb5c8 | ||
|
|
204ae0eff1 | ||
|
|
f17ab41d72 |
1
index.ts
1
index.ts
@@ -13,6 +13,7 @@ export * as nip13 from './nip13'
|
||||
export * as nip19 from './nip19'
|
||||
export * as nip26 from './nip26'
|
||||
export * as nip39 from './nip39'
|
||||
export * as nip42 from './nip42'
|
||||
export * as nip57 from './nip57'
|
||||
|
||||
export * as fj from './fakejson'
|
||||
|
||||
@@ -100,3 +100,12 @@ test('decode naddr from go-nostr with different TLV ordering', () => {
|
||||
expect(data.kind).toEqual(30023)
|
||||
expect(data.identifier).toEqual('banana')
|
||||
})
|
||||
|
||||
test('encode and decode nrelay', () => {
|
||||
let url = "wss://relay.nostr.example"
|
||||
let nrelay = nip19.nrelayEncode(url)
|
||||
expect(nrelay).toMatch(/nrelay1\w+/)
|
||||
let {type, data} = nip19.decode(nrelay)
|
||||
expect(type).toEqual('nrelay')
|
||||
expect(data).toEqual(url)
|
||||
})
|
||||
|
||||
32
nip19.ts
32
nip19.ts
@@ -23,10 +23,16 @@ export type AddressPointer = {
|
||||
relays?: string[]
|
||||
}
|
||||
|
||||
export function decode(nip19: string): {
|
||||
type: string
|
||||
data: ProfilePointer | EventPointer | AddressPointer | string
|
||||
} {
|
||||
export type DecodeResult =
|
||||
| {type: 'nprofile'; data: ProfilePointer}
|
||||
| {type: 'nrelay'; data: string}
|
||||
| {type: 'nevent'; data: EventPointer}
|
||||
| {type: 'naddr'; data: AddressPointer}
|
||||
| {type: 'nsec'; data: string}
|
||||
| {type: 'npub'; data: string}
|
||||
| {type: 'note'; data: string}
|
||||
|
||||
export function decode(nip19: string): DecodeResult {
|
||||
let {prefix, words} = bech32.decode(nip19, Bech32MaxSize)
|
||||
let data = new Uint8Array(bech32.fromWords(words))
|
||||
|
||||
@@ -82,6 +88,16 @@ export function decode(nip19: string): {
|
||||
}
|
||||
}
|
||||
|
||||
case 'nrelay': {
|
||||
let tlv = parseTLV(data)
|
||||
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nrelay')
|
||||
|
||||
return {
|
||||
type: 'nrelay',
|
||||
data: utf8Decoder.decode(tlv[0][0])
|
||||
}
|
||||
}
|
||||
|
||||
case 'nsec':
|
||||
case 'npub':
|
||||
case 'note':
|
||||
@@ -160,6 +176,14 @@ export function naddrEncode(addr: AddressPointer): string {
|
||||
return bech32.encode('naddr', words, Bech32MaxSize)
|
||||
}
|
||||
|
||||
export function nrelayEncode(url: string): string {
|
||||
let data = encodeTLV({
|
||||
0: [utf8Encoder.encode(url)]
|
||||
})
|
||||
let words = bech32.toWords(data)
|
||||
return bech32.encode('nrelay', words, Bech32MaxSize)
|
||||
}
|
||||
|
||||
function encodeTLV(tlv: TLV): Uint8Array {
|
||||
let entries: Uint8Array[] = []
|
||||
|
||||
|
||||
27
nip42.test.js
Normal file
27
nip42.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
require('websocket-polyfill')
|
||||
const {
|
||||
relayInit,
|
||||
generatePrivateKey,
|
||||
finishEvent,
|
||||
nip42
|
||||
} = require('./lib/nostr.cjs')
|
||||
|
||||
test('auth flow', done => {
|
||||
const relay = relayInit('wss://nostr.kollider.xyz')
|
||||
relay.connect()
|
||||
const sk = generatePrivateKey()
|
||||
|
||||
relay.on('auth', async challenge => {
|
||||
await expect(
|
||||
nip42.authenticate({
|
||||
challenge,
|
||||
relay,
|
||||
sign: e => finishEvent(e, sk)
|
||||
})
|
||||
).rejects.toBeTruthy()
|
||||
relay.close()
|
||||
done()
|
||||
})
|
||||
})
|
||||
42
nip42.ts
Normal file
42
nip42.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {EventTemplate, Event, Kind} from './event'
|
||||
import {Relay} from './relay'
|
||||
|
||||
/**
|
||||
* Authenticate via NIP-42 flow.
|
||||
*
|
||||
* @example
|
||||
* const sign = window.nostr.signEvent
|
||||
* relay.on('auth', challenge =>
|
||||
* authenticate({ relay, sign, challenge })
|
||||
* )
|
||||
*/
|
||||
export const authenticate = async ({
|
||||
challenge,
|
||||
relay,
|
||||
sign
|
||||
}: {
|
||||
challenge: string
|
||||
relay: Relay
|
||||
sign: (e: EventTemplate) => Promise<Event>
|
||||
}): Promise<void> => {
|
||||
const e: EventTemplate = {
|
||||
kind: Kind.ClientAuth,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['relay', relay.url],
|
||||
['challenge', challenge]
|
||||
],
|
||||
content: ''
|
||||
}
|
||||
const pub = relay.auth(await sign(e))
|
||||
return new Promise((resolve, reject) => {
|
||||
pub.on('ok', function ok() {
|
||||
pub.off('ok', ok)
|
||||
resolve()
|
||||
})
|
||||
pub.on('failed', function fail(reason: string) {
|
||||
pub.off('failed', fail)
|
||||
reject(reason)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nostr-tools",
|
||||
"version": "1.8.4",
|
||||
"version": "1.10.0",
|
||||
"description": "Tools for making a Nostr client.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,7 +19,7 @@
|
||||
"license": "Public domain",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.2.0",
|
||||
"@noble/secp256k1": "1.7.0",
|
||||
"@noble/secp256k1": "1.7.1",
|
||||
"@scure/base": "1.1.1",
|
||||
"@scure/bip32": "1.1.4",
|
||||
"@scure/bip39": "1.1.1"
|
||||
|
||||
4
pool.ts
4
pool.ts
@@ -2,7 +2,7 @@ import {Relay, relayInit} from './relay'
|
||||
import {normalizeURL} from './utils'
|
||||
import {Filter} from './filter'
|
||||
import {Event} from './event'
|
||||
import {SubscriptionOptions, Sub, Pub} from './relay'
|
||||
import {SubscriptionOptions, Sub, Pub, CountPayload} from './relay'
|
||||
|
||||
export class SimplePool {
|
||||
private _conn: {[url: string]: Relay}
|
||||
@@ -53,7 +53,7 @@ export class SimplePool {
|
||||
}
|
||||
|
||||
let subs: Sub[] = []
|
||||
let eventListeners: Set<(event: Event) => void> = new Set()
|
||||
let eventListeners: Set<any> = new Set()
|
||||
let eoseListeners: Set<() => void> = new Set()
|
||||
let eosesMissing = relays.length
|
||||
|
||||
|
||||
112
relay.ts
112
relay.ts
@@ -9,9 +9,14 @@ type RelayEvent = {
|
||||
disconnect: () => void | Promise<void>
|
||||
error: () => void | Promise<void>
|
||||
notice: (msg: string) => void | Promise<void>
|
||||
auth: (challenge: string) => void | Promise<void>
|
||||
}
|
||||
export type CountPayload = {
|
||||
count: number
|
||||
}
|
||||
type SubEvent = {
|
||||
event: (event: Event) => void | Promise<void>
|
||||
count: (payload: CountPayload) => void | Promise<void>
|
||||
eose: () => void | Promise<void>
|
||||
}
|
||||
export type Relay = {
|
||||
@@ -22,7 +27,12 @@ export type Relay = {
|
||||
sub: (filters: Filter[], opts?: SubscriptionOptions) => Sub
|
||||
list: (filters: Filter[], opts?: SubscriptionOptions) => Promise<Event[]>
|
||||
get: (filter: Filter, opts?: SubscriptionOptions) => Promise<Event | null>
|
||||
count: (
|
||||
filters: Filter[],
|
||||
opts?: SubscriptionOptions
|
||||
) => Promise<CountPayload | null>
|
||||
publish: (event: Event) => Pub
|
||||
auth: (event: Event) => Pub
|
||||
off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
|
||||
event: T,
|
||||
listener: U
|
||||
@@ -51,27 +61,32 @@ export type Sub = {
|
||||
|
||||
export type SubscriptionOptions = {
|
||||
id?: string
|
||||
verb?: 'REQ' | 'COUNT'
|
||||
skipVerification?: boolean
|
||||
alreadyHaveEvent?: null | ((id: string, relay: string) => boolean)
|
||||
}
|
||||
|
||||
const newListeners = (): {[TK in keyof RelayEvent]: RelayEvent[TK][]} => ({
|
||||
connect: [],
|
||||
disconnect: [],
|
||||
error: [],
|
||||
notice: [],
|
||||
auth: []
|
||||
})
|
||||
|
||||
export function relayInit(
|
||||
url: string,
|
||||
options: {
|
||||
getTimeout?: number
|
||||
listTimeout?: number
|
||||
countTimeout?: number
|
||||
} = {}
|
||||
): Relay {
|
||||
let {listTimeout = 3000, getTimeout = 3000} = options
|
||||
let {listTimeout = 3000, getTimeout = 3000, countTimeout = 3000} = options
|
||||
|
||||
var ws: WebSocket
|
||||
var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {}
|
||||
var listeners: {[TK in keyof RelayEvent]: RelayEvent[TK][]} = {
|
||||
connect: [],
|
||||
disconnect: [],
|
||||
error: [],
|
||||
notice: []
|
||||
}
|
||||
var listeners = newListeners()
|
||||
var subListeners: {
|
||||
[subid: string]: {[TK in keyof SubEvent]: SubEvent[TK][]}
|
||||
} = {}
|
||||
@@ -146,7 +161,7 @@ export function relayInit(
|
||||
// will naturally be caught by the encompassing try..catch block
|
||||
|
||||
switch (data[0]) {
|
||||
case 'EVENT':
|
||||
case 'EVENT': {
|
||||
let id = data[1]
|
||||
let event = data[2]
|
||||
if (
|
||||
@@ -159,6 +174,14 @@ export function relayInit(
|
||||
;(subListeners[id]?.event || []).forEach(cb => cb(event))
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'COUNT':
|
||||
let id = data[1]
|
||||
let payload = data[2]
|
||||
if (openSubs[id]) {
|
||||
;(subListeners[id]?.count || []).forEach(cb => cb(payload))
|
||||
}
|
||||
return
|
||||
case 'EOSE': {
|
||||
let id = data[1]
|
||||
if (id in subListeners) {
|
||||
@@ -183,6 +206,11 @@ export function relayInit(
|
||||
let notice = data[1]
|
||||
listeners.notice.forEach(cb => cb(notice))
|
||||
return
|
||||
case 'AUTH': {
|
||||
let challenge = data[1]
|
||||
listeners.auth?.forEach(cb => cb(challenge))
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return
|
||||
@@ -220,6 +248,7 @@ export function relayInit(
|
||||
const sub = (
|
||||
filters: Filter[],
|
||||
{
|
||||
verb = 'REQ',
|
||||
skipVerification = false,
|
||||
alreadyHaveEvent = null,
|
||||
id = Math.random().toString().slice(2)
|
||||
@@ -233,7 +262,7 @@ export function relayInit(
|
||||
skipVerification,
|
||||
alreadyHaveEvent
|
||||
}
|
||||
trySend(['REQ', subid, ...filters])
|
||||
trySend([verb, subid, ...filters])
|
||||
|
||||
return {
|
||||
sub: (newFilters, newOpts = {}) =>
|
||||
@@ -253,6 +282,7 @@ export function relayInit(
|
||||
): void => {
|
||||
subListeners[subid] = subListeners[subid] || {
|
||||
event: [],
|
||||
count: [],
|
||||
eose: []
|
||||
}
|
||||
subListeners[subid][type].push(cb)
|
||||
@@ -268,6 +298,29 @@ export function relayInit(
|
||||
}
|
||||
}
|
||||
|
||||
function _publishEvent(event: Event, type: string) {
|
||||
if (!event.id) throw new Error(`event ${event} has no id`)
|
||||
let id = event.id
|
||||
|
||||
trySend([type, event])
|
||||
|
||||
return {
|
||||
on: (type: 'ok' | 'failed', cb: any) => {
|
||||
pubListeners[id] = pubListeners[id] || {
|
||||
ok: [],
|
||||
failed: []
|
||||
}
|
||||
pubListeners[id][type].push(cb)
|
||||
},
|
||||
off: (type: 'ok' | 'failed', cb: any) => {
|
||||
let listeners = pubListeners[id]
|
||||
if (!listeners) return
|
||||
let idx = listeners[type].indexOf(cb)
|
||||
if (idx >= 0) listeners[type].splice(idx, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
sub,
|
||||
@@ -318,31 +371,28 @@ export function relayInit(
|
||||
resolve(event)
|
||||
})
|
||||
}),
|
||||
publish(event: Event): Pub {
|
||||
if (!event.id) throw new Error(`event ${event} has no id`)
|
||||
let id = event.id
|
||||
|
||||
trySend(['EVENT', event])
|
||||
|
||||
return {
|
||||
on: (type: 'ok' | 'failed', cb: any) => {
|
||||
pubListeners[id] = pubListeners[id] || {
|
||||
ok: [],
|
||||
failed: []
|
||||
}
|
||||
pubListeners[id][type].push(cb)
|
||||
},
|
||||
off: (type: 'ok' | 'failed', cb: any) => {
|
||||
let listeners = pubListeners[id]
|
||||
if (!listeners) return
|
||||
let idx = listeners[type].indexOf(cb)
|
||||
if (idx >= 0) listeners[type].splice(idx, 1)
|
||||
}
|
||||
}
|
||||
count: (filters: Filter[]): Promise<CountPayload | null> =>
|
||||
new Promise(resolve => {
|
||||
let s = sub(filters, {...sub, verb: 'COUNT'})
|
||||
let timeout = setTimeout(() => {
|
||||
s.unsub()
|
||||
resolve(null)
|
||||
}, countTimeout)
|
||||
s.on('count', (event: CountPayload) => {
|
||||
s.unsub()
|
||||
clearTimeout(timeout)
|
||||
resolve(event)
|
||||
})
|
||||
}),
|
||||
publish(event): Pub {
|
||||
return _publishEvent(event, 'EVENT')
|
||||
},
|
||||
auth(event): Pub {
|
||||
return _publishEvent(event, 'AUTH')
|
||||
},
|
||||
connect,
|
||||
close(): void {
|
||||
listeners = {connect: [], disconnect: [], error: [], notice: []}
|
||||
listeners = newListeners()
|
||||
subListeners = {}
|
||||
pubListeners = {}
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
|
||||
Reference in New Issue
Block a user