Compare commits

..

6 Commits

Author SHA1 Message Date
fiatjaf
1b77d6e080 mention nostrify.dev on readme. 2024-08-08 12:13:41 -03:00
Sam Samskies
76d3a91600 authorization should not be in the form data 2024-08-08 12:00:58 -03:00
Sepehr Safari
6f334f31a7 add and improve helpers for nip29 2024-08-01 18:18:13 -03:00
Alex Gleason
9c009ac543 getFilterLimit: handle parameterized replaceable events 2024-07-20 22:00:28 -03:00
Shusui MOYATANI
a87099fa5c remove Content-Type header from NIP-96 uploadFile 2024-07-20 09:44:45 -03:00
António Conselheiro
475a22a95f methods for abstract pool (#419)
* include method to list current pool relays connections and to close all connections

* fix prettier
2024-07-18 13:33:29 -03:00
7 changed files with 601 additions and 130 deletions

View File

@@ -4,7 +4,7 @@ Tools for developing [Nostr](https://github.com/fiatjaf/nostr) clients.
Only depends on _@scure_ and _@noble_ packages. Only depends on _@scure_ and _@noble_ packages.
This package is only providing lower-level functionality. If you want an easy-to-use fully-fledged solution that abstracts the hard parts of Nostr and makes decisions on your behalf, take a look at [NDK](https://github.com/nostr-dev-kit/ndk) and [@snort/system](https://www.npmjs.com/package/@snort/system). This package is only providing lower-level functionality. If you want more higher-level features, take a look at [Nostrify](https://nostrify.dev), or if you want an easy-to-use fully-fledged solution that abstracts the hard parts of Nostr and makes decisions on your behalf, take a look at [NDK](https://github.com/nostr-dev-kit/ndk) and [@snort/system](https://www.npmjs.com/package/@snort/system).
## Installation ## Installation

View File

@@ -208,4 +208,16 @@ export class AbstractSimplePool {
return r.publish(event) return r.publish(event)
}) })
} }
listConnectionStatus(): Map<string, boolean> {
const map = new Map<string, boolean>()
this.relays.forEach((relay, url) => map.set(url, relay.connected))
return map
}
destroy(): void {
this.relays.forEach(conn => conn.close())
this.relays = new Map()
}
} }

View File

@@ -215,6 +215,16 @@ describe('Filter', () => {
expect(getFilterLimit({ kinds: [0, 3], authors: ['alex', 'fiatjaf'] })).toEqual(4) expect(getFilterLimit({ kinds: [0, 3], authors: ['alex', 'fiatjaf'] })).toEqual(4)
}) })
test('should handle parameterized replaceable events', () => {
expect(getFilterLimit({ kinds: [30078], authors: ['alex'] })).toEqual(Infinity)
expect(getFilterLimit({ kinds: [30078], authors: ['alex'], '#d': ['ditto'] })).toEqual(1)
expect(getFilterLimit({ kinds: [30078], authors: ['alex'], '#d': ['ditto', 'soapbox'] })).toEqual(2)
expect(getFilterLimit({ kinds: [30078], authors: ['alex', 'fiatjaf'], '#d': ['ditto', 'soapbox'] })).toEqual(4)
expect(
getFilterLimit({ kinds: [30000, 30078], authors: ['alex', 'fiatjaf'], '#d': ['ditto', 'soapbox'] }),
).toEqual(8)
})
test('should return Infinity for authors with regular kinds', () => { test('should return Infinity for authors with regular kinds', () => {
expect(getFilterLimit({ kinds: [1], authors: ['alex'] })).toEqual(Infinity) expect(getFilterLimit({ kinds: [1], authors: ['alex'] })).toEqual(Infinity)
}) })

View File

