From 821a8f789506fd3bd0629d27094fd5d139e536a2 Mon Sep 17 00:00:00 2001 From: Lennon Day-Reynolds Date: Fri, 15 Jul 2022 11:49:49 -0700 Subject: [PATCH] TypeScript definitions (#18) --- .gitignore | 1 + README.md | 4 ++ build.js => build.cjs | 0 index.d.ts | 106 ++++++++++++++++++++++++++++++++++++++++++ index.js | 10 ++-- index.test-d.ts | 42 +++++++++++++++++ nip06.js | 2 +- package.json | 9 +++- pool.js | 4 +- relay.js | 4 +- tsconfig.json | 25 ++++++++++ 11 files changed, 195 insertions(+), 12 deletions(-) rename build.js => build.cjs (100%) create mode 100644 index.d.ts create mode 100644 index.test-d.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 3a90a94..290a877 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist yarn.lock package-lock.json nostr.js +.envrc diff --git a/README.md b/README.md index 4733267..a700576 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,10 @@ You can import nostr-tools as an ES module. Just add a script tag like this: 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. diff --git a/build.js b/build.cjs similarity index 100% rename from build.js rename to build.cjs diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..b67464d --- /dev/null +++ b/index.d.ts @@ -0,0 +1,106 @@ +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, + // 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, +}; + +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, + close: () => void, + status: number, +}; + +declare function relayPool(): RelayPool; + +// nip04.js + +// nip05.js + +// nip06.js diff --git a/index.js b/index.js index 200e692..d7d4c31 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ -import {generatePrivateKey, getPublicKey} from './keys' -import {relayConnect} from './relay' -import {relayPool} from './pool' +import {generatePrivateKey, getPublicKey} from './keys.js' +import {relayConnect} from './relay.js' +import {relayPool} from './pool.js' import { getBlankEvent, signEvent, @@ -8,8 +8,8 @@ import { verifySignature, serializeEvent, getEventHash -} from './event' -import {matchFilter, matchFilters} from './filter' +} from './event.js' +import {matchFilter, matchFilters} from './filter.js' export { generatePrivateKey, diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..371febf --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,42 @@ +import * as process from 'process'; +import { + relayPool, + getBlankEvent, + validateEvent, + RelayPool, + Event as NEvent +} from './index.js'; +import { expectType } from 'tsd'; + +const pool = relayPool(); +expectType(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>(publishF); + +publishF.then((event) => { + expectType(event); + + console.info({ event }); + + if (!validateEvent(event)) { + console.error(`event failed to validate!`); + process.exit(1); + } +}); diff --git a/nip06.js b/nip06.js index 7f603ca..20012bd 100644 --- a/nip06.js +++ b/nip06.js @@ -1,4 +1,4 @@ -import {wordlist} from 'micro-bip39/wordlists/english' +import {wordlist} from 'micro-bip39/wordlists/english.js' import { generateMnemonic, mnemonicToSeedSync, diff --git a/package.json b/package.json index 34cbfe2..4f5202b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "type": "git", "url": "https://github.com/fiatjaf/nostr-tools.git" }, + "type": "module", "dependencies": { "@noble/hashes": "^0.5.7", "@noble/secp256k1": "^1.5.2", @@ -31,14 +32,18 @@ ], "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-plugin-babel": "^5.3.1", + "esm-loader-typescript": "^1.0.1", "events": "^3.3.0", - "readable-stream": "^3.6.0" + "tsd": "^0.22.0", + "typescript": "^4.7.4" }, "scripts": { - "prepublish": "node build.js" + "prepublish": "node build.cjs", + "check-ts": "tsd && node --no-warnings --loader=esm-loader-typescript index.test-d.ts" } } diff --git a/pool.js b/pool.js index 6a5df85..84daa33 100644 --- a/pool.js +++ b/pool.js @@ -1,5 +1,5 @@ -import {getEventHash, verifySignature, signEvent} from './event' -import {relayConnect, normalizeRelayURL} from './relay' +import {getEventHash, verifySignature, signEvent} from './event.js' +import {relayConnect, normalizeRelayURL} from './relay.js' export function relayPool() { var globalPrivateKey diff --git a/relay.js b/relay.js index fe91148..fb65cef 100644 --- a/relay.js +++ b/relay.js @@ -2,8 +2,8 @@ import 'websocket-polyfill' -import {verifySignature, validateEvent} from './event' -import {matchFilters} from './filter' +import {verifySignature, validateEvent} from './event.js' +import {matchFilters} from './filter.js' export function normalizeRelayURL(url) { let [host, ...qs] = url.trim().split('?') diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5e9fb16 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "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" + ] +}