Compare commits

..

11 Commits

Author SHA1 Message Date
Vinit
e398617fdc chore: use describe block to group NostrTypeGuard tests
this way we don't need to repeat NostrTypeGuard in each tests description
2024-10-11 07:48:46 -03:00
Vinit
1b236faa7b fix: move NostrTypeGuard tests to nip19.test.ts
NostrTypeGuard was moved to nip19.ts in commit 45b25c5bf5
but tests stayed in core.test.ts and started failing because it still imported
NostrTypeGuard from core.ts - which wasn't there.
2024-10-11 07:48:46 -03:00
Asai Toshiya
7064e0b828 make it possible to track relays on publish. 2024-10-10 14:45:37 -03:00
space-shell
4f6976f6f8 fix nip44Decrypt sending "nip44_encrypt" request (#435)
nip44Decrypt sends nip44_decrypt request
2024-09-24 12:56:36 -03:00
António Conselheiro
a61cde77ea Kinds from nip17 nip59 (#434) 2024-09-20 17:05:52 -03:00
fiatjaf
23d95acb26 move Nip05 type to nip05.ts 2024-09-09 14:23:03 -03:00
fiatjaf
13ac04b8f8 nip19: fix ncryptsec type guard.
see https://github.com/nbd-wtf/nostr-tools/pull/409#issuecomment-2338661000
2024-09-09 14:21:29 -03:00
fiatjaf
45b25c5bf5 nip19/nip49: remove nrelay and move bech32 string guard methods from core to nip19. 2024-09-09 14:20:35 -03:00
António Conselheiro
ee76d69b4b including nostr specialized types (#409)
* including nostr types

* including tests for nostr type guard

* fix tests for nostr type guard

* fix linter and add eslint and prettier to devcontainer

* including null in nostr type guard signature

* fix type, ops

* including ncryptsec in nostr type guard

* fix linter for ncryptsec

* including ncryptsec return type for nip49

* fixing names of nostr types and types guards

* fixing names of nostr types and types guards in unit tests descriptions

* fix prettier

* including type guard for nip5
2024-09-09 14:16:23 -03:00
Vinit
21433049b8 fix: eslint no-unused vars violations and setup
@typescript-eslint documents recommends turning off the base
unused-vars rule in eslint explicitly and using '@typescript-eslint/no-unused var'
instead. In this case, the base rule failed to correctly report enums. (reported
unused even though they were used).

Also, fixed two unused variables by adding ignore pattern '_'.
2024-09-06 19:45:50 -03:00
Asai Toshiya
e8ff68f0b3 Update README.md 2024-08-15 11:29:18 -03:00
15 changed files with 254 additions and 46 deletions

View File

@@ -1,6 +1,6 @@
FROM node:20
RUN npm install typescript -g
RUN npm install typescript eslint prettier -g
# Install bun
RUN curl -fsSL https://bun.sh/install | bash

View File

@@ -116,7 +116,8 @@
"no-unexpected-multiline": 2,
"no-unneeded-ternary": [2, { "defaultAssignment": false }],
"no-unreachable": 2,
"no-unused-vars": [2, { "vars": "local", "args": "none", "varsIgnorePattern": "^_" }],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [2, { "vars": "local", "args": "none", "varsIgnorePattern": "^_" }],
"no-useless-call": 2,
"no-useless-constructor": 2,
"no-with": 2,

View File

@@ -76,7 +76,7 @@ const sub = relay.subscribe([
let sk = generateSecretKey()
let pk = getPublicKey(sk)
relay.sub([
relay.subscribe([
{
kinds: [1],
authors: [pk],

View File

@@ -205,7 +205,17 @@ export class AbstractSimplePool {
}
let r = await this.ensureRelay(url)
return r.publish(event)
return r.publish(event).then(reason => {
if (this.trackRelays) {
let set = this.seenOn.get(event.id)
if (!set) {
set = new Set()
this.seenOn.set(event.id, set)
}
set.add(r)
}
return reason
})
})
}

View File

@@ -1,5 +1,4 @@
import { test, expect } from 'bun:test'
import { sortEvents } from './core.ts'
test('sortEvents', () => {

View File

@@ -35,11 +35,12 @@ export const ShortTextNote = 1
export const RecommendRelay = 2
export const Contacts = 3
export const EncryptedDirectMessage = 4
export const EncryptedDirectMessages = 4
export const EventDeletion = 5
export const Repost = 6
export const Reaction = 7
export const BadgeAward = 8
export const Seal = 13
export const PrivateDirectMessage = 14
export const GenericRepost = 16
export const ChannelCreation = 40
export const ChannelMetadata = 41
@@ -71,6 +72,8 @@ export const BlockedRelaysList = 10006
export const SearchRelaysList = 10007
export const InterestsList = 10015
export const UserEmojiList = 10030
export const DirectMessageRelaysList = 10050
export const GiftWrap = 10059
export const FileServerPreference = 10096
export const NWCWalletInfo = 13194
export const LightningPubRPC = 21000

View File

@@ -1,7 +1,7 @@
import { test, expect } from 'bun:test'
import fetch from 'node-fetch'
import { useFetchImplementation, queryProfile } from './nip05.ts'
import { useFetchImplementation, queryProfile, NIP05_REGEX, isNip05 } from './nip05.ts'
test('fetch nip05 profiles', async () => {
useFetchImplementation(fetch)
@@ -18,3 +18,15 @@ test('fetch nip05 profiles', async () => {
expect(p3!.pubkey).toEqual('3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d')
expect(p3!.relays).toEqual(['wss://pyramid.fiatjaf.com', 'wss://nos.lol'])
})
test('validate NIP05_REGEX', () => {
expect(NIP05_REGEX.test('_@bob.com.br')).toBeTrue()
expect(NIP05_REGEX.test('bob@bob.com.br')).toBeTrue()
expect(NIP05_REGEX.test('b&b@bob.com.br')).toBeFalse()
expect('b&b@bob.com.br'.match(NIP05_REGEX)).toBeNull()
expect(Array.from('bob@bob.com.br'.match(NIP05_REGEX) || [])).toEqual(['bob@bob.com.br', 'bob', 'bob.com.br', '.br'])
expect(isNip05('bob@bob.com.br')).toBeTrue()
expect(isNip05('b&b@bob.com.br')).toBeFalse()
})

View File

@@ -1,5 +1,7 @@
import { ProfilePointer } from './nip19.ts'
export type Nip05 = `${string}@${string}`
/**
* NIP-05 regex. The localpart is optional, and should be assumed to be `_` otherwise.
*
@@ -8,6 +10,7 @@ import { ProfilePointer } from './nip19.ts'
* - 2: domain
*/
export const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$/
export const isNip05 = (value?: string | null): value is Nip05 => NIP05_REGEX.test(value || '')
var _fetch: any
@@ -47,7 +50,7 @@ export async function queryProfile(fullname: string): Promise<ProfilePointer | n
}
}
export async function isValid(pubkey: string, nip05: string): Promise<boolean> {
export async function isValid(pubkey: string, nip05: Nip05): Promise<boolean> {
let res = await queryProfile(nip05)
return res ? res.pubkey === pubkey : false
}

View File

@@ -1,16 +1,16 @@
import { test, expect } from 'bun:test'
import { test, expect, describe } from 'bun:test'
import { generateSecretKey, getPublicKey } from './pure.ts'
import {
decode,
naddrEncode,
nprofileEncode,
npubEncode,
nrelayEncode,
nsecEncode,
neventEncode,
type AddressPointer,
type ProfilePointer,
EventPointer,
NostrTypeGuard,
} from './nip19.ts'
test('encode and decode nsec', () => {
@@ -153,11 +153,154 @@ test('decode naddr from go-nostr with different TLV ordering', () => {
expect(pointer.identifier).toEqual('banana')
})
test('encode and decode nrelay', () => {
let url = 'wss://relay.nostr.example'
let nrelay = nrelayEncode(url)
expect(nrelay).toMatch(/nrelay1\w+/)
let { type, data } = decode(nrelay)
expect(type).toEqual('nrelay')
expect(data).toEqual(url)
describe('NostrTypeGuard', () => {
test('isNProfile', () => {
const is = NostrTypeGuard.isNProfile('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg')
expect(is).toBeTrue()
})
test('isNProfile invalid nprofile', () => {
const is = NostrTypeGuard.isNProfile('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxãg')
expect(is).toBeFalse()
})
test('isNProfile with invalid nprofile', () => {
const is = NostrTypeGuard.isNProfile('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')
expect(is).toBeFalse()
})
test('isNRelay', () => {
const is = NostrTypeGuard.isNRelay('nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueq4r295t')
expect(is).toBeTrue()
})
test('isNRelay with invalid nrelay', () => {
const is = NostrTypeGuard.isNRelay('nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueã4r295t')
expect(is).toBeFalse()
})
test('isNRelay with invalid nrelay', () => {
const is = NostrTypeGuard.isNRelay(
'nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9',
)
expect(is).toBeFalse()
})
test('isNEvent', () => {
const is = NostrTypeGuard.isNEvent(
'nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9',
)
expect(is).toBeTrue()
})
test('isNEvent with invalid nevent', () => {
const is = NostrTypeGuard.isNEvent(
'nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8ãrnc9',
)
expect(is).toBeFalse()
})
test('isNEvent with invalid nevent', () => {
const is = NostrTypeGuard.isNEvent('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg')
expect(is).toBeFalse()
})
test('isNAddr', () => {
const is = NostrTypeGuard.isNAddr(
'naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld',
)
expect(is).toBeTrue()
})
test('isNAddr with invalid nadress', () => {
const is = NostrTypeGuard.isNAddr('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')
expect(is).toBeFalse()
})
test('isNSec', () => {
const is = NostrTypeGuard.isNSec('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')
expect(is).toBeTrue()
})
test('isNSec with invalid nsec', () => {
const is = NostrTypeGuard.isNSec('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juã')
expect(is).toBeFalse()
})
test('isNSec with invalid nsec', () => {
const is = NostrTypeGuard.isNSec('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg')
expect(is).toBeFalse()
})
test('isNPub', () => {
const is = NostrTypeGuard.isNPub('npub1jz5mdljkmffmqjshpyjgqgrhdkuxd9ztzasv8xeh5q92fv33sjgqy4pats')
expect(is).toBeTrue()
})
test('isNPub with invalid npub', () => {
const is = NostrTypeGuard.isNPub('npub1jz5mdljkmffmqjshpyjgqgrhdkuxd9ztzãsv8xeh5q92fv33sjgqy4pats')
expect(is).toBeFalse()
})
test('isNPub with invalid npub', () => {
const is = NostrTypeGuard.isNPub('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')
expect(is).toBeFalse()
})
test('isNote', () => {
const is = NostrTypeGuard.isNote('note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky')
expect(is).toBeTrue()
})
test('isNote with invalid note', () => {
const is = NostrTypeGuard.isNote('note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sçlreky')
expect(is).toBeFalse()
})
test('isNote with invalid note', () => {
const is = NostrTypeGuard.isNote('npub1jz5mdljkmffmqjshpyjgqgrhdkuxd9ztzasv8xeh5q92fv33sjgqy4pats')
expect(is).toBeFalse()
})
test('isNcryptsec', () => {
const is = NostrTypeGuard.isNcryptsec(
'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p',
)
expect(is).toBeTrue()
})
test('isNcryptsec with invalid ncrytpsec', () => {
const is = NostrTypeGuard.isNcryptsec(
'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsã8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p',
)
expect(is).toBeFalse()
})
test('isNcryptsec with invalid ncrytpsec', () => {
const is = NostrTypeGuard.isNcryptsec('note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sçlreky')
expect(is).toBeFalse()
})
})

View File

@@ -3,6 +3,26 @@ import { bech32 } from '@scure/base'
import { utf8Decoder, utf8Encoder } from './utils.ts'
export type NProfile = `nprofile1${string}`
export type NRelay = `nrelay1${string}`
export type NEvent = `nevent1${string}`
export type NAddr = `naddr1${string}`
export type NSec = `nsec1${string}`
export type NPub = `npub1${string}`
export type Note = `note1${string}`
export type Ncryptsec = `ncryptsec1${string}`
export const NostrTypeGuard = {
isNProfile: (value?: string | null): value is NProfile => /^nprofile1[a-z\d]+$/.test(value || ''),
isNRelay: (value?: string | null): value is NRelay => /^nrelay1[a-z\d]+$/.test(value || ''),
isNEvent: (value?: string | null): value is NEvent => /^nevent1[a-z\d]+$/.test(value || ''),
isNAddr: (value?: string | null): value is NAddr => /^naddr1[a-z\d]+$/.test(value || ''),
isNSec: (value?: string | null): value is NSec => /^nsec1[a-z\d]{58}$/.test(value || ''),
isNPub: (value?: string | null): value is NPub => /^npub1[a-z\d]{58}$/.test(value || ''),
isNote: (value?: string | null): value is Note => /^note1[a-z\d]+$/.test(value || ''),
isNcryptsec: (value?: string | null): value is Ncryptsec => /^ncryptsec1[a-z\d]+$/.test(value || ''),
}
export const Bech32MaxSize = 5000
/**
@@ -45,7 +65,6 @@ export type AddressPointer = {
type Prefixes = {
nprofile: ProfilePointer
nrelay: string
nevent: EventPointer
naddr: AddressPointer
nsec: Uint8Array
@@ -119,16 +138,6 @@ export function decode(nip19: string): DecodeResult {
}
}
case 'nrelay': {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nrelay')
return {
type: 'nrelay',
data: utf8Decoder.decode(tlv[0][0]),
}
}
case 'nsec':
return { type: prefix, data }
@@ -158,15 +167,15 @@ function parseTLV(data: Uint8Array): TLV {
return result
}
export function nsecEncode(key: Uint8Array): `nsec1${string}` {
export function nsecEncode(key: Uint8Array): NSec {
return encodeBytes('nsec', key)
}
export function npubEncode(hex: string): `npub1${string}` {
export function npubEncode(hex: string): NPub {
return encodeBytes('npub', hexToBytes(hex))
}
export function noteEncode(hex: string): `note1${string}` {
export function noteEncode(hex: string): Note {
return encodeBytes('note', hexToBytes(hex))
}
@@ -179,7 +188,7 @@ export function encodeBytes<Prefix extends string>(prefix: Prefix, bytes: Uint8A
return encodeBech32(prefix, bytes)
}
export function nprofileEncode(profile: ProfilePointer): `nprofile1${string}` {
export function nprofileEncode(profile: ProfilePointer): NProfile {
let data = encodeTLV({
0: [hexToBytes(profile.pubkey)],
1: (profile.relays || []).map(url => utf8Encoder.encode(url)),
@@ -187,7 +196,7 @@ export function nprofileEncode(profile: ProfilePointer): `nprofile1${string}` {
return encodeBech32('nprofile', data)
}
export function neventEncode(event: EventPointer): `nevent1${string}` {
export function neventEncode(event: EventPointer): NEvent {
let kindArray
if (event.kind !== undefined) {
kindArray = integerToUint8Array(event.kind)
@@ -203,7 +212,7 @@ export function neventEncode(event: EventPointer): `nevent1${string}` {
return encodeBech32('nevent', data)
}
export function naddrEncode(addr: AddressPointer): `naddr1${string}` {
export function naddrEncode(addr: AddressPointer): NAddr {
let kind = new ArrayBuffer(4)
new DataView(kind).setUint32(0, addr.kind, false)
@@ -216,13 +225,6 @@ export function naddrEncode(addr: AddressPointer): `naddr1${string}` {
return encodeBech32('naddr', data)
}
export function nrelayEncode(url: string): `nrelay1${string}` {
let data = encodeTLV({
0: [utf8Encoder.encode(url)],
})
return encodeBech32('nrelay', data)
}
function encodeTLV(tlv: TLV): Uint8Array {
let entries: Uint8Array[] = []

View File

@@ -144,7 +144,7 @@ export function validateGroupAdminsEvent(event: Event): boolean {
}
// validate permissions
for (const [tag, value, label, ...permissions] of event.tags) {
for (const [tag, _value, _label, ...permissions] of event.tags) {
if (tag !== 'p') continue
for (let i = 0; i < permissions.length; i += 1) {

View File

@@ -254,7 +254,7 @@ export class BunkerSigner {
}
async nip44Decrypt(thirdPartyPubkey: string, ciphertext: string): Promise<string> {
return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, ciphertext])
return await this.sendRequest('nip44_decrypt', [thirdPartyPubkey, ciphertext])
}
}

View File

@@ -1,10 +1,15 @@
import { scrypt } from '@noble/hashes/scrypt'
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
import { concatBytes, randomBytes } from '@noble/hashes/utils'
import { Bech32MaxSize, encodeBytes } from './nip19.ts'
import { Bech32MaxSize, Ncryptsec, encodeBytes } from './nip19.ts'
import { bech32 } from '@scure/base'
export function encrypt(sec: Uint8Array, password: string, logn: number = 16, ksb: 0x00 | 0x01 | 0x02 = 0x02): string {
export function encrypt(
sec: Uint8Array,
password: string,
logn: number = 16,
ksb: 0x00 | 0x01 | 0x02 = 0x02,
): Ncryptsec {
let salt = randomBytes(16)
let n = 2 ** logn
let key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 })

View File

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

View File

@@ -205,3 +205,33 @@ test('get()', async () => {
expect(event).not.toBeNull()
expect(event).toHaveProperty('id', ids[0])
})
test('track relays when publishing', async () => {
let event1 = finalizeEvent(
{
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'hello',
},
generateSecretKey(),
)
let event2 = finalizeEvent(
{
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'hello',
},
generateSecretKey(),
)
pool.trackRelays = true
await Promise.all(pool.publish(relayURLs, event1))
expect(pool.seenOn.get(event1.id)).toBeDefined()
expect(Array.from(pool.seenOn.get(event1.id)!).map(r => r.url)).toEqual(expect.arrayContaining(relayURLs))
pool.trackRelays = false
await Promise.all(pool.publish(relayURLs, event2))
expect(pool.seenOn.get(event2.id)).toBeUndefined()
})