@@ -1,5 +1,5 @@
import { Event } from './core.ts' import { Event } from './core.ts'
import { isReplaceableKind } from './kinds.ts' import { isParameterizedReplaceableKind, isReplaceableKind } from './kinds.ts'
export type Filter = { export type Filter = {
ids?: string[] ids?: string[]
@@ -72,7 +72,10 @@ export function mergeFilters(...filters: Filter[]): Filter {
return result return result
} }
/** Calculate the intrinsic limit of a filter. This function may return `Infinity`. */ /**
* Calculate the intrinsic limit of a filter.
* This function returns a positive integer, or `Infinity` if there is no intrinsic limit.
*/
export function getFilterLimit(filter: Filter): number { export function getFilterLimit(filter: Filter): number {
if (filter.ids && !filter.ids.length) return 0 if (filter.ids && !filter.ids.length) return 0
if (filter.kinds && !filter.kinds.length) return 0 if (filter.kinds && !filter.kinds.length) return 0
@@ -83,10 +86,20 @@ export function getFilterLimit(filter: Filter): number {
} }
return Math.min( return Math.min(
// The `limit` property creates an artificial limit.
Math.max(0, filter.limit ?? Infinity), Math.max(0, filter.limit ?? Infinity),
// There can only be one event per `id`.
filter.ids?.length ?? Infinity, filter.ids?.length ?? Infinity,
// Replaceable events are limited by the number of authors and kinds.
filter.authors?.length && filter.kinds?.every(kind => isReplaceableKind(kind)) filter.authors?.length && filter.kinds?.every(kind => isReplaceableKind(kind))
? filter.authors.length * filter.kinds.length ? filter.authors.length * filter.kinds.length
: Infinity, : Infinity,
// Parameterized replaceable events are limited by the number of authors, kinds, and "d" tags.
filter.authors?.length && filter.kinds?.every(kind => isParameterizedReplaceableKind(kind)) && filter['#d']?.length
? filter.authors.length * filter.kinds.length * filter['#d'].length
: Infinity,
) )
} }

684
nip29.ts
View File

