Compare commits

..

1 Commits

Author SHA1 Message Date
fiatjaf
84e4fb1f92 update noble secp256k1 and ensure we always return hex. 2022-02-11 16:20:47 -03:00
15 changed files with 31 additions and 288 deletions

2
.gitignore vendored
View File

@@ -2,5 +2,3 @@ node_modules
dist dist
yarn.lock yarn.lock
package-lock.json package-lock.json
nostr.js
.envrc

View File

@@ -70,24 +70,3 @@ pool.addRelay('<url>')
All functions expect bytearrays as hex strings and output bytearrays as hex strings. All functions expect bytearrays as hex strings and output bytearrays as hex strings.
For other utils please read the source (for now). For other utils please read the source (for now).
### Using from the browser (if you don't want to use a bundler)
You can import nostr-tools as an ES module. Just add a script tag like this:
```html
<script type="module">
import {generatePrivateKey} from 'https://unpkg.com/nostr-tools/nostr.js'
console.log(generatePrivateKey())
</script>
```
And import whatever function you would import from `"nostr-tools"` in a bundler.
## TypeScript
This module has hand-authored TypeScript declarations. `npm run check-ts` will run a lint-check script to ensure the typings can be loaded and call at least a few standard library functions. It's not at all comprehensive and likely to contain bugs. Issues welcome; tag @rcoder as needed.
## License
Public domain.

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env node
const esbuild = require('esbuild')
const alias = require('esbuild-plugin-alias')
const nodeGlobals = require('@esbuild-plugins/node-globals-polyfill').default
const buildOptions = {
entryPoints: ['index.js'],
outfile: 'nostr.js',
bundle: true,
format: 'esm',
plugins: [
alias({
stream: require.resolve('readable-stream')
}),
nodeGlobals({buffer: true})
],
define: {
window: 'self',
global: 'self'
},
loader: {'.js': 'jsx'}
}
esbuild.build(buildOptions).then(() => console.log('build success.'))

View File

@@ -52,7 +52,7 @@ export function verifySignature(event) {
} }
export async function signEvent(event, key) { export async function signEvent(event, key) {
return Buffer.from( return Buffer.from(secp256k1.schnorr.sign(getEventHash(event), key)).toString(
await secp256k1.schnorr.sign(getEventHash(event), key) 'hex'
).toString('hex') )
} }

107
index.d.ts vendored
View File

@@ -1,107 +0,0 @@
import { type Buffer } from 'buffer';
// these should be available from the native @noble/secp256k1 type
// declarations, but they somehow aren't so instead: copypasta
declare type Hex = Uint8Array | string;
declare type PrivKey = Hex | bigint | number;
declare enum EventKind {
Metadata = 0,
Text = 1,
RelayRec = 2,
Contacts = 3,
DM = 4,
Deleted = 5,
}
// event.js
declare type Event = {
kind: EventKind,
pubkey?: string,
content: string,
tags: string[],
created_at: number,
};
declare function getBlankEvent(): Event;
declare function serializeEvent(event: Event): string;
declare function getEventHash(event: Event): string;
declare function validateEvent(event: Event): boolean;
declare function validateSignature(event: Event): boolean;
declare function signEvent(event: Event, key: PrivKey): Promise<[Uint8Array, number]>;
// filter.js
declare type Filter = {
ids: string[],
kinds: EventKind[],
authors: string[],
since: number,
until: number,
"#e": string[],
"#p": string[],
};
declare function matchFilter(filter: Filter, event: Event): boolean;
declare function matchFilters(filters: Filter[], event: Event): boolean;
// general
declare type ClientMessage =
["EVENT", Event] |
["REQ", string, Filter[]] |
["CLOSE", string];
declare type ServerMessage =
["EVENT", string, Event] |
["NOTICE", unknown];
// keys.js
declare function generatePrivateKey(): string;
declare function getPublicKey(privateKey: Buffer): string;
// pool.js
declare type RelayPolicy = {
read: boolean,
write: boolean,
};
declare type SubscriptionCallback = (event: Event, relay: string) => void;
declare type SubscriptionOptions = {
cb: SubscriptionCallback,
filter: Filter,
skipVerification: boolean
// TODO: thread through how `beforeSend` actually works before trying to type it
// beforeSend(event: Event):
};
declare type Subscription = {
unsub(): void,
};
declare type PublishCallback = (status: number) => void;
// relay.js
declare type Relay = {
url: string,
sub: SubscriptionCallback,
publish: (event: Event, cb: PublishCallback) => Promise<Event>,
};
declare type PoolPublishCallback = (status: number, relay: string) => void;
declare type RelayPool = {
setPrivateKey(key: string): void,
addRelay(url: string, opts?: RelayPolicy): Relay,
sub(opts: SubscriptionOptions, id?: string): Subscription,
publish(event: Event, cb: PoolPublishCallback): Promise<Event>,
close: () => void,
status: number,
};
declare function relayPool(): RelayPool;
// nip04.js
// nip05.js
// nip06.js

View File

