mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 00:28:51 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32fd25556b | ||
|
|
0925f5db81 | ||
|
|
bce976fecd | ||
|
|
45e479d7aa | ||
|
|
b92407b156 | ||
|
|
2431896921 | ||
|
|
d13eecad4a | ||
|
|
df6f887d7e | ||
|
|
e00362e7c9 | ||
|
|
9efdd16e26 | ||
|
|
de7e128818 | ||
|
|
4978c858e7 | ||
|
|
16c7ae2a70 | ||
|
|
3368e8c00e | ||
|
|
e5a3ad9855 | ||
|
|
03185c654b | ||
|
|
9d690814ca | ||
|
|
17590cce91 | ||
|
|
ee9f37e192 |
@@ -108,13 +108,7 @@ let event = {
|
|||||||
event.id = getEventHash(event)
|
event.id = getEventHash(event)
|
||||||
event.sig = getSignature(event, sk)
|
event.sig = getSignature(event, sk)
|
||||||
|
|
||||||
let pub = relay.publish(event)
|
await relay.publish(event)
|
||||||
pub.on('ok', () => {
|
|
||||||
console.log(`${relay.url} has accepted our event`)
|
|
||||||
})
|
|
||||||
pub.on('failed', reason => {
|
|
||||||
console.log(`failed to publish to ${relay.url}: ${reason}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
let events = await relay.list([{kinds: [0, 1]}])
|
let events = await relay.list([{kinds: [0, 1]}])
|
||||||
let event = await relay.get({
|
let event = await relay.get({
|
||||||
|
|||||||
12
event.ts
12
event.ts
@@ -5,6 +5,7 @@ import {bytesToHex} from '@noble/hashes/utils'
|
|||||||
import {getPublicKey} from './keys.ts'
|
import {getPublicKey} from './keys.ts'
|
||||||
import {utf8Encoder} from './utils.ts'
|
import {utf8Encoder} from './utils.ts'
|
||||||
|
|
||||||
|
/** @deprecated Use numbers instead. */
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
export enum Kind {
|
export enum Kind {
|
||||||
Metadata = 0,
|
Metadata = 0,
|
||||||
@@ -30,21 +31,22 @@ export enum Kind {
|
|||||||
HttpAuth = 27235,
|
HttpAuth = 27235,
|
||||||
ProfileBadge = 30008,
|
ProfileBadge = 30008,
|
||||||
BadgeDefinition = 30009,
|
BadgeDefinition = 30009,
|
||||||
Article = 30023
|
Article = 30023,
|
||||||
|
FileMetadata = 1063
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventTemplate<K extends number = Kind> = {
|
export type EventTemplate<K extends number = number> = {
|
||||||
kind: K
|
kind: K
|
||||||
tags: string[][]
|
tags: string[][]
|
||||||
content: string
|
content: string
|
||||||
created_at: number
|
created_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UnsignedEvent<K extends number = Kind> = EventTemplate<K> & {
|
export type UnsignedEvent<K extends number = number> = EventTemplate<K> & {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Event<K extends number = Kind> = UnsignedEvent<K> & {
|
export type Event<K extends number = number> = UnsignedEvent<K> & {
|
||||||
id: string
|
id: string
|
||||||
sig: string
|
sig: string
|
||||||
}
|
}
|
||||||
@@ -60,7 +62,7 @@ export function getBlankEvent<K>(kind: K | Kind.Blank = Kind.Blank) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function finishEvent<K extends number = Kind>(
|
export function finishEvent<K extends number = number>(
|
||||||
t: EventTemplate<K>,
|
t: EventTemplate<K>,
|
||||||
privateKey: string
|
privateKey: string
|
||||||
): Event<K> {
|
): Event<K> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Event, type Kind} from './event.ts'
|
import {Event, type Kind} from './event.ts'
|
||||||
|
|
||||||
export type Filter<K extends number = Kind> = {
|
export type Filter<K extends number = number> = {
|
||||||
ids?: string[]
|
ids?: string[]
|
||||||
kinds?: K[]
|
kinds?: K[]
|
||||||
authors?: string[]
|
authors?: string[]
|
||||||
@@ -8,7 +8,7 @@ export type Filter<K extends number = Kind> = {
|
|||||||
until?: number
|
until?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
search?: string
|
search?: string
|
||||||
[key: `#${string}`]: string[]
|
[key: `#${string}`]: string[] | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchFilter(
|
export function matchFilter(
|
||||||
@@ -34,7 +34,7 @@ export function matchFilter(
|
|||||||
if (
|
if (
|
||||||
values &&
|
values &&
|
||||||
!event.tags.find(
|
!event.tags.find(
|
||||||
([t, v]) => t === f.slice(1) && values.indexOf(v) !== -1
|
([t, v]) => t === f.slice(1) && values!.indexOf(v) !== -1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
|||||||
2
index.ts
2
index.ts
@@ -16,8 +16,10 @@ export * as nip21 from './nip21.ts'
|
|||||||
export * as nip25 from './nip25.ts'
|
export * as nip25 from './nip25.ts'
|
||||||
export * as nip26 from './nip26.ts'
|
export * as nip26 from './nip26.ts'
|
||||||
export * as nip27 from './nip27.ts'
|
export * as nip27 from './nip27.ts'
|
||||||
|
export * as nip28 from './nip28.ts'
|
||||||
export * as nip39 from './nip39.ts'
|
export * as nip39 from './nip39.ts'
|
||||||
export * as nip42 from './nip42.ts'
|
export * as nip42 from './nip42.ts'
|
||||||
|
export * as nip44 from './nip44.ts'
|
||||||
export * as nip57 from './nip57.ts'
|
export * as nip57 from './nip57.ts'
|
||||||
export * as nip98 from './nip98.ts'
|
export * as nip98 from './nip98.ts'
|
||||||
|
|
||||||
|
|||||||
134
nip28.test.ts
Normal file
134
nip28.test.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import {Kind} from './event.ts'
|
||||||
|
import {getPublicKey} from './keys.ts'
|
||||||
|
import {
|
||||||
|
channelCreateEvent,
|
||||||
|
channelMetadataEvent,
|
||||||
|
channelMessageEvent,
|
||||||
|
channelHideMessageEvent,
|
||||||
|
channelMuteUserEvent,
|
||||||
|
ChannelMetadata,
|
||||||
|
ChannelMessageEventTemplate
|
||||||
|
} from './nip28.ts'
|
||||||
|
|
||||||
|
const privateKey =
|
||||||
|
'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf'
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
describe('NIP-28 Functions', () => {
|
||||||
|
const channelMetadata: ChannelMetadata = {
|
||||||
|
name: 'Test Channel',
|
||||||
|
about: 'This is a test channel',
|
||||||
|
picture: 'https://example.com/picture.jpg'
|
||||||
|
}
|
||||||
|
|
||||||
|
it('channelCreateEvent should create an event with given template', () => {
|
||||||
|
const template = {
|
||||||
|
content: channelMetadata,
|
||||||
|
created_at: 1617932115
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = channelCreateEvent(template, privateKey)
|
||||||
|
expect(event!.kind).toEqual(Kind.ChannelCreation)
|
||||||
|
expect(event!.content).toEqual(JSON.stringify(template.content))
|
||||||
|
expect(event!.pubkey).toEqual(publicKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('channelMetadataEvent should create a signed event with given template', () => {
|
||||||
|
const template = {
|
||||||
|
channel_create_event_id: 'channel creation event id',
|
||||||
|
content: channelMetadata,
|
||||||
|
created_at: 1617932115
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = channelMetadataEvent(template, privateKey)
|
||||||
|
expect(event!.kind).toEqual(Kind.ChannelMetadata)
|
||||||
|
expect(event!.tags).toEqual([['e', template.channel_create_event_id]])
|
||||||
|
expect(event!.content).toEqual(JSON.stringify(template.content))
|
||||||
|
expect(event!.pubkey).toEqual(publicKey)
|
||||||
|
expect(typeof event!.id).toEqual('string')
|
||||||
|
expect(typeof event!.sig).toEqual('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('channelMessageEvent should create a signed message event with given template', () => {
|
||||||
|
const template = {
|
||||||
|
channel_create_event_id: 'channel creation event id',
|
||||||
|
relay_url: 'https://relay.example.com',
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = channelMessageEvent(template, privateKey)
|
||||||
|
expect(event.kind).toEqual(Kind.ChannelMessage)
|
||||||
|
expect(event.tags[0]).toEqual([
|
||||||
|
'e',
|
||||||
|
template.channel_create_event_id,
|
||||||
|
template.relay_url,
|
||||||
|
'root'
|
||||||
|
])
|
||||||
|
expect(event.content).toEqual(template.content)
|
||||||
|
expect(event.pubkey).toEqual(publicKey)
|
||||||
|
expect(typeof event.id).toEqual('string')
|
||||||
|
expect(typeof event.sig).toEqual('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('channelMessageEvent should create a signed message reply event with given template', () => {
|
||||||
|
const template: ChannelMessageEventTemplate = {
|
||||||
|
channel_create_event_id: 'channel creation event id',
|
||||||
|
reply_to_channel_message_event_id: 'channel message event id',
|
||||||
|
relay_url: 'https://relay.example.com',
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = channelMessageEvent(template, privateKey)
|
||||||
|
expect(event.kind).toEqual(Kind.ChannelMessage)
|
||||||
|
expect(event.tags).toContainEqual([
|
||||||
|
'e',
|
||||||
|
template.channel_create_event_id,
|
||||||
|
template.relay_url,
|
||||||
|
'root'
|
||||||
|
])
|
||||||
|
expect(event.tags).toContainEqual([
|
||||||
|
'e',
|
||||||
|
template.reply_to_channel_message_event_id,
|
||||||
|
template.relay_url,
|
||||||
|
'reply'
|
||||||
|
])
|
||||||
|
expect(event.content).toEqual(template.content)
|
||||||
|
expect(event.pubkey).toEqual(publicKey)
|
||||||
|
expect(typeof event.id).toEqual('string')
|
||||||
|
expect(typeof event.sig).toEqual('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('channelHideMessageEvent should create a signed event with given template', () => {
|
||||||
|
const template = {
|
||||||
|
channel_message_event_id: 'channel message event id',
|
||||||
|
content: {reason: 'Inappropriate content'},
|
||||||
|
created_at: 1617932115
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = channelHideMessageEvent(template, privateKey)
|
||||||
|
expect(event!.kind).toEqual(Kind.ChannelHideMessage)
|
||||||
|
expect(event!.tags).toEqual([['e', template.channel_message_event_id]])
|
||||||
|
expect(event!.content).toEqual(JSON.stringify(template.content))
|
||||||
|
expect(event!.pubkey).toEqual(publicKey)
|
||||||
|
expect(typeof event!.id).toEqual('string')
|
||||||
|
expect(typeof event!.sig).toEqual('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('channelMuteUserEvent should create a signed event with given template', () => {
|
||||||
|
const template = {
|
||||||
|
content: {reason: 'Spamming'},
|
||||||
|
created_at: 1617932115,
|
||||||
|
pubkey_to_mute: 'pubkey to mute'
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = channelMuteUserEvent(template, privateKey)
|
||||||
|
expect(event!.kind).toEqual(Kind.ChannelMuteUser)
|
||||||
|
expect(event!.tags).toEqual([['p', template.pubkey_to_mute]])
|
||||||
|
expect(event!.content).toEqual(JSON.stringify(template.content))
|
||||||
|
expect(event!.pubkey).toEqual(publicKey)
|
||||||
|
expect(typeof event!.id).toEqual('string')
|
||||||
|
expect(typeof event!.sig).toEqual('string')
|
||||||
|
})
|
||||||
|
})
|
||||||
163
nip28.ts
Normal file
163
nip28.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import {Event, finishEvent, Kind} from './event.ts'
|
||||||
|
|
||||||
|
export interface ChannelMetadata {
|
||||||
|
name: string
|
||||||
|
about: string
|
||||||
|
picture: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelCreateEventTemplate {
|
||||||
|
/* JSON string containing ChannelMetadata as defined for Kind 40 and 41 in nip-28. */
|
||||||
|
content: string | ChannelMetadata
|
||||||
|
created_at: number
|
||||||
|
tags?: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelMetadataEventTemplate {
|
||||||
|
channel_create_event_id: string
|
||||||
|
/* JSON string containing ChannelMetadata as defined for Kind 40 and 41 in nip-28. */
|
||||||
|
content: string | ChannelMetadata
|
||||||
|
created_at: number
|
||||||
|
tags?: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelMessageEventTemplate {
|
||||||
|
channel_create_event_id: string
|
||||||
|
reply_to_channel_message_event_id?: string
|
||||||
|
relay_url: string
|
||||||
|
content: string
|
||||||
|
created_at: number
|
||||||
|
tags?: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelHideMessageEventTemplate {
|
||||||
|
channel_message_event_id: string
|
||||||
|
content: string | {reason: string}
|
||||||
|
created_at: number
|
||||||
|
tags?: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelMuteUserEventTemplate {
|
||||||
|
content: string | {reason: string}
|
||||||
|
created_at: number
|
||||||
|
pubkey_to_mute: string
|
||||||
|
tags?: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const channelCreateEvent = (
|
||||||
|
t: ChannelCreateEventTemplate,
|
||||||
|
privateKey: string
|
||||||
|
): Event<Kind.ChannelCreation> | undefined => {
|
||||||
|
let content: string
|
||||||
|
if (typeof t.content === 'object') {
|
||||||
|
content = JSON.stringify(t.content)
|
||||||
|
} else if (typeof t.content === 'string') {
|
||||||
|
content = t.content
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishEvent(
|
||||||
|
{
|
||||||
|
kind: Kind.ChannelCreation,
|
||||||
|
tags: [...(t.tags ?? [])],
|
||||||
|
content: content,
|
||||||
|
created_at: t.created_at
|
||||||
|
},
|
||||||
|
privateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const channelMetadataEvent = (
|
||||||
|
t: ChannelMetadataEventTemplate,
|
||||||
|
privateKey: string
|
||||||
|
): Event<Kind.ChannelMetadata> | undefined => {
|
||||||
|
let content: string
|
||||||
|
if (typeof t.content === 'object') {
|
||||||
|
content = JSON.stringify(t.content)
|
||||||
|
} else if (typeof t.content === 'string') {
|
||||||
|
content = t.content
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishEvent(
|
||||||
|
{
|
||||||
|
kind: Kind.ChannelMetadata,
|
||||||
|
tags: [['e', t.channel_create_event_id], ...(t.tags ?? [])],
|
||||||
|
content: content,
|
||||||
|
created_at: t.created_at
|
||||||
|
},
|
||||||
|
privateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const channelMessageEvent = (
|
||||||
|
t: ChannelMessageEventTemplate,
|
||||||
|
privateKey: string
|
||||||
|
): Event<Kind.ChannelMessage> => {
|
||||||
|
const tags = [['e', t.channel_create_event_id, t.relay_url, 'root']]
|
||||||
|
|
||||||
|
if (t.reply_to_channel_message_event_id) {
|
||||||
|
tags.push(['e', t.reply_to_channel_message_event_id, t.relay_url, 'reply'])
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishEvent(
|
||||||
|
{
|
||||||
|
kind: Kind.ChannelMessage,
|
||||||
|
tags: [...tags, ...(t.tags ?? [])],
|
||||||
|
content: t.content,
|
||||||
|
created_at: t.created_at
|
||||||
|
},
|
||||||
|
privateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "e" tag should be the kind 42 event to hide */
|
||||||
|
export const channelHideMessageEvent = (
|
||||||
|
t: ChannelHideMessageEventTemplate,
|
||||||
|
privateKey: string
|
||||||
|
): Event<Kind.ChannelHideMessage> | undefined => {
|
||||||
|
let content: string
|
||||||
|
if (typeof t.content === 'object') {
|
||||||
|
content = JSON.stringify(t.content)
|
||||||
|
} else if (typeof t.content === 'string') {
|
||||||
|
content = t.content
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishEvent(
|
||||||
|
{
|
||||||
|
kind: Kind.ChannelHideMessage,
|
||||||
|
tags: [['e', t.channel_message_event_id], ...(t.tags ?? [])],
|
||||||
|
content: content,
|
||||||
|
created_at: t.created_at
|
||||||
|
},
|
||||||
|
privateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const channelMuteUserEvent = (
|
||||||
|
t: ChannelMuteUserEventTemplate,
|
||||||
|
privateKey: string
|
||||||
|
): Event<Kind.ChannelMuteUser> | undefined => {
|
||||||
|
let content: string
|
||||||
|
if (typeof t.content === 'object') {
|
||||||
|
content = JSON.stringify(t.content)
|
||||||
|
} else if (typeof t.content === 'string') {
|
||||||
|
content = t.content
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishEvent(
|
||||||
|
{
|
||||||
|
kind: Kind.ChannelMuteUser,
|
||||||
|
tags: [['p', t.pubkey_to_mute], ...(t.tags ?? [])],
|
||||||
|
content: content,
|
||||||
|
created_at: t.created_at
|
||||||
|
},
|
||||||
|
privateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
16
nip42.ts
16
nip42.ts
@@ -17,7 +17,9 @@ export const authenticate = async ({
|
|||||||
}: {
|
}: {
|
||||||
challenge: string
|
challenge: string
|
||||||
relay: Relay
|
relay: Relay
|
||||||
sign: <K extends number = number>(e: EventTemplate<K>) => Promise<Event<K>> | Event<K>
|
sign: <K extends number = number>(
|
||||||
|
e: EventTemplate<K>
|
||||||
|
) => Promise<Event<K>> | Event<K>
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const e: EventTemplate = {
|
const e: EventTemplate = {
|
||||||
kind: Kind.ClientAuth,
|
kind: Kind.ClientAuth,
|
||||||
@@ -28,15 +30,5 @@ export const authenticate = async ({
|
|||||||
],
|
],
|
||||||
content: ''
|
content: ''
|
||||||
}
|
}
|
||||||
const pub = relay.auth(await sign(e))
|
return 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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
nip44.test.ts
Normal file
21
nip44.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import crypto from 'node:crypto'
|
||||||
|
import {hexToBytes} from '@noble/hashes/utils'
|
||||||
|
|
||||||
|
import {encrypt, decrypt, getSharedSecret} from './nip44.ts'
|
||||||
|
import {getPublicKey, generatePrivateKey} from './keys.ts'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
globalThis.crypto = crypto
|
||||||
|
|
||||||
|
test('encrypt and decrypt message', async () => {
|
||||||
|
let sk1 = generatePrivateKey()
|
||||||
|
let sk2 = generatePrivateKey()
|
||||||
|
let pk1 = getPublicKey(sk1)
|
||||||
|
let pk2 = getPublicKey(sk2)
|
||||||
|
let sharedKey1 = getSharedSecret(sk1, pk2)
|
||||||
|
let sharedKey2 = getSharedSecret(sk2, pk1)
|
||||||
|
|
||||||
|
expect(decrypt(hexToBytes(sk1), encrypt(hexToBytes(sk1), 'hello'))).toEqual('hello')
|
||||||
|
expect(decrypt(sharedKey2, encrypt(sharedKey1, 'hello'))).toEqual('hello')
|
||||||
|
})
|
||||||
40
nip44.ts
Normal file
40
nip44.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {base64} from '@scure/base'
|
||||||
|
import {randomBytes} from '@noble/hashes/utils'
|
||||||
|
import {secp256k1} from '@noble/curves/secp256k1'
|
||||||
|
import {sha256} from '@noble/hashes/sha256'
|
||||||
|
import {xchacha20} from '@noble/ciphers/chacha'
|
||||||
|
|
||||||
|
import {utf8Decoder, utf8Encoder} from './utils.ts'
|
||||||
|
|
||||||
|
export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array =>
|
||||||
|
sha256(secp256k1.getSharedSecret(privkey, '02' + pubkey).subarray(1, 33))
|
||||||
|
|
||||||
|
export function encrypt(key: Uint8Array, text: string, v = 1) {
|
||||||
|
if (v !== 1) {
|
||||||
|
throw new Error('NIP44: unknown encryption version')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = randomBytes(24)
|
||||||
|
const plaintext = utf8Encoder.encode(text)
|
||||||
|
const ciphertext = xchacha20(key, nonce, plaintext)
|
||||||
|
|
||||||
|
const payload = new Uint8Array(25 + ciphertext.length)
|
||||||
|
payload.set([v], 0)
|
||||||
|
payload.set(nonce, 1)
|
||||||
|
payload.set(ciphertext, 25)
|
||||||
|
|
||||||
|
return base64.encode(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(key: Uint8Array, payload: string) {
|
||||||
|
let data = base64.decode(payload)
|
||||||
|
if (data[0] !== 1) {
|
||||||
|
throw new Error(`NIP44: unknown encryption version: ${data[0]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = data.slice(1, 25)
|
||||||
|
const ciphertext = data.slice(25)
|
||||||
|
const plaintext = xchacha20(key, nonce, ciphertext)
|
||||||
|
|
||||||
|
return utf8Decoder.decode(plaintext)
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import {base64} from '@scure/base'
|
import {getToken, unpackEventFromToken, validateEvent, validateToken} from './nip98.ts'
|
||||||
import {getToken, validateToken} from './nip98.ts'
|
|
||||||
import {Event, Kind, finishEvent} from './event.ts'
|
import {Event, Kind, finishEvent} from './event.ts'
|
||||||
import {utf8Decoder} from './utils.ts'
|
|
||||||
import {generatePrivateKey, getPublicKey} from './keys.ts'
|
import {generatePrivateKey, getPublicKey} from './keys.ts'
|
||||||
|
|
||||||
const sk = generatePrivateKey()
|
const sk = generatePrivateKey()
|
||||||
@@ -12,9 +10,7 @@ describe('getToken', () => {
|
|||||||
finishEvent(e, sk)
|
finishEvent(e, sk)
|
||||||
)
|
)
|
||||||
|
|
||||||
const decodedResult: Event = JSON.parse(
|
const decodedResult: Event = await unpackEventFromToken(result)
|
||||||
utf8Decoder.decode(base64.decode(result))
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(decodedResult.created_at).toBeGreaterThan(0)
|
expect(decodedResult.created_at).toBeGreaterThan(0)
|
||||||
expect(decodedResult.content).toBe('')
|
expect(decodedResult.content).toBe('')
|
||||||
@@ -31,9 +27,7 @@ describe('getToken', () => {
|
|||||||
finishEvent(e, sk)
|
finishEvent(e, sk)
|
||||||
)
|
)
|
||||||
|
|
||||||
const decodedResult: Event = JSON.parse(
|
const decodedResult: Event = await unpackEventFromToken(result)
|
||||||
utf8Decoder.decode(base64.decode(result))
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(decodedResult.created_at).toBeGreaterThan(0)
|
expect(decodedResult.created_at).toBeGreaterThan(0)
|
||||||
expect(decodedResult.content).toBe('')
|
expect(decodedResult.content).toBe('')
|
||||||
@@ -57,9 +51,7 @@ describe('getToken', () => {
|
|||||||
|
|
||||||
expect(result.startsWith(authorizationScheme)).toBe(true)
|
expect(result.startsWith(authorizationScheme)).toBe(true)
|
||||||
|
|
||||||
const decodedResult: Event = JSON.parse(
|
const decodedResult: Event = await unpackEventFromToken(result)
|
||||||
utf8Decoder.decode(base64.decode(result.replace(authorizationScheme, '')))
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(decodedResult.created_at).toBeGreaterThan(0)
|
expect(decodedResult.created_at).toBeGreaterThan(0)
|
||||||
expect(decodedResult.content).toBe('')
|
expect(decodedResult.content).toBe('')
|
||||||
@@ -136,4 +128,43 @@ describe('validateToken', () => {
|
|||||||
const result = validateToken(validToken, 'http://test.com', 'post')
|
const result = validateToken(validToken, 'http://test.com', 'post')
|
||||||
await expect(result).rejects.toThrow(Error)
|
await expect(result).rejects.toThrow(Error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('validateEvent returns true for valid decoded token with authorization scheme', async () => {
|
||||||
|
const validToken = await getToken(
|
||||||
|
'http://test.com',
|
||||||
|
'get',
|
||||||
|
e => finishEvent(e, sk),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||||
|
|
||||||
|
const result = await validateEvent(decodedResult, 'http://test.com', 'get')
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateEvent throws an error for a wrong url', async () => {
|
||||||
|
const validToken = await getToken(
|
||||||
|
'http://test.com',
|
||||||
|
'get',
|
||||||
|
e => finishEvent(e, sk),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||||
|
|
||||||
|
const result = validateEvent(decodedResult, 'http://wrong-test.com', 'get')
|
||||||
|
await expect(result).rejects.toThrow(Error)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateEvent throws an error for a wrong method', async () => {
|
||||||
|
const validToken = await getToken(
|
||||||
|
'http://test.com',
|
||||||
|
'get',
|
||||||
|
e => finishEvent(e, sk),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const decodedResult: Event = await unpackEventFromToken(validToken)
|
||||||
|
|
||||||
|
const result = validateEvent(decodedResult, 'http://test.com', 'post')
|
||||||
|
await expect(result).rejects.toThrow(Error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
29
nip98.ts
29
nip98.ts
@@ -8,11 +8,6 @@ import {
|
|||||||
} from './event'
|
} from './event'
|
||||||
import {utf8Decoder, utf8Encoder} from './utils'
|
import {utf8Decoder, utf8Encoder} from './utils'
|
||||||
|
|
||||||
enum HttpMethod {
|
|
||||||
Get = 'get',
|
|
||||||
Post = 'post'
|
|
||||||
}
|
|
||||||
|
|
||||||
const _authorizationScheme = 'Nostr '
|
const _authorizationScheme = 'Nostr '
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,11 +15,11 @@ const _authorizationScheme = 'Nostr '
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const sign = window.nostr.signEvent
|
* const sign = window.nostr.signEvent
|
||||||
* await getToken('https://example.com/login', 'post', sign, true)
|
* await nip98.getToken('https://example.com/login', 'post', (e) => sign(e), true)
|
||||||
*/
|
*/
|
||||||
export async function getToken(
|
export async function getToken(
|
||||||
loginUrl: string,
|
loginUrl: string,
|
||||||
httpMethod: HttpMethod | string,
|
httpMethod: string,
|
||||||
sign: <K extends number = number>(
|
sign: <K extends number = number>(
|
||||||
e: EventTemplate<K>
|
e: EventTemplate<K>
|
||||||
) => Promise<Event<K>> | Event<K>,
|
) => Promise<Event<K>> | Event<K>,
|
||||||
@@ -32,8 +27,6 @@ export async function getToken(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!loginUrl || !httpMethod)
|
if (!loginUrl || !httpMethod)
|
||||||
throw new Error('Missing loginUrl or httpMethod')
|
throw new Error('Missing loginUrl or httpMethod')
|
||||||
if (httpMethod !== HttpMethod.Get && httpMethod !== HttpMethod.Post)
|
|
||||||
throw new Error('Unknown httpMethod')
|
|
||||||
|
|
||||||
const event = getBlankEvent(Kind.HttpAuth)
|
const event = getBlankEvent(Kind.HttpAuth)
|
||||||
|
|
||||||
@@ -58,13 +51,20 @@ export async function getToken(
|
|||||||
* Validate token for NIP-98 flow.
|
* Validate token for NIP-98 flow.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await validateToken('Nostr base64token', 'https://example.com/login', 'post')
|
* await nip98.validateToken('Nostr base64token', 'https://example.com/login', 'post')
|
||||||
*/
|
*/
|
||||||
export async function validateToken(
|
export async function validateToken(
|
||||||
token: string,
|
token: string,
|
||||||
url: string,
|
url: string,
|
||||||
method: string
|
method: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const event = await unpackEventFromToken(token).catch((error) => { throw(error) })
|
||||||
|
const valid = await validateEvent(event, url, method).catch((error) => { throw(error) })
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unpackEventFromToken(token: string): Promise<Event> {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('Missing token')
|
throw new Error('Missing token')
|
||||||
}
|
}
|
||||||
@@ -76,6 +76,15 @@ export async function validateToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event = JSON.parse(eventB64) as Event
|
const event = JSON.parse(eventB64) as Event
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateEvent(
|
||||||
|
event: Event,
|
||||||
|
url: string,
|
||||||
|
method: string
|
||||||
|
): Promise<boolean> {
|
||||||
if (!event) {
|
if (!event) {
|
||||||
throw new Error('Invalid nostr event')
|
throw new Error('Invalid nostr event')
|
||||||
}
|
}
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "1.13.0",
|
"version": "1.14.2",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -19,11 +19,12 @@
|
|||||||
},
|
},
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/curves": "1.0.0",
|
"@noble/curves": "1.1.0",
|
||||||
"@noble/hashes": "1.3.0",
|
"@noble/hashes": "1.3.1",
|
||||||
|
"@noble/ciphers": "^0.2.0",
|
||||||
"@scure/base": "1.1.1",
|
"@scure/base": "1.1.1",
|
||||||
"@scure/bip32": "1.3.0",
|
"@scure/bip32": "1.3.1",
|
||||||
"@scure/bip39": "1.2.0"
|
"@scure/bip39": "1.2.1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"decentralization",
|
"decentralization",
|
||||||
|
|||||||
175
pool.ts
175
pool.ts
@@ -1,27 +1,44 @@
|
|||||||
import {
|
import {
|
||||||
relayInit,
|
relayInit,
|
||||||
type Pub,
|
|
||||||
type Relay,
|
type Relay,
|
||||||
type Sub,
|
type Sub,
|
||||||
type SubscriptionOptions,
|
type SubscriptionOptions
|
||||||
} from './relay.ts'
|
} from './relay.ts'
|
||||||
import {normalizeURL} from './utils.ts'
|
import {normalizeURL} from './utils.ts'
|
||||||
|
|
||||||
import type {Event} from './event.ts'
|
import type {Event} from './event.ts'
|
||||||
import type {Filter} from './filter.ts'
|
import {matchFilters, type Filter} from './filter.ts'
|
||||||
|
|
||||||
|
type BatchedRequest = {
|
||||||
|
filters: Filter<any>[]
|
||||||
|
relays: string[]
|
||||||
|
resolve: (events: Event<any>[]) => void
|
||||||
|
events: Event<any>[]
|
||||||
|
}
|
||||||
|
|
||||||
export class SimplePool {
|
export class SimplePool {
|
||||||
private _conn: {[url: string]: Relay}
|
private _conn: {[url: string]: Relay}
|
||||||
private _seenOn: {[id: string]: Set<string>} = {} // a map of all events we've seen in each relay
|
private _seenOn: {[id: string]: Set<string>} = {} // a map of all events we've seen in each relay
|
||||||
|
private batchedByKey: {[batchKey: string]: BatchedRequest[]} = {}
|
||||||
|
|
||||||
private eoseSubTimeout: number
|
private eoseSubTimeout: number
|
||||||
private getTimeout: number
|
private getTimeout: number
|
||||||
private seenOnEnabled: boolean = true
|
private seenOnEnabled: boolean = true
|
||||||
|
private batchInterval: number = 100
|
||||||
|
|
||||||
constructor(options: {eoseSubTimeout?: number; getTimeout?: number; seenOnEnabled?: boolean} = {}) {
|
constructor(
|
||||||
|
options: {
|
||||||
|
eoseSubTimeout?: number
|
||||||
|
getTimeout?: number
|
||||||
|
seenOnEnabled?: boolean
|
||||||
|
batchInterval?: number
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
this._conn = {}
|
this._conn = {}
|
||||||
this.eoseSubTimeout = options.eoseSubTimeout || 3400
|
this.eoseSubTimeout = options.eoseSubTimeout || 3400
|
||||||
this.getTimeout = options.getTimeout || 3400
|
this.getTimeout = options.getTimeout || 3400
|
||||||
this.seenOnEnabled = options.seenOnEnabled !== false
|
this.seenOnEnabled = options.seenOnEnabled !== false
|
||||||
|
this.batchInterval = options.batchInterval || 100
|
||||||
}
|
}
|
||||||
|
|
||||||
close(relays: string[]): void {
|
close(relays: string[]): void {
|
||||||
@@ -46,7 +63,11 @@ export class SimplePool {
|
|||||||
return relay
|
return relay
|
||||||
}
|
}
|
||||||
|
|
||||||
sub<K extends number = number>(relays: string[], filters: Filter<K>[], opts?: SubscriptionOptions): Sub<K> {
|
sub<K extends number = number>(
|
||||||
|
relays: string[],
|
||||||
|
filters: Filter<K>[],
|
||||||
|
opts?: SubscriptionOptions
|
||||||
|
): Sub<K> {
|
||||||
let _knownIds: Set<string> = new Set()
|
let _knownIds: Set<string> = new Set()
|
||||||
let modifiedOpts = {...(opts || {})}
|
let modifiedOpts = {...(opts || {})}
|
||||||
modifiedOpts.alreadyHaveEvent = (id, url) => {
|
modifiedOpts.alreadyHaveEvent = (id, url) => {
|
||||||
@@ -72,34 +93,36 @@ export class SimplePool {
|
|||||||
for (let cb of eoseListeners.values()) cb()
|
for (let cb of eoseListeners.values()) cb()
|
||||||
}, this.eoseSubTimeout)
|
}, this.eoseSubTimeout)
|
||||||
|
|
||||||
relays.forEach(async relay => {
|
relays
|
||||||
let r
|
.filter((r, i, a) => a.indexOf(r) == i)
|
||||||
try {
|
.forEach(async relay => {
|
||||||
r = await this.ensureRelay(relay)
|
let r
|
||||||
} catch (err) {
|
try {
|
||||||
handleEose()
|
r = await this.ensureRelay(relay)
|
||||||
return
|
} catch (err) {
|
||||||
}
|
handleEose()
|
||||||
if (!r) return
|
return
|
||||||
let s = r.sub(filters, modifiedOpts)
|
|
||||||
s.on('event', (event) => {
|
|
||||||
_knownIds.add(event.id as string)
|
|
||||||
for (let cb of eventListeners.values()) cb(event)
|
|
||||||
})
|
|
||||||
s.on('eose', () => {
|
|
||||||
if (eoseSent) return
|
|
||||||
handleEose()
|
|
||||||
})
|
|
||||||
subs.push(s)
|
|
||||||
|
|
||||||
function handleEose() {
|
|
||||||
eosesMissing--
|
|
||||||
if (eosesMissing === 0) {
|
|
||||||
clearTimeout(eoseTimeout)
|
|
||||||
for (let cb of eoseListeners.values()) cb()
|
|
||||||
}
|
}
|
||||||
}
|
if (!r) return
|
||||||
})
|
let s = r.sub(filters, modifiedOpts)
|
||||||
|
s.on('event', event => {
|
||||||
|
_knownIds.add(event.id as string)
|
||||||
|
for (let cb of eventListeners.values()) cb(event)
|
||||||
|
})
|
||||||
|
s.on('eose', () => {
|
||||||
|
if (eoseSent) return
|
||||||
|
handleEose()
|
||||||
|
})
|
||||||
|
subs.push(s)
|
||||||
|
|
||||||
|
function handleEose() {
|
||||||
|
eosesMissing--
|
||||||
|
if (eosesMissing === 0) {
|
||||||
|
clearTimeout(eoseTimeout)
|
||||||
|
for (let cb of eoseListeners.values()) cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let greaterSub: Sub = {
|
let greaterSub: Sub = {
|
||||||
sub(filters, opts) {
|
sub(filters, opts) {
|
||||||
@@ -138,7 +161,7 @@ export class SimplePool {
|
|||||||
sub.unsub()
|
sub.unsub()
|
||||||
resolve(null)
|
resolve(null)
|
||||||
}, this.getTimeout)
|
}, this.getTimeout)
|
||||||
sub.on('event', (event) => {
|
sub.on('event', event => {
|
||||||
resolve(event)
|
resolve(event)
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
sub.unsub()
|
sub.unsub()
|
||||||
@@ -155,7 +178,7 @@ export class SimplePool {
|
|||||||
let events: Event<K>[] = []
|
let events: Event<K>[] = []
|
||||||
let sub = this.sub(relays, filters, opts)
|
let sub = this.sub(relays, filters, opts)
|
||||||
|
|
||||||
sub.on('event', (event) => {
|
sub.on('event', event => {
|
||||||
events.push(event)
|
events.push(event)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -167,39 +190,63 @@ export class SimplePool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
publish(relays: string[], event: Event<number>): Pub {
|
batchedList<K extends number = number>(
|
||||||
const pubPromises: Promise<Pub>[] = relays.map(async relay => {
|
batchKey: string,
|
||||||
let r
|
relays: string[],
|
||||||
try {
|
filters: Filter<K>[]
|
||||||
r = await this.ensureRelay(relay)
|
): Promise<Event<K>[]> {
|
||||||
return r.publish(event)
|
return new Promise(resolve => {
|
||||||
} catch (_) {
|
if (!this.batchedByKey[batchKey]) {
|
||||||
return {on() {}, off() {}}
|
this.batchedByKey[batchKey] = [
|
||||||
|
{
|
||||||
|
filters,
|
||||||
|
relays,
|
||||||
|
resolve,
|
||||||
|
events: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
Object.keys(this.batchedByKey).forEach(async batchKey => {
|
||||||
|
const batchedRequests = this.batchedByKey[batchKey]
|
||||||
|
|
||||||
|
const filters = [] as Filter[]
|
||||||
|
const relays = [] as string[]
|
||||||
|
batchedRequests.forEach(br => {
|
||||||
|
filters.push(...br.filters)
|
||||||
|
relays.push(...br.relays)
|
||||||
|
})
|
||||||
|
|
||||||
|
const sub = this.sub(relays, filters)
|
||||||
|
sub.on('event', event => {
|
||||||
|
batchedRequests.forEach(
|
||||||
|
br => matchFilters(br.filters, event) && br.events.push(event)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
sub.on('eose', () => {
|
||||||
|
sub.unsub()
|
||||||
|
batchedRequests.forEach(br => br.resolve(br.events))
|
||||||
|
})
|
||||||
|
|
||||||
|
delete this.batchedByKey[batchKey]
|
||||||
|
})
|
||||||
|
}, this.batchInterval)
|
||||||
|
} else {
|
||||||
|
this.batchedByKey[batchKey].push({
|
||||||
|
filters,
|
||||||
|
relays,
|
||||||
|
resolve,
|
||||||
|
events: []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const callbackMap = new Map()
|
publish(relays: string[], event: Event<number>): Promise<void>[] {
|
||||||
|
return relays.map(async relay => {
|
||||||
return {
|
let r = await this.ensureRelay(relay)
|
||||||
on(type, cb) {
|
return r.publish(event)
|
||||||
relays.forEach(async (relay, i) => {
|
})
|
||||||
let pub = await pubPromises[i]
|
|
||||||
let callback = () => cb(relay)
|
|
||||||
callbackMap.set(cb, callback)
|
|
||||||
pub.on(type, callback)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
off(type, cb) {
|
|
||||||
relays.forEach(async (_, i) => {
|
|
||||||
let callback = callbackMap.get(cb)
|
|
||||||
if (callback) {
|
|
||||||
let pub = await pubPromises[i]
|
|
||||||
pub.off(type, callback)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
seenOn(id: string): string[] {
|
seenOn(id: string): string[] {
|
||||||
|
|||||||
86
relay.ts
86
relay.ts
@@ -3,7 +3,7 @@
|
|||||||
import {verifySignature, validateEvent, type Event} from './event.ts'
|
import {verifySignature, validateEvent, type Event} from './event.ts'
|
||||||
import {matchFilters, type Filter} from './filter.ts'
|
import {matchFilters, type Filter} from './filter.ts'
|
||||||
import {getHex64, getSubscriptionId} from './fakejson.ts'
|
import {getHex64, getSubscriptionId} from './fakejson.ts'
|
||||||
import { MessageQueue } from './utils.ts'
|
import {MessageQueue} from './utils.ts'
|
||||||
|
|
||||||
type RelayEvent = {
|
type RelayEvent = {
|
||||||
connect: () => void | Promise<void>
|
connect: () => void | Promise<void>
|
||||||
@@ -25,15 +25,24 @@ export type Relay = {
|
|||||||
status: number
|
status: number
|
||||||
connect: () => Promise<void>
|
connect: () => Promise<void>
|
||||||
close: () => void
|
close: () => void
|
||||||
sub: <K extends number = number>(filters: Filter<K>[], opts?: SubscriptionOptions) => Sub<K>
|
sub: <K extends number = number>(
|
||||||
list: <K extends number = number>(filters: Filter<K>[], opts?: SubscriptionOptions) => Promise<Event<K>[]>
|
filters: Filter<K>[],
|
||||||
get: <K extends number = number>(filter: Filter<K>, opts?: SubscriptionOptions) => Promise<Event<K> | null>
|
opts?: SubscriptionOptions
|
||||||
|
) => Sub<K>
|
||||||
|
list: <K extends number = number>(
|
||||||
|
filters: Filter<K>[],
|
||||||
|
opts?: SubscriptionOptions
|
||||||
|
) => Promise<Event<K>[]>
|
||||||
|
get: <K extends number = number>(
|
||||||
|
filter: Filter<K>,
|
||||||
|
opts?: SubscriptionOptions
|
||||||
|
) => Promise<Event<K> | null>
|
||||||
count: (
|
count: (
|
||||||
filters: Filter[],
|
filters: Filter[],
|
||||||
opts?: SubscriptionOptions
|
opts?: SubscriptionOptions
|
||||||
) => Promise<CountPayload | null>
|
) => Promise<CountPayload | null>
|
||||||
publish: (event: Event<number>) => Pub
|
publish: (event: Event<number>) => Promise<void>
|
||||||
auth: (event: Event<number>) => Pub
|
auth: (event: Event<number>) => Promise<void>
|
||||||
off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
|
off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
|
||||||
event: T,
|
event: T,
|
||||||
listener: U
|
listener: U
|
||||||
@@ -43,12 +52,11 @@ export type Relay = {
|
|||||||
listener: U
|
listener: U
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
export type Pub = {
|
|
||||||
on: (type: 'ok' | 'failed', cb: any) => void
|
|
||||||
off: (type: 'ok' | 'failed', cb: any) => void
|
|
||||||
}
|
|
||||||
export type Sub<K extends number = number> = {
|
export type Sub<K extends number = number> = {
|
||||||
sub: <K extends number = number>(filters: Filter<K>[], opts: SubscriptionOptions) => Sub<K>
|
sub: <K extends number = number>(
|
||||||
|
filters: Filter<K>[],
|
||||||
|
opts: SubscriptionOptions
|
||||||
|
) => Sub<K>
|
||||||
unsub: () => void
|
unsub: () => void
|
||||||
on: <T extends keyof SubEvent<K>, U extends SubEvent<K>[T]>(
|
on: <T extends keyof SubEvent<K>, U extends SubEvent<K>[T]>(
|
||||||
event: T,
|
event: T,
|
||||||
@@ -93,9 +101,8 @@ export function relayInit(
|
|||||||
} = {}
|
} = {}
|
||||||
var pubListeners: {
|
var pubListeners: {
|
||||||
[eventid: string]: {
|
[eventid: string]: {
|
||||||
ok: Array<() => void>
|
resolve: (_: unknown) => void
|
||||||
seen: Array<() => void>
|
reject: (err: Error) => void
|
||||||
failed: Array<(reason: string) => void>
|
|
||||||
}
|
}
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
@@ -196,10 +203,9 @@ export function relayInit(
|
|||||||
let ok: boolean = data[2]
|
let ok: boolean = data[2]
|
||||||
let reason: string = data[3] || ''
|
let reason: string = data[3] || ''
|
||||||
if (id in pubListeners) {
|
if (id in pubListeners) {
|
||||||
if (ok) pubListeners[id].ok.forEach(cb => cb())
|
let {resolve, reject} = pubListeners[id]
|
||||||
else pubListeners[id].failed.forEach(cb => cb(reason))
|
if (ok) resolve(null)
|
||||||
pubListeners[id].ok = [] // 'ok' only happens once per pub, so stop listeners here
|
else reject(new Error(reason))
|
||||||
pubListeners[id].failed = []
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -294,26 +300,16 @@ export function relayInit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _publishEvent(event: Event<number>, type: string) {
|
function _publishEvent(event: Event<number>, type: string) {
|
||||||
if (!event.id) throw new Error(`event ${event} has no id`)
|
return new Promise((resolve, reject) => {
|
||||||
let id = event.id
|
if (!event.id) {
|
||||||
|
reject(new Error(`event ${event} has no id`))
|
||||||
trySend([type, event])
|
return
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let id = event.id
|
||||||
|
trySend([type, event])
|
||||||
|
pubListeners[id] = {resolve, reject}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -349,7 +345,7 @@ export function relayInit(
|
|||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
resolve(events)
|
resolve(events)
|
||||||
})
|
})
|
||||||
s.on('event', (event) => {
|
s.on('event', event => {
|
||||||
events.push(event)
|
events.push(event)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -360,7 +356,7 @@ export function relayInit(
|
|||||||
s.unsub()
|
s.unsub()
|
||||||
resolve(null)
|
resolve(null)
|
||||||
}, getTimeout)
|
}, getTimeout)
|
||||||
s.on('event', (event) => {
|
s.on('event', event => {
|
||||||
s.unsub()
|
s.unsub()
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
resolve(event)
|
resolve(event)
|
||||||
@@ -379,19 +375,19 @@ export function relayInit(
|
|||||||
resolve(event)
|
resolve(event)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
publish(event): Pub {
|
async publish(event): Promise<void> {
|
||||||
return _publishEvent(event, 'EVENT')
|
await _publishEvent(event, 'EVENT')
|
||||||
},
|
},
|
||||||
auth(event): Pub {
|
async auth(event): Promise<void> {
|
||||||
return _publishEvent(event, 'AUTH')
|
await _publishEvent(event, 'AUTH')
|
||||||
},
|
},
|
||||||
connect,
|
connect,
|
||||||
close(): void {
|
close(): void {
|
||||||
listeners = newListeners()
|
listeners = newListeners()
|
||||||
subListeners = {}
|
subListeners = {}
|
||||||
pubListeners = {}
|
pubListeners = {}
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
ws?.close()
|
ws.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get status() {
|
get status() {
|
||||||
|
|||||||
Reference in New Issue
Block a user