nostr-tools 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.json ADDED
@@ -0,0 +1,142 @@
1
+ {
2
+ "parserOptions": {
3
+ "ecmaVersion": 9,
4
+ "ecmaFeatures": {
5
+ "jsx": true
6
+ },
7
+ "sourceType": "module",
8
+ "allowImportExportEverywhere": false
9
+ },
10
+
11
+ "env": {
12
+ "es6": true,
13
+ "node": true
14
+ },
15
+
16
+ "plugins": [
17
+ "babel"
18
+ ],
19
+
20
+ "globals": {
21
+ "document": false,
22
+ "navigator": false,
23
+ "window": false,
24
+ "location": false,
25
+ "URL": false,
26
+ "URLSearchParams": false,
27
+ "fetch": false,
28
+ "EventSource": false,
29
+ "localStorage": false,
30
+ "sessionStorage": false
31
+ },
32
+
33
+ "rules": {
34
+ "accessor-pairs": 2,
35
+ "arrow-spacing": [2, { "before": true, "after": true }],
36
+ "block-spacing": [2, "always"],
37
+ "brace-style": [2, "1tbs", { "allowSingleLine": true }],
38
+ "comma-dangle": 0,
39
+ "comma-spacing": [2, { "before": false, "after": true }],
40
+ "comma-style": [2, "last"],
41
+ "constructor-super": 2,
42
+ "curly": [0, "multi-line"],
43
+ "dot-location": [2, "property"],
44
+ "eol-last": 2,
45
+ "eqeqeq": [2, "allow-null"],
46
+ "generator-star-spacing": [2, { "before": true, "after": true }],
47
+ "handle-callback-err": [2, "^(err|error)$" ],
48
+ "indent": 0,
49
+ "jsx-quotes": [2, "prefer-double"],
50
+ "key-spacing": [2, { "beforeColon": false, "afterColon": true }],
51
+ "keyword-spacing": [2, { "before": true, "after": true }],
52
+ "new-cap": 0,
53
+ "new-parens": 0,
54
+ "no-array-constructor": 2,
55
+ "no-caller": 2,
56
+ "no-class-assign": 2,
57
+ "no-cond-assign": 2,
58
+ "no-const-assign": 2,
59
+ "no-control-regex": 0,
60
+ "no-debugger": 0,
61
+ "no-delete-var": 2,
62
+ "no-dupe-args": 2,
63
+ "no-dupe-class-members": 2,
64
+ "no-dupe-keys": 2,
65
+ "no-duplicate-case": 2,
66
+ "no-empty-character-class": 2,
67
+ "no-empty-pattern": 2,
68
+ "no-eval": 0,
69
+ "no-ex-assign": 2,
70
+ "no-extend-native": 2,
71
+ "no-extra-bind": 2,
72
+ "no-extra-boolean-cast": 2,
73
+ "no-extra-parens": [2, "functions"],
74
+ "no-fallthrough": 2,
75
+ "no-floating-decimal": 2,
76
+ "no-func-assign": 2,
77
+ "no-implied-eval": 2,
78
+ "no-inner-declarations": [0, "functions"],
79
+ "no-invalid-regexp": 2,
80
+ "no-irregular-whitespace": 2,
81
+ "no-iterator": 2,
82
+ "no-label-var": 2,
83
+ "no-labels": [2, { "allowLoop": false, "allowSwitch": false }],
84
+ "no-lone-blocks": 2,
85
+ "no-mixed-spaces-and-tabs": 2,
86
+ "no-multi-spaces": 2,
87
+ "no-multi-str": 2,
88
+ "no-multiple-empty-lines": [2, { "max": 2 }],
89
+ "no-native-reassign": 2,
90
+ "no-negated-in-lhs": 2,
91
+ "no-new": 0,
92
+ "no-new-func": 2,
93
+ "no-new-object": 2,
94
+ "no-new-require": 2,
95
+ "no-new-symbol": 2,
96
+ "no-new-wrappers": 2,
97
+ "no-obj-calls": 2,
98
+ "no-octal": 2,
99
+ "no-octal-escape": 2,
100
+ "no-path-concat": 0,
101
+ "no-proto": 2,
102
+ "no-redeclare": 2,
103
+ "no-regex-spaces": 2,
104
+ "no-return-assign": 0,
105
+ "no-self-assign": 2,
106
+ "no-self-compare": 2,
107
+ "no-sequences": 2,
108
+ "no-shadow-restricted-names": 2,
109
+ "no-spaced-func": 2,
110
+ "no-sparse-arrays": 2,
111
+ "no-this-before-super": 2,
112
+ "no-throw-literal": 2,
113
+ "no-trailing-spaces": 2,
114
+ "no-undef": 2,
115
+ "no-undef-init": 2,
116
+ "no-unexpected-multiline": 2,
117
+ "no-unneeded-ternary": [2, { "defaultAssignment": false }],
118
+ "no-unreachable": 2,
119
+ "no-unused-vars": [2, { "vars": "local", "args": "none", "varsIgnorePattern": "^_"}],
120
+ "no-useless-call": 2,
121
+ "no-useless-constructor": 2,
122
+ "no-with": 2,
123
+ "one-var": [0, { "initialized": "never" }],
124
+ "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
125
+ "padded-blocks": [2, "never"],
126
+ "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
127
+ "semi": [2, "never"],
128
+ "semi-spacing": [2, { "before": false, "after": true }],
129
+ "space-before-blocks": [2, "always"],
130
+ "space-before-function-paren": 0,
131
+ "space-in-parens": [2, "never"],
132
+ "space-infix-ops": 2,
133
+ "space-unary-ops": [2, { "words": true, "nonwords": false }],
134
+ "spaced-comment": 0,
135
+ "template-curly-spacing": [2, "never"],
136
+ "use-isnan": 2,
137
+ "valid-typeof": 2,
138
+ "wrap-iife": [2, "any"],
139
+ "yield-star-spacing": [2, "both"],
140
+ "yoda": [0]
141
+ }
142
+ }
@@ -0,0 +1,10 @@
1
+ semi: false
2
+ arrowParens: avoid
3
+ insertPragma: false
4
+ printWidth: 80
5
+ proseWrap: preserve
6
+ singleQuote: true
7
+ trailingComma: none
8
+ useTabs: false
9
+ jsxBracketSameLine: false
10
+ bracketSpacing: false
package/event.js ADDED
@@ -0,0 +1,43 @@
1
+ import Buffer from 'buffer'
2
+ import * as secp256k1 from '@noble/secp256k1'
3
+
4
+ import {sha256} from './utils'
5
+
6
+ export function getBlankEvent() {
7
+ return {
8
+ kind: 255,
9
+ pubkey: null,
10
+ content: '',
11
+ tags: [],
12
+ created_at: 0
13
+ }
14
+ }
15
+
16
+ export function serializeEvent(evt) {
17
+ return JSON.stringify([
18
+ 0,
19
+ evt.pubkey,
20
+ evt.created_at,
21
+ evt.kind,
22
+ evt.tags || [],
23
+ evt.content
24
+ ])
25
+ }
26
+
27
+ export async function getEventHash(event) {
28
+ let eventHash = await sha256(Buffer.from(serializeEvent(event)))
29
+ return Buffer.from(eventHash).toString('hex')
30
+ }
31
+
32
+ export async function verifySignature(event) {
33
+ return await secp256k1.schnorr.verify(
34
+ event.sig,
35
+ await getEventHash(event),
36
+ event.pubkey
37
+ )
38
+ }
39
+
40
+ export async function signEvent(event, key) {
41
+ let eventHash = await getEventHash(event)
42
+ return await secp256k1.schnorr.sign(eventHash, key)
43
+ }
package/index.js ADDED
@@ -0,0 +1,25 @@
1
+ import {relayConnect} from './relay'
2
+ import {relayPool} from './pool'
3
+ import {
4
+ getBlankEvent,
5
+ signEvent,
6
+ verifySignature,
7
+ serializeEvent,
8
+ getEventHash
9
+ } from './event'
10
+ import {makeRandom32, sha256, getPublicKey} from './utils'
11
+
12
+ export {
13
+ relayConnect,
14
+ relayPool,
15
+ signEvent,
16
+ verifySignature,
17
+ serializeEvent,
18
+ getEventHash,
19
+ makeRandom32,
20
+ sha256,
21
+ getPublicKey,
22
+ getBlankEvent
23
+ }
24
+ export * from './nip04'
25
+ export * from './nip05'
package/nip04.js ADDED
@@ -0,0 +1,37 @@
1
+ import Buffer from 'buffer'
2
+ import * as secp256k1 from '@noble/secp256k1'
3
+
4
+ export function encrypt(privkey, pubkey, text) {
5
+ const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
6
+ const normalizedKey = getOnlyXFromFullSharedSecret(key)
7
+
8
+ let iv = crypto.randomFillSync(new Uint8Array(16))
9
+ var cipher = crypto.createCipheriv(
10
+ 'aes-256-cbc',
11
+ Buffer.from(normalizedKey, 'hex'),
12
+ iv
13
+ )
14
+ let encryptedMessage = cipher.update(text, 'utf8', 'base64')
15
+ encryptedMessage += cipher.final('base64')
16
+
17
+ return [encryptedMessage, Buffer.from(iv.buffer).toString('base64')]
18
+ }
19
+
20
+ export function decrypt(privkey, pubkey, ciphertext, iv) {
21
+ const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
22
+ const normalizedKey = getOnlyXFromFullSharedSecret(key)
23
+
24
+ var decipher = crypto.createDecipheriv(
25
+ 'aes-256-cbc',
26
+ Buffer.from(normalizedKey, 'hex'),
27
+ Buffer.from(iv, 'base64')
28
+ )
29
+ let decryptedMessage = decipher.update(ciphertext, 'base64')
30
+ decryptedMessage += decipher.final('utf8')
31
+
32
+ return decryptedMessage
33
+ }
34
+
35
+ function getOnlyXFromFullSharedSecret(fullSharedSecretCoordinates) {
36
+ return fullSharedSecretCoordinates.substr(2, 64)
37
+ }
package/nip05.js ADDED
@@ -0,0 +1,52 @@
1
+ import Buffer from 'buffer'
2
+ import dnsPacket from 'dns-packet'
3
+
4
+ const dohProviders = [
5
+ 'cloudflare-dns.com',
6
+ 'fi.doh.dns.snopyta.org',
7
+ 'basic.bravedns.com',
8
+ 'hydra.plan9-ns1.com',
9
+ 'doh.pl.ahadns.net',
10
+ 'dns.flatuslifir.is',
11
+ 'doh.dns.sb',
12
+ 'doh.li'
13
+ ]
14
+
15
+ let counter = 0
16
+
17
+ export async function keyFromDomain(domain) {
18
+ let host = dohProviders[counter % dohProviders.length]
19
+
20
+ let buf = dnsPacket.encode({
21
+ type: 'query',
22
+ id: Math.floor(Math.random() * 65534),
23
+ flags: dnsPacket.RECURSION_DESIRED,
24
+ questions: [
25
+ {
26
+ type: 'TXT',
27
+ name: `_nostrkey.${domain}`
28
+ }
29
+ ]
30
+ })
31
+
32
+ let fetching = fetch(`https://${host}/dns-query`, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/dns-message',
36
+ 'Content-Length': Buffer.byteLength(buf)
37
+ },
38
+ body: buf
39
+ })
40
+
41
+ counter++
42
+
43
+ try {
44
+ let response = Buffer.from(await (await fetching).arrayBuffer())
45
+ let {answers} = dnsPacket.decode(response)
46
+ if (answers.length === 0) return null
47
+ return Buffer.from(answers[0].data[0]).toString()
48
+ } catch (err) {
49
+ console.log(`error querying DNS for ${domain} on ${host}`, err)
50
+ return null
51
+ }
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-tools",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Tools for making a Nostr client.",
5
5
  "main": "dist/nostr-tools.esm.min.js",
