Compare commits

..

7 Commits

Author SHA1 Message Date
fiatjaf
364c37cac5 fix autopublishing to npm. 2022-12-20 20:15:43 -03:00
fiatjaf
385cdb4ac6 README examples for nip05 and nip19. 2022-12-20 18:42:24 -03:00
fiatjaf
3f1025f551 nip05.queryProfile() and test. 2022-12-20 18:36:49 -03:00
fiatjaf
482c5affd4 add nip19. 2022-12-20 18:26:30 -03:00
fiatjaf
679ac0c133 fix standalone script URL. 2022-12-20 17:01:35 -03:00
fiatjaf
b96159ad36 better publishing built files. 2022-12-20 16:56:05 -03:00
fiatjaf
6dede4a688 use semisol relay that has our desired event on test. 2022-12-20 16:26:55 -03:00
16 changed files with 289 additions and 30 deletions

View File

@@ -1,8 +1,8 @@
name: publish npm package
on:
release:
types: [created]
push:
tags: [v*]
jobs:
publish-npm:
@@ -11,8 +11,11 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
node-version: 18
- run: yarn --ignore-engines
- run: node build.js
- run: yarn test
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
greater-version-only: true

View File

@@ -1,6 +1,8 @@
name: test every commit
on:
- push
push:
branches:
- master
jobs:
test:
@@ -11,5 +13,5 @@ jobs:
with:
node-version: 18
- run: yarn --ignore-engines
- run: node build.cjs
- run: node build.js
- run: yarn test

4
.gitignore vendored
View File

@@ -3,7 +3,5 @@ dist
yarn.lock
package-lock.json
.envrc
standalone
cjs
esm
lib
test.html

View File

@@ -112,6 +112,50 @@ pub.on('failed', reason => {
await relay.close()
```
### Querying profile data from a NIP-05 address
```js
import {nip05} from 'nostr-tools'
let profile = await nip05.queryProfile('jb55.com')
console.log(profile.pubkey)
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
console.log(profile.relays)
// prints: [wss://relay.damus.io]
// on nodejs, install node-fetch@2 and call this first:
nip05.useFetchImplementation(require('node-fetch'))
```
### Encoding and decoding NIP-19 codes
```js
import {nip19, generatePrivateKey, getPublicKey} from 'nostr-tools'
let sk = generatePrivateKey()
let nsec = nip19.nsecEncode(sk)
let {type, data} = nip19.decode(nsec)
assert(type === 'nsec')
assert(data === sk)
let pk = getPublicKey(generatePrivateKey())
let npub = nip19.npubEncode(pk)
let {type, data} = nip19.decode(npub)
assert(type === 'npub')
assert(data === pk)
let pk = getPublicKey(generatePrivateKey())
let relays = [
'wss://relay.nostr.example.mydomain.example.com',
'wss://nostr.banana.com'
]
let nprofile = nip19.nprofileEncode({pubkey: pk, relays})
let {type, data} = nip19.decode(nprofile)
assert(type === 'nprofile')
assert(data.pubkey === pk)
assert(data.relays.length === 2)
```
### Encrypting and decrypting direct messages
```js
@@ -153,7 +197,7 @@ Please consult the tests or [the source code](https://github.com/fiatjaf/nostr-t
### Using from the browser (if you don't want to use a bundler)
```html
<script src="https://unpkg.com/nostr-tools/standalone/index.js"></script>
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
<script>
window.NostrTools.generatePrivateKey('...') // and so on
</script>

View File

@@ -15,17 +15,27 @@ let common = {
}
esbuild
.build({...common, outdir: 'esm/', format: 'esm', packages: 'external'})
.build({
...common,
outfile: 'lib/nostr.esm.js',
format: 'esm',
packages: 'external'
})
.then(() => console.log('esm build success.'))
esbuild
.build({...common, outdir: 'cjs/', format: 'cjs', packages: 'external'})
.build({
...common,
outfile: 'lib/nostr.cjs.js',
format: 'cjs',
packages: 'external'
})
.then(() => console.log('cjs build success.'))
esbuild
.build({
...common,
outdir: 'standalone/',
outfile: 'lib/nostr.bundle.js',
format: 'iife',
globalName: 'NostrTools',
define: {

View File

@@ -6,7 +6,7 @@ const {
signEvent,
getEventHash,
getPublicKey
} = require('./cjs')
} = require('./lib/nostr.cjs')
const event = {
id: 'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027',

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
const {matchFilters} = require('./cjs')
const {matchFilters} = require('./lib/nostr.cjs')
test('test if filters match', () => {
;[

View File

@@ -6,3 +6,4 @@ export * from './filter'
export * as nip04 from './nip04'
export * as nip05 from './nip05'
export * as nip06 from './nip06'
export * as nip19 from './nip19'

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
const {generatePrivateKey, getPublicKey} = require('./cjs')
const {generatePrivateKey, getPublicKey} = require('./lib/nostr.cjs')
test('test private key generation', () => {
expect(generatePrivateKey()).toMatch(/[a-f0-9]{64}/)

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
const {nip04, getPublicKey, generatePrivateKey} = require('./cjs')
const {nip04, getPublicKey, generatePrivateKey} = require('./lib/nostr.cjs')
test('encrypt and decrypt message', () => {
let sk1 = generatePrivateKey()

20
nip05.test.js Normal file
View File

@@ -0,0 +1,20 @@
/* eslint-env jest */
const fetch = require('node-fetch')
const {nip05} = require('./lib/nostr.cjs')
test('fetch nip05 profiles', async () => {
nip05.useFetchImplementation(fetch)
let p1 = await nip05.queryProfile('jb55.com')
expect(p1.pubkey).toEqual(
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
)
expect(p1.relays).toEqual(['wss://relay.damus.io'])
let p2 = await nip05.queryProfile('jb55@jb55.com')
expect(p2.pubkey).toEqual(
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
)
expect(p2.relays).toEqual(['wss://relay.damus.io'])
})

View File

@@ -1,3 +1,5 @@
import {ProfilePointer} from './nip19'
var _fetch = fetch
export function useFetchImplementation(fetchImplementation: any) {
@@ -19,13 +21,28 @@ export async function searchDomain(
}
}
export async function queryName(fullname: string): Promise<string> {
export async function queryProfile(
fullname: string
): Promise<ProfilePointer | null> {
let [name, domain] = fullname.split('@')
if (!domain) throw new Error('invalid identifier, must contain an @')
if (!domain) {
// if there is no @, it is because it is just a domain, so assume the name is "_"
domain = name
name = '_'
}
let res = await (
await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`)
).json()
return res.names && res.names[name]
if (!res?.names?.[name]) return null
let pubkey = res.names[name] as string
let relays = (res.relays?.[pubkey] || []) as string[] // nip35
return {
pubkey,
relays
}
}

