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
This commit is contained in:
parent
21433049b8
commit
ee76d69b4b
|
@ -1,6 +1,6 @@
|
||||||
FROM node:20
|
FROM node:20
|
||||||
|
|
||||||
RUN npm install typescript -g
|
RUN npm install typescript eslint prettier -g
|
||||||
|
|
||||||
# Install bun
|
# Install bun
|
||||||
RUN curl -fsSL https://bun.sh/install | bash
|
RUN curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
153
core.test.ts
153
core.test.ts
|
@ -1,6 +1,5 @@
|
||||||
import { test, expect } from 'bun:test'
|
import { test, expect } from 'bun:test'
|
||||||
|
import { NostrTypeGuard, sortEvents } from './core.ts'
|
||||||
import { sortEvents } from './core.ts'
|
|
||||||
|
|
||||||
test('sortEvents', () => {
|
test('sortEvents', () => {
|
||||||
const events = [
|
const events = [
|
||||||
|
@ -17,3 +16,153 @@ test('sortEvents', () => {
|
||||||
{ id: 'abc123', pubkey: 'key1', created_at: 1610000000, kind: 1, tags: [], content: 'Hello', sig: 'sig1' },
|
{ 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
core.ts
21
core.ts
|
@ -23,6 +23,27 @@ export type NostrEvent = Event
|
||||||
export type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'>
|
export type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'>
|
||||||
export type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>
|
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. */
|
/** An event whose signature has been verified. */
|
||||||
export interface VerifiedEvent extends Event {
|
export interface VerifiedEvent extends Event {
|
||||||
[verifiedSymbol]: true
|
[verifiedSymbol]: true
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { test, expect } from 'bun:test'
|
import { test, expect } from 'bun:test'
|
||||||
import fetch from 'node-fetch'
|
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 () => {
|
test('fetch nip05 profiles', async () => {
|
||||||
useFetchImplementation(fetch)
|
useFetchImplementation(fetch)
|
||||||
|
@ -18,3 +18,15 @@ test('fetch nip05 profiles', async () => {
|
||||||
expect(p3!.pubkey).toEqual('3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d')
|
expect(p3!.pubkey).toEqual('3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d')
|
||||||
expect(p3!.relays).toEqual(['wss://pyramid.fiatjaf.com', 'wss://nos.lol'])
|
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
nip05.ts
4
nip05.ts
|
@ -1,3 +1,4 @@
|
||||||
|
import { Nip05 } from './core.ts'
|
||||||
import { ProfilePointer } from './nip19.ts'
|
import { ProfilePointer } from './nip19.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,6 +9,7 @@ import { ProfilePointer } from './nip19.ts'
|
||||||
* - 2: domain
|
* - 2: domain
|
||||||
*/
|
*/
|
||||||
export const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$/
|
export const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$/
|
||||||
|
export const isNip05 = (value?: string | null): value is Nip05 => NIP05_REGEX.test(value || '')
|
||||||
|
|
||||||
var _fetch: any
|
var _fetch: any
|
||||||
|
|
||||||
|
@ -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)
|
let res = await queryProfile(nip05)
|
||||||
return res ? res.pubkey === pubkey : false
|
return res ? res.pubkey === pubkey : false
|
||||||
}
|
}
|
||||||
|
|
15
nip19.ts
15
nip19.ts
|
@ -2,6 +2,7 @@ import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils'
|
||||||
import { bech32 } from '@scure/base'
|
import { bech32 } from '@scure/base'
|
||||||
|
|
||||||
import { utf8Decoder, utf8Encoder } from './utils.ts'
|
import { utf8Decoder, utf8Encoder } from './utils.ts'
|
||||||
|
import { NAddr, NEvent, Note, NProfile, NPub, NRelay, NSec } from './core.ts'
|
||||||
|
|
||||||
export const Bech32MaxSize = 5000
|
export const Bech32MaxSize = 5000
|
||||||
|
|
||||||
|
@ -158,15 +159,15 @@ function parseTLV(data: Uint8Array): TLV {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nsecEncode(key: Uint8Array): `nsec1${string}` {
|
export function nsecEncode(key: Uint8Array): NSec {
|
||||||
return encodeBytes('nsec', key)
|
return encodeBytes('nsec', key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function npubEncode(hex: string): `npub1${string}` {
|
export function npubEncode(hex: string): NPub {
|
||||||
return encodeBytes('npub', hexToBytes(hex))
|
return encodeBytes('npub', hexToBytes(hex))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function noteEncode(hex: string): `note1${string}` {
|
export function noteEncode(hex: string): Note {
|
||||||
return encodeBytes('note', hexToBytes(hex))
|
return encodeBytes('note', hexToBytes(hex))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +180,7 @@ export function encodeBytes<Prefix extends string>(prefix: Prefix, bytes: Uint8A
|
||||||
return encodeBech32(prefix, bytes)
|
return encodeBech32(prefix, bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nprofileEncode(profile: ProfilePointer): `nprofile1${string}` {
|
export function nprofileEncode(profile: ProfilePointer): NProfile {
|
||||||
let data = encodeTLV({
|
let data = encodeTLV({
|
||||||
0: [hexToBytes(profile.pubkey)],
|
0: [hexToBytes(profile.pubkey)],
|
||||||
1: (profile.relays || []).map(url => utf8Encoder.encode(url)),
|
1: (profile.relays || []).map(url => utf8Encoder.encode(url)),
|
||||||
|
@ -187,7 +188,7 @@ export function nprofileEncode(profile: ProfilePointer): `nprofile1${string}` {
|
||||||
return encodeBech32('nprofile', data)
|
return encodeBech32('nprofile', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function neventEncode(event: EventPointer): `nevent1${string}` {
|
export function neventEncode(event: EventPointer): NEvent {
|
||||||
let kindArray
|
let kindArray
|
||||||
if (event.kind !== undefined) {
|
if (event.kind !== undefined) {
|
||||||
kindArray = integerToUint8Array(event.kind)
|
kindArray = integerToUint8Array(event.kind)
|
||||||
|
@ -203,7 +204,7 @@ export function neventEncode(event: EventPointer): `nevent1${string}` {
|
||||||
return encodeBech32('nevent', data)
|
return encodeBech32('nevent', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function naddrEncode(addr: AddressPointer): `naddr1${string}` {
|
export function naddrEncode(addr: AddressPointer): NAddr {
|
||||||
let kind = new ArrayBuffer(4)
|
let kind = new ArrayBuffer(4)
|
||||||
new DataView(kind).setUint32(0, addr.kind, false)
|
new DataView(kind).setUint32(0, addr.kind, false)
|
||||||
|
|
||||||
|
@ -216,7 +217,7 @@ export function naddrEncode(addr: AddressPointer): `naddr1${string}` {
|
||||||
return encodeBech32('naddr', data)
|
return encodeBech32('naddr', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nrelayEncode(url: string): `nrelay1${string}` {
|
export function nrelayEncode(url: string): NRelay {
|
||||||
let data = encodeTLV({
|
let data = encodeTLV({
|
||||||
0: [utf8Encoder.encode(url)],
|
0: [utf8Encoder.encode(url)],
|
||||||
})
|
})
|
||||||
|
|
8
nip49.ts
8
nip49.ts
|
@ -3,8 +3,14 @@ import { xchacha20poly1305 } from '@noble/ciphers/chacha'
|
||||||
import { concatBytes, randomBytes } from '@noble/hashes/utils'
|
import { concatBytes, randomBytes } from '@noble/hashes/utils'
|
||||||
import { Bech32MaxSize, encodeBytes } from './nip19.ts'
|
import { Bech32MaxSize, encodeBytes } from './nip19.ts'
|
||||||
import { bech32 } from '@scure/base'
|
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 salt = randomBytes(16)
|
||||||
let n = 2 ** logn
|
let n = 2 ** logn
|
||||||
let key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
let key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
||||||
|
|
Loading…
Reference in New Issue