Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

including nostr specialized types #409

Merged
merged 13 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
153 changes: 151 additions & 2 deletions core.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { test, expect } from 'bun:test'

import { sortEvents } from './core.ts'
import { NostrTypeGuard, sortEvents } from './core.ts'

test('sortEvents', () => {
const events = [
Expand All @@ -17,3 +16,153 @@ test('sortEvents', () => {
{ id: 'abc123', pubkey: 'key1', created_at: 1610000000, kind: 1, tags: [], content: 'Hello', sig: 'sig1' },
])
})

test('NostrTypeGuard isNProfile', () => {
const is = NostrTypeGuard.isNProfile('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg')

expect(is).toBeTrue()
})

test('NostrTypeGuard isNProfile invalid nprofile', () => {
const is = NostrTypeGuard.isNProfile('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxãg')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNProfile with invalid nprofile', () => {
const is = NostrTypeGuard.isNProfile('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNRelay', () => {
const is = NostrTypeGuard.isNRelay('nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueq4r295t')

expect(is).toBeTrue()
})

test('NostrTypeGuard isNRelay with invalid nrelay', () => {
const is = NostrTypeGuard.isNRelay('nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueã4r295t')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNRelay with invalid nrelay', () => {
const is = NostrTypeGuard.isNRelay(
'nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9',
)

expect(is).toBeFalse()
})

test('NostrTypeGuard isNEvent', () => {
const is = NostrTypeGuard.isNEvent(
'nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9',
)

expect(is).toBeTrue()
})

test('NostrTypeGuard isNEvent with invalid nevent', () => {
const is = NostrTypeGuard.isNEvent(
'nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8ãrnc9',
)

expect(is).toBeFalse()
})

test('NostrTypeGuard isNEvent with invalid nevent', () => {
const is = NostrTypeGuard.isNEvent('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNAddr', () => {
const is = NostrTypeGuard.isNAddr(
'naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld',
)

expect(is).toBeTrue()
})

test('NostrTypeGuard isNAddr with invalid nadress', () => {
const is = NostrTypeGuard.isNAddr('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNSec', () => {
const is = NostrTypeGuard.isNSec('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')

expect(is).toBeTrue()
})

test('NostrTypeGuard isNSec with invalid nsec', () => {
const is = NostrTypeGuard.isNSec('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juã')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNSec with invalid nsec', () => {
const is = NostrTypeGuard.isNSec('nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNPub', () => {
const is = NostrTypeGuard.isNPub('npub1jz5mdljkmffmqjshpyjgqgrhdkuxd9ztzasv8xeh5q92fv33sjgqy4pats')

expect(is).toBeTrue()
})

test('NostrTypeGuard isNPub with invalid npub', () => {
const is = NostrTypeGuard.isNPub('npub1jz5mdljkmffmqjshpyjgqgrhdkuxd9ztzãsv8xeh5q92fv33sjgqy4pats')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNPub with invalid npub', () => {
const is = NostrTypeGuard.isNPub('nsec1lqw6zqyanj9mz8gwhdam6tqge42vptz4zg93qsfej440xm5h5esqya0juv')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNote', () => {
const is = NostrTypeGuard.isNote('note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky')

expect(is).toBeTrue()
})

test('NostrTypeGuard isNote with invalid note', () => {
const is = NostrTypeGuard.isNote('note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sçlreky')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNote with invalid note', () => {
const is = NostrTypeGuard.isNote('npub1jz5mdljkmffmqjshpyjgqgrhdkuxd9ztzasv8xeh5q92fv33sjgqy4pats')

expect(is).toBeFalse()
})

test('NostrTypeGuard isNcryptsec', () => {
const is = NostrTypeGuard.isNcryptsec(
'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p',
)

expect(is).toBeTrue()
})

test('NostrTypeGuard isNcryptsec with invalid ncrytpsec', () => {
const is = NostrTypeGuard.isNcryptsec(
'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsã8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p',
)

expect(is).toBeFalse()
})

test('NostrTypeGuard isNcryptsec with invalid ncrytpsec', () => {
const is = NostrTypeGuard.isNcryptsec('note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sçlreky')

expect(is).toBeFalse()
})
21 changes: 21 additions & 0 deletions core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ export type NostrEvent = Event
export type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'>
export type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>

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 type Nip05 = `${string}@${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 Note => /^ncryptsec1[a-z\d]+$/.test(value || ''),
}

/** An event whose signature has been verified. */
export interface VerifiedEvent extends Event {
[verifiedSymbol]: true
Expand Down
14 changes: 13 additions & 1 deletion nip05.test.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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()
})
4 changes: 3 additions & 1 deletion nip05.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Nip05 } from './core.ts'
import { ProfilePointer } from './nip19.ts'

/**
Expand All @@ -8,6 +9,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

Expand Down Expand Up @@ -47,7 +49,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
}
15 changes: 8 additions & 7 deletions nip19.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils'
import { bech32 } from '@scure/base'

import { utf8Decoder, utf8Encoder } from './utils.ts'
import { NAddr, NEvent, Note, NProfile, NPub, NRelay, NSec } from './core.ts'

export const Bech32MaxSize = 5000

Expand Down Expand Up @@ -158,15 +159,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))
}

Expand All @@ -179,15 +180,15 @@ 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)),
})
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)
Expand All @@ -203,7 +204,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)

Expand All @@ -216,7 +217,7 @@ export function naddrEncode(addr: AddressPointer): `naddr1${string}` {
return encodeBech32('naddr', data)
}

export function nrelayEncode(url: string): `nrelay1${string}` {
export function nrelayEncode(url: string): NRelay {
let data = encodeTLV({
0: [utf8Encoder.encode(url)],
})
Expand Down
8 changes: 7 additions & 1 deletion nip49.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { xchacha20poly1305 } from '@noble/ciphers/chacha'
import { concatBytes, randomBytes } from '@noble/hashes/utils'
import { Bech32MaxSize, encodeBytes } from './nip19.ts'
import { bech32 } from '@scure/base'
import { Ncryptsec } from './core.ts'

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 })
Expand Down
Loading