6
6
  "module": "dist/nostr-tools.esm.min.js",
@@ -30,9 +30,6 @@
30
30
  "devDependencies": {
31
31
  "rollup": "^2.61.1"
32
32
  },
33
- "files": [
34
- "dist"
35
- ],
36
33
  "scripts": {
37
34
  "prepublish": "rollup -c"
38
35
  }
package/pool.js ADDED
@@ -0,0 +1,122 @@
1
+ import {getEventHash, signEvent} from './event'
2
+ import {relayConnect, normalizeRelayURL} from './relay'
3
+
4
+ export function relayPool(globalPrivateKey) {
5
+ const relays = {}
6
+ const globalSub = []
7
+ const noticeCallbacks = []
8
+
9
+ function propagateNotice(notice, relayURL) {
10
+ for (let i = 0; i < noticeCallbacks.length; i++) {
11
+ let {relay} = relays[relayURL]
12
+ noticeCallbacks[i](notice, relay)
13
+ }
14
+ }
15
+
16
+ const activeSubscriptions = {}
17
+
18
+ const sub = ({cb, filter}, id = Math.random().toString().slice(2)) => {
19
+ const subControllers = Object.fromEntries(
20
+ Object.values(relays)
21
+ .filter(({policy}) => policy.read)
22
+ .map(({relay}) => [
23
+ relay.url,
24
+ relay.sub({filter, cb: event => cb(event, relay.url)})
25
+ ])
26
+ )
27
+
28
+ const activeCallback = cb
29
+ const activeFilters = filter
30
+
31
+ activeSubscriptions[id] = {
32
+ sub: ({cb = activeCallback, filter = activeFilters}) =>
33
+ Object.entries(subControllers).map(([relayURL, sub]) => [
34
+ relayURL,
35
+ sub.sub({cb, filter}, id)
36
+ ]),
37
+ addRelay: relay => {
38
+ subControllers[relay.url] = relay.sub({cb, filter})
39
+ },
40
+ removeRelay: relayURL => {
41
+ if (relayURL in subControllers) {
42
+ subControllers[relayURL].unsub()
43
+ if (Object.keys(subControllers).length === 0) unsub()
44
+ }
45
+ },
46
+ unsub: () => {
47
+ Object.values(subControllers).forEach(sub => sub.unsub())
48
+ delete activeSubscriptions[id]
49
+ }
50
+ }
51
+
52
+ return activeSubscriptions[id]
53
+ }
54
+
55
+ return {
56
+ sub,
57
+ relays,
58
+ setPrivateKey(privateKey) {
59
+ globalPrivateKey = privateKey
60
+ },
61
+ async addRelay(url, policy = {read: true, write: true}) {
62
+ let relayURL = normalizeRelayURL(url)
63
+ if (relayURL in relays) return
64
+
65
+ let relay = await relayConnect(url, notice => {
66
+ propagateNotice(notice, relayURL)
67
+ })
68
+ relays[relayURL] = {relay, policy}
69
+
70
+ Object.values(activeSubscriptions).forEach(subscription =>
71
+ subscription.addRelay(relay)
72
+ )
73
+
74
+ return relay
75
+ },
76
+ removeRelay(url) {
77
+ let relayURL = normalizeRelayURL(url)
78
+ let {relay} = relays[relayURL]
79
+ if (!relay) return
80
+ Object.values(activeSubscriptions).forEach(subscription =>
81
+ subscription.removeRelay(relay)
82
+ )
83
+ relay.close()
84
+ delete relays[relayURL]
85
+ },
86
+ onNotice(cb) {
87
+ noticeCallbacks.push(cb)
88
+ },
89
+ offNotice(cb) {
90
+ let index = noticeCallbacks.indexOf(cb)
91
+ if (index !== -1) noticeCallbacks.splice(index, 1)
92
+ },
93
+ async publish(event, statusCallback = (status, relayURL) => {}) {
94
+ if (!event.sig) {
95
+ event.tags = event.tags || []
96
+
97
+ if (globalPrivateKey) {
98
+ event.id = await getEventHash(event)
99
+ event.sig = await signEvent(event, globalPrivateKey)
100
+ } else {
101
+ throw new Error(
102
+ "can't publish unsigned event. either sign this event beforehand or pass a private key while initializing this relay pool so it can be signed automatically."
103
+ )
104
+ }
105
+ }
106
+
107
+ Object.values(relays)
108
+ .filter(({policy}) => policy.write)
109
+ .map(async ({relay}) => {
110
+ try {
111
+ await relay.publish(event, status =>
112
+ statusCallback(status, relay.url)
113
+ )
114
+ } catch (err) {
115
+ statusCallback(-1, relay.url)
116
+ }
117
+ })
118
+
119
+ return event
120
+ }
121
+ }
122
+ }
package/relay.js ADDED
@@ -0,0 +1,160 @@
1
+ import 'websocket-polyfill'
2
+
3
+ import {verifySignature} from './event'
4
+
5
+ export function normalizeRelayURL(url) {
6
+ let [host, ...qs] = url.split('?')
7
+ if (host.slice(0, 4) === 'http') host = 'ws' + host.slice(4)
8
+ if (host.slice(0, 2) !== 'ws') host = 'wss://' + host
9
+ if (host.length && host[host.length - 1] === '/') host = host.slice(0, -1)
10
+ return [host, ...qs].join('?')
11
+ }
12
+
13
+ export function relayConnect(url, onNotice) {
14
+ url = normalizeRelayURL(url)
15
+
16
+ var ws, resolveOpen, untilOpen
17
+ var openSubs = {}
18
+ let attemptNumber = 1
19
+ let nextAttemptSeconds = 1
20
+
21
+ function resetOpenState() {
22
+ untilOpen = new Promise(resolve => {
23
+ resolveOpen = resolve
24
+ })
25
+ }
26
+
27
+ var channels = {}
28
+
29
+ function connect() {
30
+ ws = new WebSocket(url)
31
+
32
+ ws.onopen = () => {
33
+ console.log('connected to', url)
34
+ resolveOpen()
35
+
36
+ // restablish old subscriptions
37
+ for (let channel in openSubs) {
38
+ let filters = openSubs[channel]
39
+ let cb = channels[channel]
40
+ sub({cb, filter: filters}, channel)
41
+ }
42
+ }
43
+ ws.onerror = () => {
44
+ console.log('error connecting to relay', url)
45
+ }
46
+ ws.onclose = () => {
47
+ resetOpenState()
48
+ attemptNumber++
49
+ nextAttemptSeconds += attemptNumber
50
+ console.log(
51
+ `relay ${url} connection closed. reconnecting in ${nextAttemptSeconds} seconds.`
52
+ )
53
+ setTimeout(async () => {
54
+ try {
55
+ connect()
56
+ } catch (err) {}
57
+ }, nextAttemptSeconds * 1000)
58
+ }
59
+
60
+ ws.onmessage = async e => {
61
+ var data
62
+ try {
63
+ data = JSON.parse(e.data)
64
+ } catch (err) {
65
+ data = e.data
66
+ }
67
+
68
+ if (data.length > 1) {
69
+ if (data[0] === 'NOTICE') {
70
+ if (data.length < 2) return
71
+
72
+ console.log('message from relay ' + url + ': ' + data[1])
73
+ onNotice(data[1])
74
+ return
75
+ }
76
+
77
+ if (data[0] === 'EVENT') {
78
+ if (data.length < 3) return
79
+
80
+ let channel = data[1]
81
+ let event = data[2]
82
+
83
+ if (await verifySignature(event)) {
84
+ if (channels[channel]) {
85
+ channels[channel](event)
86
+ }
87
+ } else {
88
+ console.warn('got event with invalid signature from ' + url, event)
89
+ }
90
+ return
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ resetOpenState()
97
+
98
+ try {
99
+ connect()
100
+ } catch (err) {}
101
+
102
+ async function trySend(params) {
103
+ let msg = JSON.stringify(params)
104
+
105
+ await untilOpen
106
+ ws.send(msg)
107
+ }
108
+
109
+ const sub = ({cb, filter}, channel = Math.random().toString().slice(2)) => {
110
+ var filters = []
111
+ if (Array.isArray(filter)) {
112
+ filters = filter
113
+ } else {
114
+ filters.push(filter)
115
+ }
116
+
117
+ trySend(['REQ', channel, ...filters])
118
+ channels[channel] = cb
119
+ openSubs[channel] = filters
120
+
121
+ const activeCallback = cb
122
+ const activeFilters = filters
123
+
124
+ return {
125
+ sub: ({cb = activeCallback, filter = activeFilters}) =>
126
+ sub({cb, filter}, channel),
127
+ unsub: () => {
128
+ delete openSubs[channel]
129
+ delete channels[channel]
130
+ trySend(['CLOSE', channel])
131
+ }
132
+ }
133
+ }
134
+
135
+ return {
136
+ url,
137
+ sub,
138
+ async publish(event, statusCallback = status => {}) {
139
+ try {
140
+ await trySend(['EVENT', event])
141
+ statusCallback(0)
142
+ let {unsub} = relay.sub({
143
+ cb: () => {
144
+ statusCallback(1)
145
+ },
146
+ filter: {id: event.id}
147
+ })
148
+ setTimeout(unsub, 5000)
149
+ } catch (err) {
150
+ statusCallback(-1)
151
+ }
152
+ },
153
+ close() {
154
+ ws.close()
155
+ },
156
+ get status() {
157
+ return ws.readyState
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,16 @@
1
+ import pkg from './package.json'
2
+
3
+ export default {
4
+ input: 'index.js',
5
+ output: [
6
+ {
7
+ name: 'nostrtools',
8
+ file: pkg.browser,
9
+ format: 'umd'
10
+ },
11
+ {
12
+ file: pkg.module,
13
+ format: 'es'
14
+ }
15
+ ]
16
+ }
package/utils.js ADDED
@@ -0,0 +1,6 @@
1
+ import * as secp256k1 from '@noble/secp256k1'
2
+
3
+ export const makeRandom32 = () => secp256k1.utils.randomPrivateKey()
4
+ export const sha256 = m => secp256k1.utils.sha256(Uint8Array.from(m))
5
+ export const getPublicKey = privateKey =>
6
+ secp256k1.schnorr.getPublicKey(privateKey)