@@ -1,80 +1,514 @@
import { AbstractSimplePool } from './abstract-pool.ts' import { AbstractSimplePool } from './abstract-pool.ts'
import { Subscription } from './abstract-relay.ts' import { Subscription } from './abstract-relay.ts'
import { decode } from './nip19.ts' import type { Event, EventTemplate } from './core.ts'
import type { Event } from './core.ts' import { fetchRelayInformation, RelayInformation } from './nip11.ts'
import { fetchRelayInformation } from './nip11.ts' import { AddressPointer, decode } from './nip19.ts'
import { normalizeURL } from './utils.ts' import { normalizeURL } from './utils.ts'
import { AddressPointer } from './nip19.ts'
export function subscribeRelayGroups( /**
pool: AbstractSimplePool, * Represents a NIP29 group.
url: string, */
params: { export type Group = {
ongroups: (_: Group[]) => void relay: string
onerror: (_: Error) => void metadata: GroupMetadata
onconnect?: () => void admins?: GroupAdmin[]
}, members?: GroupMember[]
): () => void { reference: GroupReference
let normalized = normalizeURL(url)
let sub: Subscription
let groups: Group[] = []
fetchRelayInformation(normalized)
.then(async info => {
let rl = await pool.ensureRelay(normalized)
params.onconnect?.()
sub = rl.prepareSubscription(
[
{
kinds: [39000],
limit: 50,
authors: [info.pubkey],
},
],
{
onevent(event: Event) {
groups.push(parseGroup(event, normalized))
},
oneose() {
params.ongroups(groups)
sub.onevent = (event: Event) => {
groups.push(parseGroup(event, normalized))
params.ongroups(groups)
}
},
},
)
sub.fire()
})
.catch(params.onerror)
return () => sub.close()
} }
export async function loadGroup(pool: AbstractSimplePool, gr: GroupReference): Promise<Group> { /**
let normalized = normalizeURL(gr.host) * Represents the metadata for a NIP29 group.
*/
let info = await fetchRelayInformation(normalized) export type GroupMetadata = {
let event = await pool.get([normalized], { id: string
kinds: [39000], pubkey: string
authors: [info.pubkey], name?: string
'#d': [gr.id], picture?: string
}) about?: string
if (!event) throw new Error(`group '${gr.id}' not found on ${gr.host}`) isPublic?: boolean
return parseGroup(event, normalized) isOpen?: boolean
}
export async function loadGroupFromCode(pool: AbstractSimplePool, code: string): Promise<Group> {
let gr = parseGroupCode(code)
if (!gr) throw new Error(`code "${code}" does not identify a group`)
return loadGroup(pool, gr)
} }
/**
* Represents a NIP29 group reference.
*/
export type GroupReference = { export type GroupReference = {
id: string id: string
host: string host: string
} }
/**
* Represents a NIP29 group member.
*/
export type GroupMember = {
pubkey: string
label?: string
}
/**
* Represents a NIP29 group admin.
*/
export type GroupAdmin = {
pubkey: string
label?: string
permissions: GroupAdminPermission[]
}
/**
* Represents the permissions that a NIP29 group admin can have.
*/
export enum GroupAdminPermission {
AddUser = 'add-user',
EditMetadata = 'edit-metadata',
DeleteEvent = 'delete-event',
RemoveUser = 'remove-user',
AddPermission = 'add-permission',
RemovePermission = 'remove-permission',
EditGroupStatus = 'edit-group-status',
}
/**
* Generates a group metadata event template.
*
* @param group - The group object.
* @returns An event template with the generated group metadata that can be signed later.
*/
export function generateGroupMetadataEventTemplate(group: Group): EventTemplate {
const tags: string[][] = [['d', group.metadata.id]]
group.metadata.name && tags.push(['name', group.metadata.name])
group.metadata.picture && tags.push(['picture', group.metadata.picture])
group.metadata.about && tags.push(['about', group.metadata.about])
group.metadata.isPublic && tags.push(['public'])
group.metadata.isOpen && tags.push(['open'])
return {
content: '',
created_at: Math.floor(Date.now() / 1000),
kind: 39000,
tags,
}
}
/**
* Validates a group metadata event.
*
* @param event - The event to validate.
* @returns A boolean indicating whether the event is valid.
*/
export function validateGroupMetadataEvent(event: Event): boolean {
if (event.kind !== 39000) return false
if (!event.pubkey) return false
const requiredTags = ['d'] as const
for (const tag of requiredTags) {
if (!event.tags.find(([t]) => t == tag)) return false
}
return true
}
/**
* Generates an event template for group admins.
*
* @param group - The group object.
* @param admins - An array of group admins.
* @returns The generated event template with the group admins that can be signed later.
*/
export function generateGroupAdminsEventTemplate(group: Group, admins: GroupAdmin[]): EventTemplate {
const tags: string[][] = [['d', group.metadata.id]]
for (const admin of admins) {
tags.push(['p', admin.pubkey, admin.label || '', ...admin.permissions])
}
return {
content: '',
created_at: Math.floor(Date.now() / 1000),
kind: 39001,
tags,
}
}
/**
* Validates a group admins event.
*
* @param event - The event to validate.
* @returns True if the event is valid, false otherwise.
*/
export function validateGroupAdminsEvent(event: Event): boolean {
if (event.kind !== 39001) return false
const requiredTags = ['d'] as const
for (const tag of requiredTags) {
if (!event.tags.find(([t]) => t == tag)) return false
}
// validate permissions
for (const [tag, value, label, ...permissions] of event.tags) {
if (tag !== 'p') continue
for (let i = 0; i < permissions.length; i += 1) {
if (typeof permissions[i] !== 'string') return false
// validate permission name from the GroupAdminPermission enum
if (!Object.values(GroupAdminPermission).includes(permissions[i] as GroupAdminPermission)) return false
}
}
return true
}
/**
* Generates an event template for a group with its members.
*
* @param group - The group object.
* @param members - An array of group members.
* @returns The generated event template with the group members that can be signed later.
*/
export function generateGroupMembersEventTemplate(group: Group, members: GroupMember[]): EventTemplate {
const tags: string[][] = [['d', group.metadata.id]]
for (const member of members) {
tags.push(['p', member.pubkey, member.label || ''])
}
return {
content: '',
created_at: Math.floor(Date.now() / 1000),
kind: 39002,
tags,
}
}
/**
* Validates a group members event.
*
* @param event - The event to validate.
* @returns Returns `true` if the event is a valid group members event, `false` otherwise.
*/
export function validateGroupMembersEvent(event: Event): boolean {
if (event.kind !== 39002) return false
const requiredTags = ['d'] as const
for (const tag of requiredTags) {
if (!event.tags.find(([t]) => t == tag)) return false
}
return true
}
/**
* Returns the normalized relay URL based on the provided group reference.
*
* @param groupReference - The group reference object containing the host.
* @returns The normalized relay URL.
*/
export function getNormalizedRelayURLByGroupReference(groupReference: GroupReference): string {
return normalizeURL(groupReference.host)
}
/**
* Fetches relay information by group reference.
*
* @param groupReference The group reference.
* @returns A promise that resolves to the relay information.
*/
export async function fetchRelayInformationByGroupReference(groupReference: GroupReference): Promise<RelayInformation> {
const normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
return fetchRelayInformation(normalizedRelayURL)
}
/**
* Fetches the group metadata event from the specified pool.
* If the normalizedRelayURL is not provided, it will be obtained using the groupReference.
* If the relayInformation is not provided, it will be fetched using the normalizedRelayURL.
*
* @param {Object} options - The options object.
* @param {AbstractSimplePool} options.pool - The pool to fetch the group metadata event from.
* @param {GroupReference} options.groupReference - The reference to the group.
* @param {string} [options.normalizedRelayURL] - The normalized URL of the relay.
* @param {RelayInformation} [options.relayInformation] - The relay information object.
* @returns {Promise<Event>} The group metadata event that can be parsed later to get the group metadata object.
* @throws {Error} If the group is not found on the specified relay.
*/
export async function fetchGroupMetadataEvent({
pool,
groupReference,
relayInformation,
normalizedRelayURL,
}: {
pool: AbstractSimplePool
groupReference: GroupReference
normalizedRelayURL?: string
relayInformation?: RelayInformation
}): Promise<Event> {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL)
}
const groupMetadataEvent = await pool.get([normalizedRelayURL], {
kinds: [39000],
authors: [relayInformation.pubkey],
'#d': [groupReference.id],
})
if (!groupMetadataEvent) throw new Error(`group '${groupReference.id}' not found on ${normalizedRelayURL}`)
return groupMetadataEvent
}
/**
* Parses a group metadata event and returns the corresponding GroupMetadata object.
*
* @param event - The event to parse.
* @returns The parsed GroupMetadata object.
* @throws An error if the group metadata event is invalid.
*/
export function parseGroupMetadataEvent(event: Event): GroupMetadata {
if (!validateGroupMetadataEvent(event)) throw new Error('invalid group metadata event')
const metadata: GroupMetadata = {
id: '',
pubkey: event.pubkey,
}
for (const [tag, value] of event.tags) {
switch (tag) {
case 'd':
metadata.id = value
break
case 'name':
metadata.name = value
break
case 'picture':
metadata.picture = value
break
case 'about':
metadata.about = value
break
case 'public':
metadata.isPublic = true
break
case 'open':
metadata.isOpen = true
break
}
}
return metadata
}
/**
* Fetches the group admins event from the specified pool.
* If the normalizedRelayURL is not provided, it will be obtained from the groupReference.
* If the relayInformation is not provided, it will be fetched using the normalizedRelayURL.
*
* @param {Object} options - The options object.
* @param {AbstractSimplePool} options.pool - The pool to fetch the group admins event from.
* @param {GroupReference} options.groupReference - The reference to the group.
* @param {string} [options.normalizedRelayURL] - The normalized relay URL.
* @param {RelayInformation} [options.relayInformation] - The relay information.
* @returns {Promise<Event>} The group admins event that can be parsed later to get the group admins object.
* @throws {Error} If the group admins event is not found on the specified relay.
*/
export async function fetchGroupAdminsEvent({
pool,
groupReference,
relayInformation,
normalizedRelayURL,
}: {
pool: AbstractSimplePool
groupReference: GroupReference
normalizedRelayURL?: string
relayInformation?: RelayInformation
}): Promise<Event> {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL)
}
const groupAdminsEvent = await pool.get([normalizedRelayURL], {
kinds: [39001],
authors: [relayInformation.pubkey],
'#d': [groupReference.id],
})
if (!groupAdminsEvent) throw new Error(`admins for group '${groupReference.id}' not found on ${normalizedRelayURL}`)
return groupAdminsEvent
}
/**
* Parses a group admins event and returns an array of GroupAdmin objects.
*
* @param event - The event to parse.
* @returns An array of GroupAdmin objects.
* @throws Throws an error if the group admins event is invalid.
*/
export function parseGroupAdminsEvent(event: Event): GroupAdmin[] {
if (!validateGroupAdminsEvent(event)) throw new Error('invalid group admins event')
const admins: GroupAdmin[] = []
for (const [tag, value, label, ...permissions] of event.tags) {
if (tag !== 'p') continue
admins.push({
pubkey: value,
label,
permissions: permissions as GroupAdminPermission[],
})
}
return admins
}
/**
* Fetches the group members event from the specified relay.
* If the normalizedRelayURL is not provided, it will be obtained using the groupReference.
* If the relayInformation is not provided, it will be fetched using the normalizedRelayURL.
*
* @param {Object} options - The options object.
* @param {AbstractSimplePool} options.pool - The pool object.
* @param {GroupReference} options.groupReference - The group reference object.
* @param {string} [options.normalizedRelayURL] - The normalized relay URL.
* @param {RelayInformation} [options.relayInformation] - The relay information object.
* @returns {Promise<Event>} The group members event that can be parsed later to get the group members object.
* @throws {Error} If the group members event is not found.
*/
export async function fetchGroupMembersEvent({
pool,
groupReference,
relayInformation,
normalizedRelayURL,
}: {
pool: AbstractSimplePool
groupReference: GroupReference
normalizedRelayURL?: string
relayInformation?: RelayInformation
}): Promise<Event> {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL)
}
const groupMembersEvent = await pool.get([normalizedRelayURL], {
kinds: [39002],
authors: [relayInformation.pubkey],
'#d': [groupReference.id],
})
if (!groupMembersEvent) throw new Error(`members for group '${groupReference.id}' not found on ${normalizedRelayURL}`)
return groupMembersEvent
}
/**
* Parses a group members event and returns an array of GroupMember objects.
* @param event - The event to parse.
* @returns An array of GroupMember objects.
* @throws Throws an error if the group members event is invalid.
*/
export function parseGroupMembersEvent(event: Event): GroupMember[] {
if (!validateGroupMembersEvent(event)) throw new Error('invalid group members event')
const members: GroupMember[] = []
for (const [tag, value, label] of event.tags) {
if (tag !== 'p') continue
members.push({
pubkey: value,
label,
})
}
return members
}
/**
* Fetches and parses the group metadata event, group admins event, and group members event from the specified pool.
* If the normalized relay URL is not provided, it will be obtained using the group reference.
* If the relay information is not provided, it will be fetched using the normalized relay URL.
*
* @param {Object} options - The options for loading the group.
* @param {AbstractSimplePool} options.pool - The pool to load the group from.
* @param {GroupReference} options.groupReference - The reference of the group to load.
* @param {string} [options.normalizedRelayURL] - The normalized URL of the relay to use.
* @param {RelayInformation} [options.relayInformation] - The relay information to use.
* @returns {Promise<Group>} A promise that resolves to the loaded group.
*/
export async function loadGroup({
pool,
groupReference,
normalizedRelayURL,
relayInformation,
}: {
pool: AbstractSimplePool
groupReference: GroupReference
normalizedRelayURL?: string
relayInformation?: RelayInformation
}): Promise<Group> {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL)
}
const metadataEvent = await fetchGroupMetadataEvent({ pool, groupReference, normalizedRelayURL, relayInformation })
const metadata = parseGroupMetadataEvent(metadataEvent)
const adminsEvent = await fetchGroupAdminsEvent({ pool, groupReference, normalizedRelayURL, relayInformation })
const admins = parseGroupAdminsEvent(adminsEvent)
const membersEvent = await fetchGroupMembersEvent({ pool, groupReference, normalizedRelayURL, relayInformation })
const members = parseGroupMembersEvent(membersEvent)
const group: Group = {
relay: normalizedRelayURL,
metadata,
admins,
members,
reference: groupReference,
}
return group
}
/**
* Loads a group from the specified pool using the provided group code.
*
* @param {AbstractSimplePool} pool - The pool to load the group from.
* @param {string} code - The code representing the group.
* @returns {Promise<Group>} - A promise that resolves to the loaded group.
* @throws {Error} - If the group code is invalid.
*/
export async function loadGroupFromCode(pool: AbstractSimplePool, code: string): Promise<Group> {
const groupReference = parseGroupCode(code)
if (!groupReference) throw new Error('invalid group code')
return loadGroup({ pool, groupReference })
}
/**
* Parses a group code and returns a GroupReference object.
*
* @param code The group code to parse.
* @returns A GroupReference object if the code is valid, otherwise null.
*/
export function parseGroupCode(code: string): null | GroupReference { export function parseGroupCode(code: string): null | GroupReference {
if (code.startsWith('naddr1')) { if (code.startsWith('naddr1')) {
try { try {
@@ -99,68 +533,74 @@ export function parseGroupCode(code: string): null | GroupReference {
return null return null
} }
/**
* Encodes a group reference into a string.
*
* @param gr - The group reference to encode.
* @returns The encoded group reference as a string.
*/
export function encodeGroupReference(gr: GroupReference): string { export function encodeGroupReference(gr: GroupReference): string {
if (gr.host.startsWith('https://')) gr.host = gr.host.slice(8) const { host, id } = gr
if (gr.host.startsWith('wss://')) gr.host = gr.host.slice(6) const normalizedHost = host.replace(/^(https?:\/\/|wss?:\/\/)/, '')
return `${gr.host}'${gr.id}`
return `${normalizedHost}'${id}`
} }
export type Group = { /**
id: string * Subscribes to relay groups metadata events and calls the provided event handler function
relay: string * when an event is received.
pubkey: string *
name?: string * @param {Object} options - The options for subscribing to relay groups metadata events.
picture?: string * @param {AbstractSimplePool} options.pool - The pool to subscribe to.
about?: string * @param {string} options.relayURL - The URL of the relay.
public?: boolean * @param {Function} options.onError - The error handler function.
open?: boolean * @param {Function} options.onEvent - The event handler function.
} * @param {Function} [options.onConnect] - The connect handler function.
* @returns {Function} - A function to close the subscription
*/
export function subscribeRelayGroupsMetadataEvents({
pool,
relayURL,
onError,
onEvent,
onConnect,
}: {
pool: AbstractSimplePool
relayURL: string
onError: (err: Error) => void
onEvent: (event: Event) => void
onConnect?: () => void
}): () => void {
let sub: Subscription
export function parseGroup(event: Event, relay: string): Group { const normalizedRelayURL = normalizeURL(relayURL)
const group: Partial<Group> = { relay, pubkey: event.pubkey }
for (let i = 0; i < event.tags.length; i++) {
const tag = event.tags[i]
switch (tag[0]) {
case 'd':
group.id = tag[1] || ''
break
case 'name':
group.name = tag[1] || ''
break
case 'about':
group.about = tag[1] || ''
break
case 'picture':
group.picture = tag[1] || ''
break
case 'open':
group.open = true
break
case 'public':
group.public = true
break
}
}
return group as Group
}
export type Member = { fetchRelayInformation(normalizedRelayURL)
pubkey: string .then(async info => {
label?: string const abstractedRelay = await pool.ensureRelay(normalizedRelayURL)
permissions: string[]
}
export function parseMembers(event: Event): Member[] { onConnect?.()
const members = []
for (let i = 0; i < event.tags.length; i++) { sub = abstractedRelay.prepareSubscription(
const tag = event.tags[i] [
if (tag.length < 2) continue {
if (tag[0] !== 'p') continue kinds: [39000],
if (!tag[1].match(/^[0-9a-f]{64}$/)) continue limit: 50,
const member: Member = { pubkey: tag[1], permissions: [] } authors: [info.pubkey],
if (tag.length > 2) member.label = tag[2] },
if (tag.length > 3) member.permissions = tag.slice(3) ],
members.push(member) {
} onevent(event: Event) {
return members onEvent(event)
},
},
)
})
.catch(err => {
sub.close()
onError(err)
})
return () => sub.close()
} }

View File

@@ -340,9 +340,6 @@ export async function uploadFile(
// Create FormData object // Create FormData object
const formData = new FormData() const formData = new FormData()
// Append the authorization header to HTML Form Data
formData.append('Authorization', nip98AuthorizationHeader)
// Append optional fields to FormData // Append optional fields to FormData
optionalFormDataFields && optionalFormDataFields &&
Object.entries(optionalFormDataFields).forEach(([key, value]) => { Object.entries(optionalFormDataFields).forEach(([key, value]) => {
@@ -359,7 +356,6 @@ export async function uploadFile(
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: nip98AuthorizationHeader, Authorization: nip98AuthorizationHeader,
'Content-Type': 'multipart/form-data',
}, },
body: formData, body: formData,
}) })

View File

@@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "nostr-tools", "name": "nostr-tools",
"version": "2.7.1", "version": "2.7.2",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"repository": { "repository": {
"type": "git", "type": "git",