36
nip19.test.js Normal file
View File

@@ -0,0 +1,36 @@
/* eslint-env jest */
const {nip19, generatePrivateKey, getPublicKey} = require('./lib/nostr.cjs')
test('encode and decode nsec', () => {
let sk = generatePrivateKey()
let nsec = nip19.nsecEncode(sk)
expect(nsec).toMatch(/nsec1\w+/)
let {type, data} = nip19.decode(nsec)
expect(type).toEqual('nsec')
expect(data).toEqual(sk)
})
test('encode and decode npub', () => {
let pk = getPublicKey(generatePrivateKey())
let npub = nip19.npubEncode(pk)
expect(npub).toMatch(/npub1\w+/)
let {type, data} = nip19.decode(npub)
expect(type).toEqual('npub')
expect(data).toEqual(pk)
})
test('encode and decode nprofile', () => {
let pk = getPublicKey(generatePrivateKey())
let relays = [
'wss://relay.nostr.example.mydomain.example.com',
'wss://nostr.banana.com'
]
let nprofile = nip19.nprofileEncode({pubkey: pk, relays})
expect(nprofile).toMatch(/nprofile1\w+/)
let {type, data} = nip19.decode(nprofile)
expect(type).toEqual('nprofile')
expect(data.pubkey).toEqual(pk)
expect(data.relays).toContain(relays[0])
expect(data.relays).toContain(relays[1])
})

126
nip19.ts Normal file
View File

