diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index a3907b2e..05b16d41 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -309,4 +309,4 @@ "zjJZBd": "You're ready!", "zonsdq": "Failed to load LNURL service", "zvCDao": "Automatically show latest notes" -} \ No newline at end of file +} diff --git a/packages/nostr/package.json b/packages/nostr/package.json index cbc9a23e..7927d439 100644 --- a/packages/nostr/package.json +++ b/packages/nostr/package.json @@ -26,6 +26,7 @@ "dependencies": { "@noble/hashes": "^1.2.0", "@noble/secp256k1": "^1.7.1", + "base64-js": "^1.5.1", "bech32": "^2.0.0", "events": "^3.3.0", "isomorphic-ws": "^5.0.0", diff --git a/packages/nostr/src/client/conn.ts b/packages/nostr/src/client/conn.ts index 8b24b0b8..e35ff618 100644 --- a/packages/nostr/src/client/conn.ts +++ b/packages/nostr/src/client/conn.ts @@ -120,7 +120,6 @@ export class Conn { return { kind: "event", subscriptionId: new SubscriptionId(json[1]), - signed: await SignedEvent.verify(raw), raw, } } @@ -175,7 +174,6 @@ export type IncomingKind = "event" | "notice" | "ok" export interface IncomingEvent { kind: "event" subscriptionId: SubscriptionId - signed: SignedEvent raw: RawEvent } @@ -266,18 +264,18 @@ function serializeFilters(filters: Filters[]): RawFilters[] { return [{}] } return filters.map((filter) => ({ - ids: filter.ids?.map((id) => id.toString()), - authors: filter.authors?.map((author) => author.toString()), + ids: filter.ids?.map((id) => id.toHex()), + authors: filter.authors?.map((author) => author), kinds: filter.kinds?.map((kind) => kind), - ["#e"]: filter.eventTags?.map((e) => e.toString()), - ["#p"]: filter.pubkeyTags?.map((p) => p.toString()), + ["#e"]: filter.eventTags?.map((e) => e.toHex()), + ["#p"]: filter.pubkeyTags?.map((p) => p.toHex()), since: filter.since !== undefined ? unixTimestamp(filter.since) : undefined, until: filter.until !== undefined ? unixTimestamp(filter.until) : undefined, limit: filter.limit, })) } -function parseEventData(json: object): RawEvent { +function parseEventData(json: { [key: string]: unknown }): RawEvent { if ( typeof json["id"] !== "string" || typeof json["pubkey"] !== "string" || @@ -292,7 +290,7 @@ function parseEventData(json: object): RawEvent { ) { throw new ProtocolError(`invalid event: ${JSON.stringify(json)}`) } - return json as RawEvent + return json as unknown as RawEvent } function parseJson(data: string) { diff --git a/packages/nostr/src/client/index.ts b/packages/nostr/src/client/index.ts index d83e1891..34af5e12 100644 --- a/packages/nostr/src/client/index.ts +++ b/packages/nostr/src/client/index.ts @@ -1,9 +1,10 @@ import { ProtocolError } from "../error" import { EventId, Event, EventKind, SignedEvent, RawEvent } from "../event" -import { PrivateKey, PublicKey } from "../keypair" +import { PrivateKey, PublicKey } from "../crypto" import { Conn } from "./conn" import * as secp from "@noble/secp256k1" import { EventEmitter } from "./emitter" +import { defined } from "../util" /** * A nostr client. @@ -22,6 +23,16 @@ export class Nostr extends EventEmitter { */ readonly #subscriptions: Map = new Map() + /** + * Optional client private key. + */ + readonly #key?: PrivateKey + + constructor(key?: PrivateKey) { + super() + this.#key = key + } + /** * Open a connection and start communicating with a relay. This method recreates all existing * subscriptions on the new relay as well. If there is already an existing connection, @@ -52,13 +63,13 @@ export class Nostr extends EventEmitter { const conn = new Conn({ url: connUrl, // Handle messages on this connection. - onMessage: (msg) => { + onMessage: async (msg) => { try { if (msg.kind === "event") { this.emit( "event", { - signed: msg.signed, + signed: await SignedEvent.verify(msg.raw, this.#key), subscriptionId: msg.subscriptionId, raw: msg.raw, }, @@ -208,7 +219,7 @@ export class Nostr extends EventEmitter { "publish called with an unsigned Event, private key must be specified" ) } - if (event.pubkey.toString() !== key.pubkey.toString()) { + if (event.pubkey.toHex() !== key.pubkey.toHex()) { throw new Error("invalid private key") } } @@ -218,7 +229,7 @@ export class Nostr extends EventEmitter { continue } if (!(event instanceof SignedEvent) && !("sig" in event)) { - event = await SignedEvent.sign(event, key) + event = await SignedEvent.sign(event, defined(key)) } conn.send({ kind: "event", @@ -263,30 +274,13 @@ export class SubscriptionId { } } -// TODO Rethink this type. Maybe it's not very idiomatic. -/** - * A prefix filter. These filters match events which have the appropriate prefix. - * This also means that exact matches pass the filters. No special syntax is required. - */ -export class Prefix { - #prefix: T - - constructor(prefix: T) { - this.#prefix = prefix - } - - toString(): string { - return this.#prefix.toString() - } -} - /** * Subscription filters. All filters from the fields must pass for a message to get through. */ export interface Filters { // TODO Document the filters, document that for the arrays only one is enough for the message to pass - ids?: Prefix[] - authors?: Prefix[] + ids?: EventId[] + authors?: string[] kinds?: EventKind[] /** * Filters for the "#e" tags. diff --git a/packages/nostr/src/crypto.ts b/packages/nostr/src/crypto.ts new file mode 100644 index 00000000..8a4fa8c0 --- /dev/null +++ b/packages/nostr/src/crypto.ts @@ -0,0 +1,224 @@ +import * as secp from "@noble/secp256k1" +import { ProtocolError } from "./error" +import base64 from "base64-js" +import { bech32 } from "bech32" + +// TODO Use toHex as well as toString? Might be more explicit +// Or maybe replace toString with toHex +// TODO Or maybe always store Uint8Array and properly use the format parameter passed into toString + +/** + * A 32-byte secp256k1 public key. + */ +export class PublicKey { + #hex: Hex + + /** + * Expects the key encoded as an npub-prefixed bech32 string, lowercase hex string, or byte buffer. + */ + constructor(key: string | Uint8Array) { + this.#hex = parseKey(key, "npub1") + if (this.#hex.toString().length !== 64) { + throw new ProtocolError(`invalid pubkey: ${key}`) + } + } + + toHex(): string { + return this.#hex.toString() + } + + toString(): string { + return this.toHex() + } +} + +/** + * A 32-byte secp256k1 private key. + */ +export class PrivateKey { + #hex: Hex + + /** + * Expects the key encoded as an nsec-prefixed bech32 string, lowercase hex string, or byte buffer. + */ + constructor(key: string | Uint8Array) { + this.#hex = parseKey(key, "nsec1") + if (this.#hex.toString().length !== 64) { + throw new ProtocolError(`invalid private key: ${this.#hex}`) + } + } + + get pubkey(): PublicKey { + return new PublicKey(secp.schnorr.getPublicKey(this.#hex.toString())) + } + + /** + * The hex representation of the private key. Use with caution! + */ + toHexDangerous(): string { + return this.#hex.toString() + } + + toString(): string { + return "PrivateKey" + } +} + +/** + * Parse a public or private key into its hex representation. + */ +function parseKey(key: string | Uint8Array, bechPrefix: string): Hex { + if (typeof key === "string") { + // If the key is bech32-encoded, decode it. + if (key.startsWith(bechPrefix)) { + const { words } = bech32.decode(key) + const bytes = Uint8Array.from(bech32.fromWords(words)) + return new Hex(bytes) + } + } + return new Hex(key) +} + +/** + * Get the SHA256 hash of the data. + */ +export async function sha256(data: Uint8Array): Promise { + return await secp.utils.sha256(data) +} + +/** + * Sign the data using elliptic curve cryptography. + */ +export async function schnorrSign( + data: Hex, + key: PrivateKey +): Promise { + return secp.schnorr.sign(data.toString(), key.toHexDangerous()) +} + +/** + * Verify that the elliptic curve signature is correct. + */ +export async function schnorrVerify( + sig: Hex, + data: Hex, + key: PublicKey +): Promise { + return secp.schnorr.verify(sig.toString(), data.toString(), key.toString()) +} + +interface AesEncryptedBase64 { + data: string + iv: string +} + +export async function aesEncryptBase64( + sender: PrivateKey, + recipient: PublicKey, + plaintext: string +): Promise { + const sharedPoint = secp.getSharedSecret( + sender.toHexDangerous(), + "02" + recipient.toHex() + ) + const sharedKey = sharedPoint.slice(2, 33) + if (typeof window === "object") { + const key = await window.crypto.subtle.importKey( + "raw", + sharedKey, + { name: "AES-CBC" }, + false, + ["encrypt", "decrypt"] + ) + const iv = window.crypto.getRandomValues(new Uint8Array(16)) + const data = new TextEncoder().encode(plaintext) + const encrypted = await window.crypto.subtle.encrypt( + { + name: "AES-CBC", + iv, + }, + key, + data + ) + return { + data: base64.fromByteArray(new Uint8Array(encrypted)), + iv: base64.fromByteArray(iv), + } + } else { + const crypto = await import("crypto") + const iv = crypto.randomFillSync(new Uint8Array(16)) + const cipher = crypto.createCipheriv( + "aes-256-cbc", + // TODO If this code is correct, also fix the example code + // TODO I also this that the slice() above is incorrect because the author + // thought this was hex but it's actually bytes so should take 32 bytes not 64 + // TODO Actually it's probably cleanest to leave out the end of the slice completely, if possible, and it should be + Buffer.from(sharedKey), + iv + ) + let encrypted = cipher.update(plaintext, "utf8", "base64") + // TODO Could save an allocation here by avoiding the += + encrypted += cipher.final() + return { + data: encrypted, + iv: Buffer.from(iv.buffer).toString("base64"), + } + } +} + +// TODO +export async function aesDecryptBase64( + sender: PublicKey, + recipient: PrivateKey, + { data, iv }: AesEncryptedBase64 +): Promise { + const sharedPoint = secp.getSharedSecret( + recipient.toHexDangerous(), + "02" + sender.toHex() + ) + const sharedKey = sharedPoint.slice(2, 33) + if (typeof window === "object") { + // TODO Can copy this from the legacy code + throw new Error("todo") + } else { + const crypto = await import("crypto") + const decipher = crypto.createDecipheriv( + "aes-256-cbc", + Buffer.from(sharedKey), + base64.toByteArray(iv) + ) + const plaintext = decipher.update(data, "base64", "utf8") + return plaintext + decipher.final() + } +} + +/** + * A string in lowercase hex. This type is not available to the users of the library. + */ +export class Hex { + #value: string + + /** + * Passing a non-lowercase or non-hex string to the constructor + * results in an error being thrown. + */ + constructor(value: string | Uint8Array) { + if (value instanceof Uint8Array) { + value = secp.utils.bytesToHex(value).toLowerCase() + } + if (value.length % 2 != 0) { + throw new ProtocolError(`invalid lowercase hex string: ${value}`) + } + const valid = "0123456789abcdef" + for (const c of value) { + if (!valid.includes(c)) { + throw new ProtocolError(`invalid lowercase hex string: ${value}`) + } + } + this.#value = value + } + + toString(): string { + return this.#value + } +} diff --git a/packages/nostr/src/error.ts b/packages/nostr/src/error.ts index 48699112..075c677e 100644 --- a/packages/nostr/src/error.ts +++ b/packages/nostr/src/error.ts @@ -1,3 +1,4 @@ +// TODO Rename to NostrError and move to util.ts, always throw NostrError and never throw Error /** * An error in the protocol. This error is thrown when a relay sends invalid or * unexpected data, or otherwise behaves in an unexpected way. diff --git a/packages/nostr/src/event.ts b/packages/nostr/src/event.ts index 8dcfb29f..4e865f68 100644 --- a/packages/nostr/src/event.ts +++ b/packages/nostr/src/event.ts @@ -1,7 +1,14 @@ import { ProtocolError } from "./error" -import * as secp from "@noble/secp256k1" -import { PublicKey, PrivateKey } from "./keypair" -import { unixTimestamp } from "./util" +import { + PublicKey, + PrivateKey, + sha256, + Hex, + schnorrSign, + schnorrVerify, + aesDecryptBase64, +} from "./crypto" +import { defined, unixTimestamp } from "./util" // TODO This file is missing proper documentation // TODO Add remaining event types @@ -24,13 +31,19 @@ export enum EventKind { ZapReceipt = 9735, // NIP 57 } -export type Event = SetMetadataEvent | TextNoteEvent | UnknownEvent +export type Event = + | SetMetadataEvent + | TextNoteEvent + | DirectMessageEvent + | UnknownEvent interface EventCommon { pubkey: PublicKey createdAt: Date } +// TODO Refactor: the event names don't need to all end with *Event + export interface SetMetadataEvent extends EventCommon { kind: EventKind.SetMetadata content: UserMetadata @@ -47,13 +60,23 @@ export interface TextNoteEvent extends EventCommon { content: string } +export interface DirectMessageEvent extends EventCommon { + kind: EventKind.DirectMessage + /** + * The plaintext message, or undefined if this client is not the recipient. + */ + message?: string + recipient: PublicKey + previous?: EventId +} + export interface UnknownEvent extends EventCommon { kind: Exclude } // TODO Doc comment export class EventId { - #hex: string + #hex: Hex static async create(event: Event | RawEvent): Promise { // It's not defined whether JSON.stringify produces a string with whitespace stripped. @@ -66,34 +89,33 @@ export class EventId { .map((tag) => `[${tag.map((v) => `"${v}"`).join(",")}]`) .join(",")}]` const serialized = `[0,"${event.pubkey}",${event.created_at},${event.kind},${serializedTags},"${event.content}"]` - const hash = await secp.utils.sha256( - Uint8Array.from(charCodes(serialized)) - ) - return new EventId(secp.utils.bytesToHex(hash).toLowerCase()) + const hash = await sha256(Uint8Array.from(charCodes(serialized))) + return new EventId(hash) } else { // Not a raw event. - const tags = serializeEventTags(event) - const content = serializeEventContent(event) + const tags = serializeTags(event) + const content = serializeContent(event) const serializedTags = `[${tags .map((tag) => `[${tag.map((v) => `"${v}"`).join(",")}]`) .join(",")}]` const serialized = `[0,"${event.pubkey}",${unixTimestamp( event.createdAt )},${event.kind},${serializedTags},"${content}"]` - const hash = await secp.utils.sha256( - Uint8Array.from(charCodes(serialized)) - ) - return new EventId(secp.utils.bytesToHex(hash).toLowerCase()) + const hash = await sha256(Uint8Array.from(charCodes(serialized))) + return new EventId(hash) } } - constructor(hex: string) { - // TODO Validate that this is 32-byte hex - this.#hex = hex + constructor(hex: string | Uint8Array) { + this.#hex = new Hex(hex) + } + + toHex(): string { + return this.#hex.toString() } toString(): string { - return this.#hex + return this.toHex() } } @@ -103,7 +125,7 @@ export class EventId { export class SignedEvent { #event: Readonly #eventId: EventId - #signature: string + #signature: Hex /** * Sign an event using the specified private key. The private key must match the @@ -111,29 +133,34 @@ export class SignedEvent { */ static async sign(event: Event, key: PrivateKey): Promise { const id = await EventId.create(event) - const sig = secp.utils - .bytesToHex(await secp.schnorr.sign(id.toString(), key.hexDangerous())) - .toLowerCase() - return new SignedEvent(event, id, sig) + const sig = await schnorrSign(new Hex(id.toHex()), key) + return new SignedEvent(event, id, new Hex(sig)) } /** * Verify the signature of a raw event. Throw a `ProtocolError` if the signature * is invalid. */ - static async verify(raw: RawEvent): Promise { + static async verify(raw: RawEvent, key?: PrivateKey): Promise { const id = await EventId.create(raw) - if (id.toString() !== raw.id) { + if (id.toHex() !== raw.id) { throw new ProtocolError(`invalid event id: ${raw.id}, expected ${id}`) } - if (!(await secp.schnorr.verify(raw.sig, id.toString(), raw.pubkey))) { - throw new ProtocolError(`invalid signature: ${raw.sig}`) + const sig = new Hex(raw.sig) + if ( + !(await schnorrVerify( + sig, + new Hex(id.toHex()), + new PublicKey(raw.pubkey) + )) + ) { + throw new ProtocolError(`invalid signature: ${sig}`) } - return new SignedEvent(parseEvent(raw), id, raw.sig) + return new SignedEvent(await parseEvent(raw, key), id, sig) } - private constructor(event: Event, eventId: EventId, signature: string) { - this.#event = cloneEvent(event) + private constructor(event: Event, eventId: EventId, signature: Hex) { + this.#event = deepCopy(event) this.#eventId = eventId this.#signature = signature } @@ -149,14 +176,14 @@ export class SignedEvent { * Event data. */ get event(): Event { - return cloneEvent(this.#event) + return deepCopy(this.#event) } /** - * Event signature. + * Event signature in hex format. */ get signature(): string { - return this.#signature + return this.#signature.toString() } /** @@ -164,11 +191,11 @@ export class SignedEvent { */ serialize(): RawEvent { const { event, eventId: id, signature } = this - const tags = serializeEventTags(event) - const content = serializeEventContent(event) + const tags = serializeTags(event) + const content = serializeContent(event) return { - id: id.toString(), - pubkey: event.pubkey.toString(), + id: id.toHex(), + pubkey: event.pubkey.toHex(), created_at: unixTimestamp(event.createdAt), kind: event.kind, tags, @@ -191,7 +218,10 @@ export interface RawEvent { /** * Parse an event from its raw format. */ -function parseEvent(raw: RawEvent): Event { +async function parseEvent( + raw: RawEvent, + key: PrivateKey | undefined +): Promise { const pubkey = new PublicKey(raw.pubkey) const createdAt = new Date(raw.created_at * 1000) const event = { @@ -206,7 +236,9 @@ function parseEvent(raw: RawEvent): Event { typeof userMetadata["about"] !== "string" || typeof userMetadata["picture"] !== "string" ) { - throw new ProtocolError(`invalid user metadata: ${userMetadata}`) + throw new ProtocolError( + `invalid user metadata ${userMetadata} in ${JSON.stringify(raw)}` + ) } return { ...event, @@ -223,18 +255,59 @@ function parseEvent(raw: RawEvent): Event { } } + if (raw.kind === EventKind.DirectMessage) { + // Parse the tag identifying the recipient. + const recipientTag = raw.tags.find((tag) => tag[0] === "p") + if (typeof recipientTag?.[1] !== "string") { + throw new ProtocolError( + `expected "p" tag to be of type string, but got ${ + recipientTag?.[1] + } in ${JSON.stringify(raw)}` + ) + } + const recipient = new PublicKey(recipientTag[1]) + + // Parse the tag identifying the optional previous message. + const previousTag = raw.tags.find((tag) => tag[0] === "e") + if (typeof recipientTag[1] !== "string") { + throw new ProtocolError( + `expected "e" tag to be of type string, but got ${ + previousTag?.[1] + } in ${JSON.stringify(raw)}` + ) + } + const previous = new EventId(defined(previousTag?.[1])) + + // Decrypt the message content. + const [data, iv] = raw.content.split("?iv=") + if (data === undefined || iv === undefined) { + throw new ProtocolError(`invalid direct message content ${raw.content}`) + } + let message: string | undefined + if (key?.pubkey?.toHex() === recipient.toHex()) { + message = await aesDecryptBase64(event.pubkey, key, { data, iv }) + } + return { + ...event, + kind: EventKind.DirectMessage, + message, + recipient, + previous, + } + } + return { ...event, kind: raw.kind, } } -function serializeEventTags(_event: Event): string[][] { +function serializeTags(_event: Event): string[][] { // TODO As I add different event kinds, this will change return [] } -function serializeEventContent(event: Event): string { +function serializeContent(event: Event): string { if (event.kind === EventKind.SetMetadata) { return JSON.stringify(event.content) } else if (event.kind === EventKind.TextNote) { @@ -244,10 +317,13 @@ function serializeEventContent(event: Event): string { } } -function cloneEvent(event: Event): Event { +/** + * Create a deep copy of the event. + */ +function deepCopy(event: Event): Event { const common = { createdAt: structuredClone(event.createdAt), - pubkey: new PublicKey(event.pubkey.toString()), + pubkey: event.pubkey, } if (event.kind === EventKind.SetMetadata) { return { @@ -265,6 +341,8 @@ function cloneEvent(event: Event): Event { content: event.content, ...common, } + } else if (event.kind === EventKind.DirectMessage) { + throw new Error("todo") } else { return { kind: event.kind, diff --git a/packages/nostr/src/keypair.ts b/packages/nostr/src/keypair.ts deleted file mode 100644 index c43b6990..00000000 --- a/packages/nostr/src/keypair.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { bech32 } from "bech32" -import { ProtocolError } from "./error" -import * as secp from "@noble/secp256k1" - -/** - * A 32-byte secp256k1 public key. - */ -export class PublicKey { - #hex: string - - /** - * Expects the key encoded as an npub-prefixed bech32 string, hex string, or byte buffer. - */ - constructor(key: string | Uint8Array) { - this.#hex = parseKey(key, "npub1") - if (this.#hex.length !== 64) { - throw new ProtocolError(`invalid pubkey: ${key}`) - } - } - - toString(): string { - return this.#hex - } -} - -/** - * A 32-byte secp256k1 private key. - */ -export class PrivateKey { - #hex: string - - /** - * Expects the key encoded as an nsec-prefixed bech32 string, hex string, or byte buffer. - */ - constructor(key: string | Uint8Array) { - this.#hex = parseKey(key, "nsec1") - if (this.#hex.length !== 64) { - throw new ProtocolError(`invalid private key: ${this.#hex}`) - } - } - - get pubkey(): PublicKey { - return new PublicKey(secp.schnorr.getPublicKey(this.#hex)) - } - - /** - * The hex representation of the private key. Use with caution! - */ - hexDangerous(): string { - return this.#hex - } -} - -/** - * Parse a key into its hex representation. - */ -function parseKey(key: string | Uint8Array, bechPrefix: string): string { - if (typeof key === "string") { - // Is the key encoded in bech32? - if (key.startsWith(bechPrefix)) { - const { words } = bech32.decode(key) - const bytes = Uint8Array.from(bech32.fromWords(words)) - return secp.utils.bytesToHex(bytes).toLowerCase() - } - // If not, it must be lowercase hex. - const valid = "0123456789abcdef" - if (key.length % 2 != 0) { - throw new ProtocolError(`invalid lowercase hex string: ${key}`) - } - for (const c of key) { - if (!valid.includes(c)) { - throw new ProtocolError(`invalid lowercase hex string: ${key}`) - } - } - return key - } else { - return secp.utils.bytesToHex(key).toLowerCase() - } -} diff --git a/packages/nostr/src/legacy/Connection.ts b/packages/nostr/src/legacy/Connection.ts index 6adcadc0..a44723c8 100644 --- a/packages/nostr/src/legacy/Connection.ts +++ b/packages/nostr/src/legacy/Connection.ts @@ -11,7 +11,10 @@ import Nips from "./Nips"; import { unwrap } from "./Util"; export type CustomHook = (state: Readonly) => void; -export type AuthHandler = (challenge: string, relay: string) => Promise; +export type AuthHandler = ( + challenge: string, + relay: string +) => Promise; /** * Relay settings @@ -57,7 +60,7 @@ export class Connection { AwaitingAuth: Map; Authed: boolean; - constructor(addr: string, options: RelaySettings, auth: AuthHandler = undefined) { + constructor(addr: string, options: RelaySettings, auth?: AuthHandler) { this.Id = uuid(); this.Address = addr; this.Socket = null; @@ -387,7 +390,7 @@ export class Connection { const authCleanup = () => { this.AwaitingAuth.delete(challenge); }; - if(!this.Auth) { + if (!this.Auth) { throw new Error("Auth hook not registered"); } this.AwaitingAuth.set(challenge, true); diff --git a/packages/nostr/src/util.ts b/packages/nostr/src/util.ts index 5f1858ea..0a3a3d76 100644 --- a/packages/nostr/src/util.ts +++ b/packages/nostr/src/util.ts @@ -1,6 +1,18 @@ +import { ProtocolError } from "./error" + /** * Calculate the unix timestamp (seconds since epoch) of the `Date`. */ export function unixTimestamp(date: Date): number { return Math.floor(date.getTime() / 1000) } + +/** + * Throw if the parameter is null or undefined. Return the parameter otherwise. + */ +export function defined(v: T | undefined | null): T { + if (v === undefined || v === null) { + throw new ProtocolError("bug: unexpected undefined") + } + return v +} diff --git a/packages/nostr/test/simple-communication.ts b/packages/nostr/test/simple-communication.ts index 4841a6e9..c7e52cb3 100644 --- a/packages/nostr/test/simple-communication.ts +++ b/packages/nostr/test/simple-communication.ts @@ -1,6 +1,6 @@ import { Nostr } from "../src/client" import { EventKind, SignedEvent } from "../src/event" -import { PrivateKey } from "../src/keypair" +import { PrivateKey } from "../src/crypto" import assert from "assert" import { EventParams } from "../src/client/emitter" @@ -33,7 +33,7 @@ describe("simple communication", function () { function listener({ signed: { event } }: EventParams, nostr: Nostr) { assert.equal(nostr, subscriber) assert.equal(event.kind, EventKind.TextNote) - assert.equal(event.pubkey.toString(), pubkey.toString()) + assert.equal(event.pubkey.toHex(), pubkey.toHex()) assert.equal(event.createdAt.toString(), timestamp.toString()) if (event.kind === EventKind.TextNote) { assert.equal(event.content, note) @@ -73,7 +73,7 @@ describe("simple communication", function () { ).then((event) => { publisher.on("ok", (params, nostr) => { assert.equal(nostr, publisher) - assert.equal(params.eventId.toString(), event.eventId.toString()) + assert.equal(params.eventId.toHex(), event.eventId.toHex()) assert.equal(params.relay.toString(), url.toString()) assert.equal(params.ok, true) done() diff --git a/packages/nostr/tsconfig.json b/packages/nostr/tsconfig.json index ae65c78a..75475703 100644 --- a/packages/nostr/tsconfig.json +++ b/packages/nostr/tsconfig.json @@ -9,7 +9,9 @@ "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, - "noImplicitOverride": true + "noImplicitOverride": true, + "module": "CommonJS", + "strict": true }, "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index 6abb894f..6fe90e76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3143,7 +3143,7 @@ base32-decode@^1.0.0: resolved "https://registry.yarnpkg.com/base32-decode/-/base32-decode-1.0.0.tgz#2a821d6a664890c872f20aa9aca95a4b4b80e2a7" integrity sha512-KNWUX/R7wKenwE/G/qFMzGScOgVntOmbE27vvc6GrniDGYb6a5+qWcuoXl8WIOQL7q0TpK7nZDm1Y04Yi3Yn5g== -base64-js@^1.3.1: +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==