@@ -1,6 +1,6 @@
import {generatePrivateKey, getPublicKey} from './keys.js' import {generatePrivateKey, getPublicKey} from './keys'
import {relayConnect} from './relay.js' import {relayConnect} from './relay'
import {relayPool} from './pool.js' import {relayPool} from './pool'
import { import {
getBlankEvent, getBlankEvent,
signEvent, signEvent,
@@ -8,8 +8,8 @@ import {
verifySignature, verifySignature,
serializeEvent, serializeEvent,
getEventHash getEventHash
} from './event.js' } from './event'
import {matchFilter, matchFilters} from './filter.js' import {matchFilter, matchFilters} from './filter'
export { export {
generatePrivateKey, generatePrivateKey,

View File

@@ -1,42 +0,0 @@
import * as process from 'process';
import {
relayPool,
getBlankEvent,
validateEvent,
RelayPool,
Event as NEvent
} from './index.js';
import { expectType } from 'tsd';
const pool = relayPool();
expectType<RelayPool>(pool);
const privkey = process.env.NOSTR_PRIVATE_KEY;
const pubkey = process.env.NOSTR_PUBLIC_KEY;
const message = {
...getBlankEvent(),
kind: 1,
content: `just saying hi from pid ${process.pid}`,
pubkey,
};
const publishCb = (status: number, url: string) => {
console.log({ status, url });
};
pool.setPrivateKey(privkey!);
const publishF = pool.publish(message, publishCb);
expectType<Promise<NEvent>>(publishF);
publishF.then((event) => {
expectType<NEvent>(event);
console.info({ event });
if (!validateEvent(event)) {
console.error(`event failed to validate!`);
process.exit(1);
}
});

View File

@@ -1,5 +1,4 @@
import * as secp256k1 from '@noble/secp256k1' import * as secp256k1 from '@noble/secp256k1'
import {Buffer} from 'buffer'
export function generatePrivateKey() { export function generatePrivateKey() {
return Buffer.from(secp256k1.utils.randomPrivateKey()).toString('hex') return Buffer.from(secp256k1.utils.randomPrivateKey()).toString('hex')

View File

@@ -29,7 +29,7 @@ export function decrypt(privkey, pubkey, ciphertext) {
Buffer.from(normalizedKey, 'hex'), Buffer.from(normalizedKey, 'hex'),
Buffer.from(iv, 'base64') Buffer.from(iv, 'base64')
) )
let decryptedMessage = decipher.update(cip, 'base64', 'utf8') let decryptedMessage = decipher.update(cip, 'base64')
decryptedMessage += decipher.final('utf8') decryptedMessage += decipher.final('utf8')
return decryptedMessage return decryptedMessage

View File

@@ -15,7 +15,11 @@ export async function searchDomain(domain, query = '') {
export async function queryName(fullname) { export async function queryName(fullname) {
try { try {
let [name, domain] = fullname.split('@') let [name, domain] = fullname.split('@')
if (!domain) return null
if (!domain) {
domain = name
name = '_'
}
let res = await ( let res = await (
await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`) await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`)

View File

@@ -1,4 +1,4 @@
import {wordlist} from 'micro-bip39/wordlists/english.js' import {wordlist} from 'micro-bip39/wordlists/english'
import { import {
generateMnemonic, generateMnemonic,
mnemonicToSeedSync, mnemonicToSeedSync,

View File

@@ -1,12 +1,11 @@
{ {
"name": "nostr-tools", "name": "nostr-tools",
"version": "0.24.1", "version": "0.22.0",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/fiatjaf/nostr-tools.git" "url": "https://github.com/fiatjaf/nostr-tools.git"
}, },
"type": "module",
"dependencies": { "dependencies": {
"@noble/hashes": "^0.5.7", "@noble/hashes": "^0.5.7",
"@noble/secp256k1": "^1.5.2", "@noble/secp256k1": "^1.5.2",
@@ -31,19 +30,7 @@
"client" "client"
], ],
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@types/node": "^18.0.3",
"esbuild": "^0.14.38",
"esbuild-plugin-alias": "^0.2.1",
"eslint": "^8.5.0", "eslint": "^8.5.0",
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1"
"esm-loader-typescript": "^1.0.1",
"events": "^3.3.0",
"tsd": "^0.22.0",
"typescript": "^4.7.4"
},
"scripts": {
"prepublish": "node build.cjs",
"check-ts": "tsd && node --no-warnings --loader=esm-loader-typescript index.test-d.ts"
} }
} }

24
pool.js
View File

@@ -1,5 +1,5 @@
import {getEventHash, verifySignature, signEvent} from './event.js' import {getEventHash, verifySignature, signEvent} from './event'
import {relayConnect, normalizeRelayURL} from './relay.js' import {relayConnect, normalizeRelayURL} from './relay'
export function relayPool() { export function relayPool() {
var globalPrivateKey var globalPrivateKey
@@ -26,42 +26,32 @@ export function relayPool() {
const activeSubscriptions = {} const activeSubscriptions = {}
const sub = ({cb, filter, beforeSend}, id) => { const sub = ({cb, filter}, id = Math.random().toString().slice(2)) => {
if (!id) id = Math.random().toString().slice(2)
const subControllers = Object.fromEntries( const subControllers = Object.fromEntries(
Object.values(relays) Object.values(relays)
.filter(({policy}) => policy.read) .filter(({policy}) => policy.read)
.map(({relay}) => [ .map(({relay}) => [
relay.url, relay.url,
relay.sub({cb: event => cb(event, relay.url), filter, beforeSend}, id) relay.sub({filter, cb: event => cb(event, relay.url)}, id)
]) ])
) )
const activeCallback = cb const activeCallback = cb
const activeFilters = filter const activeFilters = filter
const activeBeforeSend = beforeSend
const unsub = () => { const unsub = () => {
Object.values(subControllers).forEach(sub => sub.unsub()) Object.values(subControllers).forEach(sub => sub.unsub())
delete activeSubscriptions[id] delete activeSubscriptions[id]
} }
const sub = ({ const sub = ({cb = activeCallback, filter = activeFilters}) => {
cb = activeCallback,
filter = activeFilters,
beforeSend = activeBeforeSend
}) => {
Object.entries(subControllers).map(([relayURL, sub]) => [ Object.entries(subControllers).map(([relayURL, sub]) => [
relayURL, relayURL,
sub.sub({cb: event => cb(event, relayURL), filter, beforeSend}, id) sub.sub({cb, filter}, id)
]) ])
return activeSubscriptions[id] return activeSubscriptions[id]
} }
const addRelay = relay => { const addRelay = relay => {
subControllers[relay.url] = relay.sub( subControllers[relay.url] = relay.sub({cb, filter}, id)
{cb: event => cb(event, relay.url), filter, beforeSend},
id
)
return activeSubscriptions[id] return activeSubscriptions[id]
} }
const removeRelay = relayURL => { const removeRelay = relayURL => {

View File

@@ -2,8 +2,8 @@
import 'websocket-polyfill' import 'websocket-polyfill'
import {verifySignature, validateEvent} from './event.js' import {verifySignature, validateEvent} from './event'
import {matchFilters} from './filter.js' import {matchFilters} from './filter'
export function normalizeRelayURL(url) { export function normalizeRelayURL(url) {
let [host, ...qs] = url.trim().split('?') let [host, ...qs] = url.trim().split('?')
@@ -18,7 +18,6 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
var ws, resolveOpen, untilOpen, wasClosed var ws, resolveOpen, untilOpen, wasClosed
var openSubs = {} var openSubs = {}
var isSetToSkipVerification = {}
let attemptNumber = 1 let attemptNumber = 1
let nextAttemptSeconds = 1 let nextAttemptSeconds = 1
@@ -95,7 +94,7 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
if ( if (
validateEvent(event) && validateEvent(event) &&
(isSetToSkipVerification[channel] || verifySignature(event)) && verifySignature(event) &&
channels[channel] && channels[channel] &&
matchFilters(openSubs[channel], event) matchFilters(openSubs[channel], event)
) { ) {
@@ -120,10 +119,7 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
ws.send(msg) ws.send(msg)
} }
const sub = ( const sub = ({cb, filter}, channel = Math.random().toString().slice(2)) => {
{cb, filter, beforeSend, skipVerification},
channel = Math.random().toString().slice(2)
) => {
var filters = [] var filters = []
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
filters = filter filters = filter
@@ -131,30 +127,19 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
filters.push(filter) filters.push(filter)
} }
if (beforeSend) {
const beforeSendResult = beforeSend({filter, relay: url, channel})
filters = beforeSendResult.filter
}
trySend(['REQ', channel, ...filters]) trySend(['REQ', channel, ...filters])
channels[channel] = cb channels[channel] = cb
openSubs[channel] = filters openSubs[channel] = filters
isSetToSkipVerification[channel] = skipVerification
const activeCallback = cb const activeCallback = cb
const activeFilters = filters const activeFilters = filters
const activeBeforeSend = beforeSend
return { return {
sub: ({ sub: ({cb = activeCallback, filter = activeFilters}) =>
cb = activeCallback, sub({cb, filter}, channel),
filter = activeFilters,
beforeSend = activeBeforeSend
}) => sub({cb, filter, beforeSend, skipVerification}, channel),
unsub: () => { unsub: () => {
delete openSubs[channel] delete openSubs[channel]
delete channels[channel] delete channels[channel]
delete isSetToSkipVerification[channel]
trySend(['CLOSE', channel]) trySend(['CLOSE', channel])
} }
} }
@@ -175,7 +160,7 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
unsub() unsub()
clearTimeout(willUnsub) clearTimeout(willUnsub)
}, },
filter: {ids: [event.id]} filter: {id: event.id}
}, },
`monitor-${event.id.slice(0, 5)}` `monitor-${event.id.slice(0, 5)}`
) )

View File

@@ -1,25 +0,0 @@
{
"compilerOptions": {
"module": "es2020",
"target": "es2020",
"lib": ["dom", "es2020"],
"esModuleInterop": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"declaration": true,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"baseUrl": "./",
"typeRoots": ["."],
"types": ["node"],
"noEmit": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.d.ts",
"t/nostr-tools-tests.ts"
]
}