@@ -0,0 +1,126 @@
import * as secp256k1 from '@noble/secp256k1'
import {bech32} from 'bech32'
export type ProfilePointer = {
pubkey: string // hex
relays?: string[]
}
export type EventPointer = {
id: string // hex
relays?: string[]
}
let utf8Decoder = new TextDecoder('utf-8')
let utf8Encoder = new TextEncoder()
export function decode(nip19: string): {
type: string
data: ProfilePointer | EventPointer | string
} {
let {prefix, words} = bech32.decode(nip19, 1000)
let data = new Uint8Array(bech32.fromWords(words))
if (prefix === 'nprofile') {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
return {
type: 'nprofile',
data: {
pubkey: secp256k1.utils.bytesToHex(tlv[0][0]),
relays: tlv[1].map(d => utf8Decoder.decode(d))
}
}
}
if (prefix === 'nevent') {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nevent')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
return {
type: 'nevent',
data: {
id: secp256k1.utils.bytesToHex(tlv[0][0]),
relays: tlv[1].map(d => utf8Decoder.decode(d))
}
}
}
if (prefix === 'nsec' || prefix === 'npub' || prefix === 'note') {
return {type: prefix, data: secp256k1.utils.bytesToHex(data)}
}
throw new Error(`unknown prefix ${prefix}`)
}
type TLV = {[t: number]: Uint8Array[]}
function parseTLV(data: Uint8Array): TLV {
let result: TLV = {}
let rest = data
while (rest.length > 0) {
let t = rest[0]
let l = rest[1]
let v = rest.slice(2, 2 + l)
rest = rest.slice(2 + l)
if (v.length < l) continue
result[t] = result[t] || []
result[t].push(v)
}
return result
}
export function nsecEncode(hex: string): string {
return encodeBytes('nsec', hex)
}
export function npubEncode(hex: string): string {
return encodeBytes('npub', hex)
}
export function noteEncode(hex: string): string {
return encodeBytes('note', hex)
}
function encodeBytes(prefix: string, hex: string): string {
let data = secp256k1.utils.hexToBytes(hex)
let words = bech32.toWords(data)
return bech32.encode(prefix, words, 1000)
}
export function nprofileEncode(profile: ProfilePointer): string {
let data = encodeTLV({
0: [secp256k1.utils.hexToBytes(profile.pubkey)],
1: (profile.relays || []).map(url => utf8Encoder.encode(url))
})
let words = bech32.toWords(data)
return bech32.encode('nprofile', words, 1000)
}
export function neventEncode(event: EventPointer): string {
let data = encodeTLV({
0: [secp256k1.utils.hexToBytes(event.id)],
1: (event.relays || []).map(url => utf8Encoder.encode(url))
})
let words = bech32.toWords(data)
return bech32.encode('nevent', words, 1000)
}
function encodeTLV(tlv: TLV): Uint8Array {
let entries: Uint8Array[] = []
Object.entries(tlv).forEach(([t, vs]) => {
vs.forEach(v => {
let entry = new Uint8Array(v.length + 2)
entry.set([parseInt(t)], 0)
entry.set([v.length], 1)
entry.set(v, 2)
entries.push(entry)
})
})
return secp256k1.utils.concatBytes(...entries)
}

View File

@@ -1,18 +1,19 @@
{
"name": "nostr-tools",
"version": "1.0.0-alpha",
"version": "1.0.0-beta",
"description": "Tools for making a Nostr client.",
"repository": {
"type": "git",
"url": "https://github.com/fiatjaf/nostr-tools.git"
},
"main": "cjs/index.js",
"module": "esm/index.js",
"main": "lib/nostr.cjs.js",
"module": "lib/nostr.esm.js",
"dependencies": {
"@noble/hashes": "^0.5.7",
"@noble/secp256k1": "^1.7.0",
"@scure/bip32": "^1.1.1",
"@scure/bip39": "^1.1.0",
"bech32": "^2.0.0",
"browserify-cipher": ">=1",
"buffer": "^6.0.3",
"websocket-polyfill": "^0.0.3"
@@ -35,13 +36,14 @@
"esm-loader-typescript": "^1.0.1",
"events": "^3.3.0",
"jest": "^29.3.1",
"node-fetch": "2",
"ts-jest": "^29.0.3",
"tsd": "^0.22.0",
"typescript": "^4.9.4"
},
"scripts": {
"build": "node build.cjs",
"pretest": "node build.cjs",
"build": "node build.js",
"pretest": "node build.js",
"test": "jest"
}
}

View File

@@ -6,10 +6,10 @@ const {
getPublicKey,
getEventHash,
signEvent
} = require('./cjs')
} = require('./lib/nostr.cjs')
describe('relay interaction', () => {
let relay = relayInit('wss://nostr-pub.wellorder.net/')
let relay = relayInit('wss://nostr-pub.semisol.dev/')
beforeAll(() => {
relay.connect()