mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 00:28:51 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54e352d8e2 | ||
|
|
235a1c50cb | ||
|
|
dfc2107569 | ||
|
|
986b9d0cce | ||
|
|
753ff323ea | ||
|
|
f8c3e20f3d | ||
|
|
87a91c2daf | ||
|
|
4f1dc9ef1c | ||
|
|
faa1a9d556 | ||
|
|
97d838f254 | ||
|
|
260400b24d |
33
README.md
33
README.md
@@ -104,8 +104,11 @@ relay.close()
|
|||||||
To use this on Node.js you first must install `ws` and call something like this:
|
To use this on Node.js you first must install `ws` and call something like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { useWebSocketImplementation } from 'nostr-tools/relay'
|
import { useWebSocketImplementation } from 'nostr-tools/pool'
|
||||||
useWebSocketImplementation(require('ws'))
|
// or import { useWebSocketImplementation } from 'nostr-tools/relay' if you're using the Relay directly
|
||||||
|
|
||||||
|
import WebSocket from 'ws'
|
||||||
|
useWebSocketImplementation(WebSocket)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Interacting with multiple relays
|
### Interacting with multiple relays
|
||||||
@@ -194,6 +197,32 @@ declare global {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Generating NIP-06 keys
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
privateKeyFromSeedWords,
|
||||||
|
accountFromSeedWords,
|
||||||
|
extendedKeysFromSeedWords,
|
||||||
|
accountFromExtendedKey
|
||||||
|
} from 'nostr-tools/nip06'
|
||||||
|
|
||||||
|
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
|
||||||
|
const passphrase = '123' // optional
|
||||||
|
const accountIndex = 0
|
||||||
|
const sk0 = privateKeyFromSeedWords(mnemonic, passphrase, accountIndex)
|
||||||
|
|
||||||
|
const { privateKey: sk1, publicKey: pk1 } = accountFromSeedWords(mnemonic, passphrase, accountIndex)
|
||||||
|
|
||||||
|
const extendedAccountIndex = 0
|
||||||
|
|
||||||
|
const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(mnemonic, passphrase, extendedAccountIndex)
|
||||||
|
|
||||||
|
const { privateKey: sk2, publicKey: pk2 } = accountFromExtendedKey(privateExtendedKey)
|
||||||
|
|
||||||
|
const { publicKey: pk3 } = accountFromExtendedKey(publicExtendedKey)
|
||||||
|
```
|
||||||
|
|
||||||
### Encoding and decoding NIP-19 codes
|
### Encoding and decoding NIP-19 codes
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { AbstractRelay as AbstractRelay, SubscriptionParams, Subscription } from './abstract-relay.ts'
|
/* global WebSocket */
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbstractRelay as AbstractRelay,
|
||||||
|
SubscriptionParams,
|
||||||
|
Subscription,
|
||||||
|
type AbstractRelayConstructorOptions,
|
||||||
|
} from './abstract-relay.ts'
|
||||||
import { normalizeURL } from './utils.ts'
|
import { normalizeURL } from './utils.ts'
|
||||||
|
|
||||||
import type { Event, Nostr } from './core.ts'
|
import type { Event, Nostr } from './core.ts'
|
||||||
@@ -7,6 +14,8 @@ import { alwaysTrue } from './helpers.ts'
|
|||||||
|
|
||||||
export type SubCloser = { close: () => void }
|
export type SubCloser = { close: () => void }
|
||||||
|
|
||||||
|
export type AbstractPoolConstructorOptions = AbstractRelayConstructorOptions & {}
|
||||||
|
|
||||||
export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose' | 'id'> & {
|
export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose' | 'id'> & {
|
||||||
maxWait?: number
|
maxWait?: number
|
||||||
onclose?: (reasons: string[]) => void
|
onclose?: (reasons: string[]) => void
|
||||||
@@ -14,15 +23,18 @@ export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose' | 'id'> & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractSimplePool {
|
export class AbstractSimplePool {
|
||||||
private relays = new Map<string, AbstractRelay>()
|
protected relays = new Map<string, AbstractRelay>()
|
||||||
public seenOn: Map<string, Set<AbstractRelay>> = new Map()
|
public seenOn: Map<string, Set<AbstractRelay>> = new Map()
|
||||||
public trackRelays: boolean = false
|
public trackRelays: boolean = false
|
||||||
|
|
||||||
public verifyEvent: Nostr['verifyEvent']
|
public verifyEvent: Nostr['verifyEvent']
|
||||||
public trustedRelayURLs: Set<string> = new Set()
|
public trustedRelayURLs: Set<string> = new Set()
|
||||||
|
|
||||||
constructor(opts: { verifyEvent: Nostr['verifyEvent'] }) {
|
private _WebSocket?: typeof WebSocket
|
||||||
|
|
||||||
|
constructor(opts: AbstractPoolConstructorOptions) {
|
||||||
this.verifyEvent = opts.verifyEvent
|
this.verifyEvent = opts.verifyEvent
|
||||||
|
this._WebSocket = opts.websocketImplementation
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> {
|
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> {
|
||||||
@@ -32,6 +44,7 @@ export class AbstractSimplePool {
|
|||||||
if (!relay) {
|
if (!relay) {
|
||||||
relay = new AbstractRelay(url, {
|
relay = new AbstractRelay(url, {
|
||||||
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
||||||
|
websocketImplementation: this._WebSocket,
|
||||||
})
|
})
|
||||||
if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout
|
if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout
|
||||||
this.relays.set(url, relay)
|
this.relays.set(url, relay)
|
||||||
|
|||||||
@@ -7,14 +7,9 @@ import { Queue, normalizeURL } from './utils.ts'
|
|||||||
import { makeAuthEvent } from './nip42.ts'
|
import { makeAuthEvent } from './nip42.ts'
|
||||||
import { yieldThread } from './helpers.ts'
|
import { yieldThread } from './helpers.ts'
|
||||||
|
|
||||||
var _WebSocket: typeof WebSocket
|
export type AbstractRelayConstructorOptions = {
|
||||||
|
verifyEvent: Nostr['verifyEvent']
|
||||||
try {
|
websocketImplementation?: typeof WebSocket
|
||||||
_WebSocket = WebSocket
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
export function useWebSocketImplementation(websocketImplementation: any) {
|
|
||||||
_WebSocket = websocketImplementation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractRelay {
|
export class AbstractRelay {
|
||||||
@@ -42,12 +37,15 @@ export class AbstractRelay {
|
|||||||
private serial: number = 0
|
private serial: number = 0
|
||||||
private verifyEvent: Nostr['verifyEvent']
|
private verifyEvent: Nostr['verifyEvent']
|
||||||
|
|
||||||
constructor(url: string, opts: { verifyEvent: Nostr['verifyEvent'] }) {
|
private _WebSocket: typeof WebSocket
|
||||||
|
|
||||||
|
constructor(url: string, opts: AbstractRelayConstructorOptions) {
|
||||||
this.url = normalizeURL(url)
|
this.url = normalizeURL(url)
|
||||||
this.verifyEvent = opts.verifyEvent
|
this.verifyEvent = opts.verifyEvent
|
||||||
|
this._WebSocket = opts.websocketImplementation || WebSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect(url: string, opts: { verifyEvent: Nostr['verifyEvent'] }): Promise<AbstractRelay> {
|
static async connect(url: string, opts: AbstractRelayConstructorOptions): Promise<AbstractRelay> {
|
||||||
const relay = new AbstractRelay(url, opts)
|
const relay = new AbstractRelay(url, opts)
|
||||||
await relay.connect()
|
await relay.connect()
|
||||||
return relay
|
return relay
|
||||||
@@ -87,7 +85,7 @@ export class AbstractRelay {
|
|||||||
}, this.connectionTimeout)
|
}, this.connectionTimeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.ws = new _WebSocket(this.url)
|
this.ws = new this._WebSocket(this.url)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
return
|
return
|
||||||
@@ -100,7 +98,7 @@ export class AbstractRelay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onerror = ev => {
|
this.ws.onerror = ev => {
|
||||||
reject((ev as any).message)
|
reject((ev as any).message || 'websocket error')
|
||||||
if (this._connected) {
|
if (this._connected) {
|
||||||
this._connected = false
|
this._connected = false
|
||||||
this.connectionPromise = undefined
|
this.connectionPromise = undefined
|
||||||
|
|||||||
@@ -222,5 +222,9 @@ describe('Filter', () => {
|
|||||||
test('should return Infinity for empty filters', () => {
|
test('should return Infinity for empty filters', () => {
|
||||||
expect(getFilterLimit({})).toEqual(Infinity)
|
expect(getFilterLimit({})).toEqual(Infinity)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('empty tags return 0', () => {
|
||||||
|
expect(getFilterLimit({ '#p': [] })).toEqual(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ export function getFilterLimit(filter: Filter): number {
|
|||||||
if (filter.kinds && !filter.kinds.length) return 0
|
if (filter.kinds && !filter.kinds.length) return 0
|
||||||
if (filter.authors && !filter.authors.length) return 0
|
if (filter.authors && !filter.authors.length) return 0
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
|
if (key[0] === '#' && Array.isArray(value) && !value.length) return 0
|
||||||
|
}
|
||||||
|
|
||||||
return Math.min(
|
return Math.min(
|
||||||
Math.max(0, filter.limit ?? Infinity),
|
Math.max(0, filter.limit ?? Infinity),
|
||||||
filter.ids?.length ?? Infinity,
|
filter.ids?.length ?? Infinity,
|
||||||
|
|||||||
4
index.ts
4
index.ts
@@ -1,7 +1,7 @@
|
|||||||
export * from './pure.ts'
|
export * from './pure.ts'
|
||||||
export * from './relay.ts'
|
export { Relay } from './relay.ts'
|
||||||
export * from './filter.ts'
|
export * from './filter.ts'
|
||||||
export * from './pool.ts'
|
export { SimplePool } from './pool.ts'
|
||||||
export * from './references.ts'
|
export * from './references.ts'
|
||||||
|
|
||||||
export * as nip04 from './nip04.ts'
|
export * as nip04 from './nip04.ts'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
privateKeyFromSeedWords,
|
privateKeyFromSeedWords,
|
||||||
accountFromSeedWords,
|
accountFromSeedWords,
|
||||||
extendedKeysFromSeedWords,
|
extendedKeysFromSeedWords,
|
||||||
accountFromExtendedKey
|
accountFromExtendedKey,
|
||||||
} from './nip06.ts'
|
} from './nip06.ts'
|
||||||
|
|
||||||
test('generate private key from a mnemonic', async () => {
|
test('generate private key from a mnemonic', async () => {
|
||||||
@@ -44,14 +44,23 @@ test('generate extended keys from mnemonic', () => {
|
|||||||
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
||||||
const passphrase = ''
|
const passphrase = ''
|
||||||
const extendedAccountIndex = 0
|
const extendedAccountIndex = 0
|
||||||
const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(mnemonic, passphrase, extendedAccountIndex)
|
const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(
|
||||||
|
mnemonic,
|
||||||
|
passphrase,
|
||||||
|
extendedAccountIndex,
|
||||||
|
)
|
||||||
|
|
||||||
expect(privateExtendedKey).toBe('xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH')
|
expect(privateExtendedKey).toBe(
|
||||||
expect(publicExtendedKey).toBe('xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN')
|
'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH',
|
||||||
|
)
|
||||||
|
expect(publicExtendedKey).toBe(
|
||||||
|
'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('generate account from extended private key', () => {
|
test('generate account from extended private key', () => {
|
||||||
const xprv = 'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH'
|
const xprv =
|
||||||
|
'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH'
|
||||||
const { privateKey, publicKey } = accountFromExtendedKey(xprv)
|
const { privateKey, publicKey } = accountFromExtendedKey(xprv)
|
||||||
|
|
||||||
expect(privateKey).toBe('5f29af3b9676180290e77a4efad265c4c2ff28a5302461f73597fda26bb25731')
|
expect(privateKey).toBe('5f29af3b9676180290e77a4efad265c4c2ff28a5302461f73597fda26bb25731')
|
||||||
@@ -59,7 +68,8 @@ test('generate account from extended private key', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('generate account from extended public key', () => {
|
test('generate account from extended public key', () => {
|
||||||
const xpub = 'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN'
|
const xpub =
|
||||||
|
'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN'
|
||||||
const { publicKey } = accountFromExtendedKey(xpub)
|
const { publicKey } = accountFromExtendedKey(xpub)
|
||||||
|
|
||||||
expect(publicKey).toBe('e8bcf3823669444d0b49ad45d65088635d9fd8500a75b5f20b59abefa56a144f')
|
expect(publicKey).toBe('e8bcf3823669444d0b49ad45d65088635d9fd8500a75b5f20b59abefa56a144f')
|
||||||
|
|||||||
25
nip06.ts
25
nip06.ts
@@ -12,9 +12,13 @@ export function privateKeyFromSeedWords(mnemonic: string, passphrase?: string, a
|
|||||||
return bytesToHex(privateKey)
|
return bytesToHex(privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function accountFromSeedWords(mnemonic: string, passphrase?: string, accountIndex = 0): {
|
export function accountFromSeedWords(
|
||||||
privateKey: string,
|
mnemonic: string,
|
||||||
publicKey: string,
|
passphrase?: string,
|
||||||
|
accountIndex = 0,
|
||||||
|
): {
|
||||||
|
privateKey: string
|
||||||
|
publicKey: string
|
||||||
} {
|
} {
|
||||||
const root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
const root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
||||||
const seed = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`)
|
const seed = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`)
|
||||||
@@ -26,8 +30,12 @@ export function accountFromSeedWords(mnemonic: string, passphrase?: string, acco
|
|||||||
return { privateKey, publicKey }
|
return { privateKey, publicKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extendedKeysFromSeedWords(mnemonic: string, passphrase?: string, extendedAccountIndex = 0): {
|
export function extendedKeysFromSeedWords(
|
||||||
privateExtendedKey: string,
|
mnemonic: string,
|
||||||
|
passphrase?: string,
|
||||||
|
extendedAccountIndex = 0,
|
||||||
|
): {
|
||||||
|
privateExtendedKey: string
|
||||||
publicExtendedKey: string
|
publicExtendedKey: string
|
||||||
} {
|
} {
|
||||||
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
||||||
@@ -38,8 +46,11 @@ export function extendedKeysFromSeedWords(mnemonic: string, passphrase?: string,
|
|||||||
return { privateExtendedKey, publicExtendedKey }
|
return { privateExtendedKey, publicExtendedKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function accountFromExtendedKey(base58key: string, accountIndex = 0): {
|
export function accountFromExtendedKey(
|
||||||
privateKey?: string,
|
base58key: string,
|
||||||
|
accountIndex = 0,
|
||||||
|
): {
|
||||||
|
privateKey?: string
|
||||||
publicKey: string
|
publicKey: string
|
||||||
} {
|
} {
|
||||||
let extendedKey = HDKey.fromExtendedKey(base58key)
|
let extendedKey = HDKey.fromExtendedKey(base58key)
|
||||||
|
|||||||
2
nip07.ts
2
nip07.ts
@@ -7,7 +7,7 @@ export interface WindowNostr {
|
|||||||
getRelays(): Promise<RelayRecord>
|
getRelays(): Promise<RelayRecord>
|
||||||
nip04?: {
|
nip04?: {
|
||||||
encrypt(pubkey: string, plaintext: string): Promise<string>
|
encrypt(pubkey: string, plaintext: string): Promise<string>
|
||||||
ecrypt(pubkey: string, ciphertext: string): Promise<string>
|
decrypt(pubkey: string, ciphertext: string): Promise<string>
|
||||||
}
|
}
|
||||||
nip44?: {
|
nip44?: {
|
||||||
encrypt(pubkey: string, plaintext: string): Promise<string>
|
encrypt(pubkey: string, plaintext: string): Promise<string>
|
||||||
|
|||||||
12
nip11.ts
12
nip11.ts
@@ -68,7 +68,7 @@ export interface BasicRelayInformation {
|
|||||||
* from `[` to `]` and is after UTF-8 serialization (so some
|
* from `[` to `]` and is after UTF-8 serialization (so some
|
||||||
* unicode characters will cost 2-3 bytes). It is equal to
|
* unicode characters will cost 2-3 bytes). It is equal to
|
||||||
* the maximum size of the WebSocket message frame.
|
* the maximum size of the WebSocket message frame.
|
||||||
* @param max_subscription total number of subscriptions
|
* @param max_subscriptions total number of subscriptions
|
||||||
* that may be active on a single websocket connection to
|
* that may be active on a single websocket connection to
|
||||||
* this relay. It's possible that authenticated clients with
|
* this relay. It's possible that authenticated clients with
|
||||||
* a (paid) relationship to the relay may have higher limits.
|
* a (paid) relationship to the relay may have higher limits.
|
||||||
@@ -101,12 +101,17 @@ export interface BasicRelayInformation {
|
|||||||
* authentication to happen before a new connection may
|
* authentication to happen before a new connection may
|
||||||
* perform any other action. Even if set to False,
|
* perform any other action. Even if set to False,
|
||||||
* authentication may be required for specific actions.
|
* authentication may be required for specific actions.
|
||||||
|
* @param restricted_writes: this relay requires some kind
|
||||||
|
* of condition to be fulfilled in order to accept events
|
||||||
|
* (not necessarily, but including
|
||||||
* @param payment_required this relay requires payment
|
* @param payment_required this relay requires payment
|
||||||
* before a new connection may perform any action.
|
* before a new connection may perform any action.
|
||||||
|
* @param created_at_lower_limit: 'created_at' lower limit
|
||||||
|
* @param created_at_upper_limit: 'created_at' upper limit
|
||||||
*/
|
*/
|
||||||
export interface Limitations {
|
export interface Limitations {
|
||||||
max_message_length: number
|
max_message_length: number
|
||||||
max_subscription: number
|
max_subscriptions: number
|
||||||
max_filters: number
|
max_filters: number
|
||||||
max_limit: number
|
max_limit: number
|
||||||
max_subid_length: number
|
max_subid_length: number
|
||||||
@@ -116,6 +121,9 @@ export interface Limitations {
|
|||||||
min_pow_difficulty: number
|
min_pow_difficulty: number
|
||||||
auth_required: boolean
|
auth_required: boolean
|
||||||
payment_required: boolean
|
payment_required: boolean
|
||||||
|
created_at_lower_limit: number
|
||||||
|
created_at_upper_limit: number
|
||||||
|
restricted_writes: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RetentionDetails {
|
interface RetentionDetails {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ describe('generateEventTemplate', () => {
|
|||||||
image: 'https://example.com/image.jpg',
|
image: 'https://example.com/image.jpg',
|
||||||
summary: 'Lorem ipsum',
|
summary: 'Lorem ipsum',
|
||||||
alt: 'Image alt text',
|
alt: 'Image alt text',
|
||||||
|
fallback: ['https://fallback1.example.com/image.jpg', 'https://fallback2.example.com/image.jpg'],
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedEventTemplate: EventTemplate = {
|
const expectedEventTemplate: EventTemplate = {
|
||||||
@@ -40,6 +41,8 @@ describe('generateEventTemplate', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback1.example.com/image.jpg'],
|
||||||
|
['fallback', 'https://fallback2.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -100,6 +104,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -129,6 +134,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -158,6 +164,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -181,6 +188,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -204,6 +212,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -227,6 +236,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -259,6 +269,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -288,6 +299,7 @@ describe('validateEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -319,6 +331,8 @@ describe('parseEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback1.example.com/image.jpg'],
|
||||||
|
['fallback', 'https://fallback2.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
@@ -340,6 +354,7 @@ describe('parseEvent', () => {
|
|||||||
image: 'https://example.com/image.jpg',
|
image: 'https://example.com/image.jpg',
|
||||||
summary: 'Lorem ipsum',
|
summary: 'Lorem ipsum',
|
||||||
alt: 'Image alt text',
|
alt: 'Image alt text',
|
||||||
|
fallback: ['https://fallback1.example.com/image.jpg', 'https://fallback2.example.com/image.jpg'],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -364,6 +379,7 @@ describe('parseEvent', () => {
|
|||||||
['image', 'https://example.com/image.jpg'],
|
['image', 'https://example.com/image.jpg'],
|
||||||
['summary', 'Lorem ipsum'],
|
['summary', 'Lorem ipsum'],
|
||||||
['alt', 'Image alt text'],
|
['alt', 'Image alt text'],
|
||||||
|
['fallback', 'https://fallback.example.com/image.jpg'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
|
|||||||
10
nip94.ts
10
nip94.ts
@@ -75,6 +75,11 @@ export type FileMetadataObject = {
|
|||||||
* Optional: A description for accessibility, providing context or a brief description of the file.
|
* Optional: A description for accessibility, providing context or a brief description of the file.
|
||||||
*/
|
*/
|
||||||
alt?: string
|
alt?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: fallback URLs in case url fails.
|
||||||
|
*/
|
||||||
|
fallback?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +109,7 @@ export function generateEventTemplate(fileMetadata: FileMetadataObject): EventTe
|
|||||||
if (fileMetadata.image) eventTemplate.tags.push(['image', fileMetadata.image])
|
if (fileMetadata.image) eventTemplate.tags.push(['image', fileMetadata.image])
|
||||||
if (fileMetadata.summary) eventTemplate.tags.push(['summary', fileMetadata.summary])
|
if (fileMetadata.summary) eventTemplate.tags.push(['summary', fileMetadata.summary])
|
||||||
if (fileMetadata.alt) eventTemplate.tags.push(['alt', fileMetadata.alt])
|
if (fileMetadata.alt) eventTemplate.tags.push(['alt', fileMetadata.alt])
|
||||||
|
if (fileMetadata.fallback) fileMetadata.fallback.forEach(url => eventTemplate.tags.push(['fallback', url]))
|
||||||
|
|
||||||
return eventTemplate
|
return eventTemplate
|
||||||
}
|
}
|
||||||
@@ -194,6 +200,10 @@ export function parseEvent(event: Event): FileMetadataObject {
|
|||||||
case 'alt':
|
case 'alt':
|
||||||
fileMetadata.alt = value
|
fileMetadata.alt = value
|
||||||
break
|
break
|
||||||
|
case 'fallback':
|
||||||
|
fileMetadata.fallback ??= []
|
||||||
|
fileMetadata.fallback.push(value)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "2.6.0",
|
"version": "2.7.1",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||||
|
|
||||||
import { SimplePool } from './pool.ts'
|
import { SimplePool, useWebSocketImplementation } from './pool.ts'
|
||||||
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
||||||
import { useWebSocketImplementation } from './relay.ts'
|
|
||||||
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
|
|||||||
14
pool.ts
14
pool.ts
@@ -1,9 +1,21 @@
|
|||||||
|
/* global WebSocket */
|
||||||
|
|
||||||
import { verifyEvent } from './pure.ts'
|
import { verifyEvent } from './pure.ts'
|
||||||
import { AbstractSimplePool } from './abstract-pool.ts'
|
import { AbstractSimplePool } from './abstract-pool.ts'
|
||||||
|
|
||||||
|
var _WebSocket: typeof WebSocket
|
||||||
|
|
||||||
|
try {
|
||||||
|
_WebSocket = WebSocket
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
export function useWebSocketImplementation(websocketImplementation: any) {
|
||||||
|
_WebSocket = websocketImplementation
|
||||||
|
}
|
||||||
|
|
||||||
export class SimplePool extends AbstractSimplePool {
|
export class SimplePool extends AbstractSimplePool {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ verifyEvent })
|
super({ verifyEvent, websocketImplementation: _WebSocket })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
relay.ts
16
relay.ts
@@ -1,3 +1,5 @@
|
|||||||
|
/* global WebSocket */
|
||||||
|
|
||||||
import { verifyEvent } from './pure.ts'
|
import { verifyEvent } from './pure.ts'
|
||||||
import { AbstractRelay } from './abstract-relay.ts'
|
import { AbstractRelay } from './abstract-relay.ts'
|
||||||
|
|
||||||
@@ -8,9 +10,19 @@ export function relayConnect(url: string): Promise<Relay> {
|
|||||||
return Relay.connect(url)
|
return Relay.connect(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _WebSocket: typeof WebSocket
|
||||||
|
|
||||||
|
try {
|
||||||
|
_WebSocket = WebSocket
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
export function useWebSocketImplementation(websocketImplementation: any) {
|
||||||
|
_WebSocket = websocketImplementation
|
||||||
|
}
|
||||||
|
|
||||||
export class Relay extends AbstractRelay {
|
export class Relay extends AbstractRelay {
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
super(url, { verifyEvent })
|
super(url, { verifyEvent, websocketImplementation: _WebSocket })
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect(url: string): Promise<Relay> {
|
static async connect(url: string): Promise<Relay> {
|
||||||
@@ -20,6 +32,6 @@ export class Relay extends AbstractRelay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelayRecord = Record<string, { read: boolean; write: boolean }>;
|
export type RelayRecord = Record<string, { read: boolean; write: boolean }>
|
||||||
|
|
||||||
export * from './abstract-relay.ts'
|
export * from './abstract-relay.ts'
|
||||||
|
|||||||
Reference in New Issue
Block a user