add direct message event and refactor

This commit is contained in:
ennmichael 2023-03-06 22:47:14 +01:00
parent 0d4394e1e6
commit a7707af756
No known key found for this signature in database
GPG Key ID: 6E6E183431A26AF7
13 changed files with 398 additions and 164 deletions

View File

@ -309,4 +309,4 @@
"zjJZBd": "You're ready!",
"zonsdq": "Failed to load LNURL service",
"zvCDao": "Automatically show latest notes"
}
}

View File

@ -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",

View File

@ -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) {

View File

@ -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<string, Filters[]> = 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<T> {
#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<EventId>[]
authors?: Prefix<string>[]
ids?: EventId[]
authors?: string[]
kinds?: EventKind[]
/**
* Filters for the "#e" tags.

View File

@ -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<Uint8Array> {
return await secp.utils.sha256(data)
}
/**
* Sign the data using elliptic curve cryptography.
*/
export async function schnorrSign(
data: Hex,
key: PrivateKey
): Promise<Uint8Array> {
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<boolean> {
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<AesEncryptedBase64> {
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<string> {
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
}
}

View File

@ -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.

View File

@ -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<EventKind, EventKind.SetMetadata | EventKind.TextNote>
}
// TODO Doc comment
export class EventId {
#hex: string
#hex: Hex
static async create(event: Event | RawEvent): Promise<EventId> {
// 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<Event>
#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<SignedEvent> {
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<SignedEvent> {
static async verify(raw: RawEvent, key?: PrivateKey): Promise<SignedEvent> {
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<Event> {
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,

View File

@ -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()
}
}

View File

@ -11,7 +11,10 @@ import Nips from "./Nips";
import { unwrap } from "./Util";
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
export type AuthHandler = (challenge: string, relay: string) => Promise<NEvent | undefined>;
export type AuthHandler = (
challenge: string,
relay: string
) => Promise<NEvent | undefined>;
/**
* Relay settings
@ -57,7 +60,7 @@ export class Connection {
AwaitingAuth: Map<string, boolean>;
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);

View File

@ -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<T>(v: T | undefined | null): T {
if (v === undefined || v === null) {
throw new ProtocolError("bug: unexpected undefined")
}
return v
}

View File

@ -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()

View File

@ -9,7 +9,9 @@
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"noImplicitOverride": true
"noImplicitOverride": true,
"module": "CommonJS",
"strict": true
},
"include": ["src"]
}

View File

@ -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==