nostr
package: more tests
#434
@ -1,6 +1,8 @@
|
|||||||
version: "3.1"
|
version: "3.1"
|
||||||
services:
|
services:
|
||||||
relay:
|
relay:
|
||||||
image: scsibug/nostr-rs-relay
|
build: ./relay
|
||||||
|
restart: on-failure
|
||||||
ports:
|
ports:
|
||||||
- 12648:8080
|
- 12648:8080
|
||||||
|
- 12649:8000
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"test": "ts-mocha --type-check -j 1 test/*.ts",
|
"test": "ts-mocha --type-check -j 1 --timeout 5s test/*.ts",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -31,5 +31,12 @@
|
|||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
}
|
},
|
||||||
|
"directories": {
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": ""
|
||||||
}
|
}
|
||||||
|
1
packages/nostr/relay/.dockerignore
Normal file
1
packages/nostr/relay/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
1
packages/nostr/relay/.gitignore
vendored
Normal file
1
packages/nostr/relay/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
12
packages/nostr/relay/Dockerfile
Normal file
12
packages/nostr/relay/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM scsibug/nostr-rs-relay
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN apt-get update && apt-get install -y curl nodejs npm
|
||||||
|
RUN npm i -g yarn
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
USER $APP_USER
|
||||||
|
RUN yarn
|
||||||
|
CMD yarn app /bin/bash -c "rm -rf /usr/src/app/db/* && ./nostr-rs-relay --db /usr/src/app/db"
|
16
packages/nostr/relay/index.ts
Normal file
16
packages/nostr/relay/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import http from "node:http"
|
||||||
|
import { spawn } from "node:child_process"
|
||||||
|
|
||||||
|
const child = spawn(process.argv[2], process.argv.slice(3), {
|
||||||
|
stdio: "inherit",
|
||||||
|
})
|
||||||
|
|
||||||
|
const server = http.createServer((_, res) => {
|
||||||
|
if (!child.kill(9)) {
|
||||||
|
console.error("killing the subprocess failed")
|
||||||
|
}
|
||||||
|
res.end()
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(8000)
|
14
packages/nostr/relay/package.json
Normal file
14
packages/nostr/relay/package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "relay",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"app": "ts-node index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^18.15.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { ProtocolError } from "../error"
|
import { ProtocolError } from "../error"
|
||||||
import { Filters, SubscriptionId } from "."
|
import { Filters, SubscriptionId } from "."
|
||||||
import { EventId, RawEvent } from "../event"
|
import { EventId, RawEvent } from "../event"
|
||||||
import WebSocket from "ws"
|
import WebSocket from "isomorphic-ws"
|
||||||
import { unixTimestamp } from "../util"
|
import { unixTimestamp } from "../util"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,10 +34,12 @@ export class Conn {
|
|||||||
constructor({
|
constructor({
|
||||||
url,
|
url,
|
||||||
onMessage,
|
onMessage,
|
||||||
|
onOpen,
|
||||||
onError,
|
onError,
|
||||||
}: {
|
}: {
|
||||||
url: URL
|
url: URL
|
||||||
onMessage: (msg: IncomingMessage) => void
|
onMessage: (msg: IncomingMessage) => void
|
||||||
|
onOpen: () => void
|
||||||
onError: (err: unknown) => void
|
onError: (err: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
this.#onError = onError
|
this.#onError = onError
|
||||||
@ -66,6 +68,7 @@ export class Conn {
|
|||||||
this.send(msg)
|
this.send(msg)
|
||||||
}
|
}
|
||||||
this.#pending = []
|
this.#pending = []
|
||||||
|
onOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.#socket.addEventListener("error", (err) => {
|
this.#socket.addEventListener("error", (err) => {
|
||||||
|
@ -11,6 +11,7 @@ export class EventEmitter extends Base {
|
|||||||
eventName: "removeListener",
|
eventName: "removeListener",
|
||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
|
override addListener(eventName: "open", listener: OpenListener): this
|
||||||
override addListener(eventName: "event", listener: EventListener): this
|
override addListener(eventName: "event", listener: EventListener): this
|
||||||
override addListener(eventName: "notice", listener: NoticeListener): this
|
override addListener(eventName: "notice", listener: NoticeListener): this
|
||||||
override addListener(eventName: "ok", listener: OkListener): this
|
override addListener(eventName: "ok", listener: OkListener): this
|
||||||
@ -22,6 +23,7 @@ export class EventEmitter extends Base {
|
|||||||
|
|
||||||
override emit(eventName: "newListener", listener: NewListener): boolean
|
override emit(eventName: "newListener", listener: NewListener): boolean
|
||||||
override emit(eventName: "removeListener", listener: RemoveListener): boolean
|
override emit(eventName: "removeListener", listener: RemoveListener): boolean
|
||||||
|
override emit(eventName: "open", relay: URL, nostr: Nostr): boolean
|
||||||
override emit(eventName: "event", params: EventParams, nostr: Nostr): boolean
|
override emit(eventName: "event", params: EventParams, nostr: Nostr): boolean
|
||||||
override emit(eventName: "notice", notice: string, nostr: Nostr): boolean
|
override emit(eventName: "notice", notice: string, nostr: Nostr): boolean
|
||||||
override emit(eventName: "ok", params: OkParams, nostr: Nostr): boolean
|
override emit(eventName: "ok", params: OkParams, nostr: Nostr): boolean
|
||||||
@ -41,6 +43,7 @@ export class EventEmitter extends Base {
|
|||||||
|
|
||||||
override listeners(eventName: "newListener"): EventListener[]
|
override listeners(eventName: "newListener"): EventListener[]
|
||||||
override listeners(eventName: "removeListener"): EventListener[]
|
override listeners(eventName: "removeListener"): EventListener[]
|
||||||
|
override listeners(eventName: "open"): OpenListener[]
|
||||||
override listeners(eventName: "event"): EventListener[]
|
override listeners(eventName: "event"): EventListener[]
|
||||||
override listeners(eventName: "notice"): NoticeListener[]
|
override listeners(eventName: "notice"): NoticeListener[]
|
||||||
override listeners(eventName: "ok"): OkListener[]
|
override listeners(eventName: "ok"): OkListener[]
|
||||||
@ -52,6 +55,7 @@ export class EventEmitter extends Base {
|
|||||||
|
|
||||||
override off(eventName: "newListener", listener: NewListener): this
|
override off(eventName: "newListener", listener: NewListener): this
|
||||||
override off(eventName: "removeListener", listener: RemoveListener): this
|
override off(eventName: "removeListener", listener: RemoveListener): this
|
||||||
|
override off(eventName: "open", listener: OpenListener): this
|
||||||
override off(eventName: "event", listener: EventListener): this
|
override off(eventName: "event", listener: EventListener): this
|
||||||
override off(eventName: "notice", listener: NoticeListener): this
|
override off(eventName: "notice", listener: NoticeListener): this
|
||||||
override off(eventName: "ok", listener: OkListener): this
|
override off(eventName: "ok", listener: OkListener): this
|
||||||
@ -63,6 +67,7 @@ export class EventEmitter extends Base {
|
|||||||
|
|
||||||
override on(eventName: "newListener", listener: NewListener): this
|
override on(eventName: "newListener", listener: NewListener): this
|
||||||
override on(eventName: "removeListener", listener: RemoveListener): this
|
override on(eventName: "removeListener", listener: RemoveListener): this
|
||||||
|
override on(eventName: "open", listener: OpenListener): this
|
||||||
override on(eventName: "event", listener: EventListener): this
|
override on(eventName: "event", listener: EventListener): this
|
||||||
override on(eventName: "notice", listener: NoticeListener): this
|
override on(eventName: "notice", listener: NoticeListener): this
|
||||||
override on(eventName: "ok", listener: OkListener): this
|
override on(eventName: "ok", listener: OkListener): this
|
||||||
@ -74,6 +79,7 @@ export class EventEmitter extends Base {
|
|||||||
|
|
||||||
override once(eventName: "newListener", listener: NewListener): this
|
override once(eventName: "newListener", listener: NewListener): this
|
||||||
override once(eventName: "removeListener", listener: RemoveListener): this
|
override once(eventName: "removeListener", listener: RemoveListener): this
|
||||||
|
override once(eventName: "open", listener: OpenListener): this
|
||||||
override once(eventName: "event", listener: EventListener): this
|
override once(eventName: "event", listener: EventListener): this
|
||||||
override once(eventName: "notice", listener: NoticeListener): this
|
override once(eventName: "notice", listener: NoticeListener): this
|
||||||
override once(eventName: "ok", listener: OkListener): this
|
override once(eventName: "ok", listener: OkListener): this
|
||||||
@ -91,6 +97,7 @@ export class EventEmitter extends Base {
|
|||||||
eventName: "removeListener",
|
eventName: "removeListener",
|
||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
|
override prependListener(eventName: "open", listener: OpenListener): this
|
||||||
override prependListener(eventName: "event", listener: EventListener): this
|
override prependListener(eventName: "event", listener: EventListener): this
|
||||||
override prependListener(eventName: "notice", listener: NoticeListener): this
|
override prependListener(eventName: "notice", listener: NoticeListener): this
|
||||||
override prependListener(eventName: "ok", listener: OkListener): this
|
override prependListener(eventName: "ok", listener: OkListener): this
|
||||||
@ -108,6 +115,7 @@ export class EventEmitter extends Base {
|
|||||||
eventName: "removeListener",
|
eventName: "removeListener",
|
||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
|
override prependOnceListener(eventName: "open", listener: OpenListener): this
|
||||||
override prependOnceListener(
|
override prependOnceListener(
|
||||||
eventName: "event",
|
eventName: "event",
|
||||||
listener: EventListener
|
listener: EventListener
|
||||||
@ -135,6 +143,7 @@ export class EventEmitter extends Base {
|
|||||||
eventName: "removeListener",
|
eventName: "removeListener",
|
||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
|
override removeListener(eventName: "open", listener: OpenListener): this
|
||||||
override removeListener(eventName: "event", listener: EventListener): this
|
override removeListener(eventName: "event", listener: EventListener): this
|
||||||
override removeListener(eventName: "notice", listener: NoticeListener): this
|
override removeListener(eventName: "notice", listener: NoticeListener): this
|
||||||
override removeListener(eventName: "ok", listener: OkListener): this
|
override removeListener(eventName: "ok", listener: OkListener): this
|
||||||
@ -152,9 +161,12 @@ export class EventEmitter extends Base {
|
|||||||
// emitter[Symbol.for('nodejs.rejection')](err, eventName[, ...args]) shenanigans?
|
// emitter[Symbol.for('nodejs.rejection')](err, eventName[, ...args]) shenanigans?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Add an open event
|
||||||
|
// TODO Refactor the params
|
||||||
type EventName =
|
type EventName =
|
||||||
| "newListener"
|
| "newListener"
|
||||||
| "removeListener"
|
| "removeListener"
|
||||||
|
| "open"
|
||||||
| "event"
|
| "event"
|
||||||
| "notice"
|
| "notice"
|
||||||
| "ok"
|
| "ok"
|
||||||
@ -163,6 +175,7 @@ type EventName =
|
|||||||
|
|
||||||
type NewListener = (eventName: EventName, listener: Listener) => void
|
type NewListener = (eventName: EventName, listener: Listener) => void
|
||||||
type RemoveListener = (eventName: EventName, listener: Listener) => void
|
type RemoveListener = (eventName: EventName, listener: Listener) => void
|
||||||
|
type OpenListener = (relay: URL, nostr: Nostr) => void
|
||||||
type EventListener = (params: EventParams, nostr: Nostr) => void
|
type EventListener = (params: EventParams, nostr: Nostr) => void
|
||||||
type NoticeListener = (notice: string, nostr: Nostr) => void
|
type NoticeListener = (notice: string, nostr: Nostr) => void
|
||||||
type OkListener = (params: OkParams, nostr: Nostr) => void
|
type OkListener = (params: OkParams, nostr: Nostr) => void
|
||||||
@ -172,6 +185,7 @@ type ErrorListener = (error: unknown, nostr: Nostr) => void
|
|||||||
type Listener =
|
type Listener =
|
||||||
| NewListener
|
| NewListener
|
||||||
| RemoveListener
|
| RemoveListener
|
||||||
|
| OpenListener
|
||||||
| EventListener
|
| EventListener
|
||||||
| NoticeListener
|
| NoticeListener
|
||||||
| OkListener
|
| OkListener
|
||||||
|
@ -85,6 +85,8 @@ export class Nostr extends EventEmitter {
|
|||||||
this.emit("error", err, this)
|
this.emit("error", err, this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Forward "open" events.
|
||||||
|
onOpen: () => this.emit("open", connUrl, this),
|
||||||
// Forward errors on this connection.
|
// Forward errors on this connection.
|
||||||
onError: (err) => this.emit("error", err, this),
|
onError: (err) => this.emit("error", err, this),
|
||||||
})
|
})
|
||||||
|
@ -104,7 +104,7 @@ export async function aesEncryptBase64(
|
|||||||
plaintext: string
|
plaintext: string
|
||||||
): Promise<AesEncryptedBase64> {
|
): Promise<AesEncryptedBase64> {
|
||||||
const sharedPoint = secp.getSharedSecret(sender, "02" + recipient)
|
const sharedPoint = secp.getSharedSecret(sender, "02" + recipient)
|
||||||
const sharedKey = sharedPoint.slice(2, 33)
|
const sharedKey = sharedPoint.slice(1, 33)
|
||||||
if (typeof window === "object") {
|
if (typeof window === "object") {
|
||||||
const key = await window.crypto.subtle.importKey(
|
const key = await window.crypto.subtle.importKey(
|
||||||
"raw",
|
"raw",
|
||||||
@ -141,7 +141,7 @@ export async function aesEncryptBase64(
|
|||||||
)
|
)
|
||||||
let encrypted = cipher.update(plaintext, "utf8", "base64")
|
let encrypted = cipher.update(plaintext, "utf8", "base64")
|
||||||
// TODO Could save an allocation here by avoiding the +=
|
// TODO Could save an allocation here by avoiding the +=
|
||||||
encrypted += cipher.final()
|
encrypted += cipher.final("base64")
|
||||||
return {
|
return {
|
||||||
data: encrypted,
|
data: encrypted,
|
||||||
iv: Buffer.from(iv.buffer).toString("base64"),
|
iv: Buffer.from(iv.buffer).toString("base64"),
|
||||||
@ -155,7 +155,7 @@ export async function aesDecryptBase64(
|
|||||||
{ data, iv }: AesEncryptedBase64
|
{ data, iv }: AesEncryptedBase64
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const sharedPoint = secp.getSharedSecret(recipient, "02" + sender)
|
const sharedPoint = secp.getSharedSecret(recipient, "02" + sender)
|
||||||
const sharedKey = sharedPoint.slice(2, 33)
|
const sharedKey = sharedPoint.slice(1, 33)
|
||||||
if (typeof window === "object") {
|
if (typeof window === "object") {
|
||||||
// TODO Can copy this from the legacy code
|
// TODO Can copy this from the legacy code
|
||||||
throw new Error("todo")
|
throw new Error("todo")
|
||||||
|
@ -5,10 +5,12 @@ import {
|
|||||||
sha256,
|
sha256,
|
||||||
schnorrSign,
|
schnorrSign,
|
||||||
schnorrVerify,
|
schnorrVerify,
|
||||||
|
parsePublicKey,
|
||||||
aesDecryptBase64,
|
aesDecryptBase64,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
HexOrBechPrivateKey,
|
HexOrBechPrivateKey,
|
||||||
parsePrivateKey,
|
parsePrivateKey,
|
||||||
|
aesEncryptBase64,
|
||||||
} from "./crypto"
|
} from "./crypto"
|
||||||
import { defined, unixTimestamp } from "./util"
|
import { defined, unixTimestamp } from "./util"
|
||||||
|
|
||||||
@ -64,7 +66,7 @@ interface DirectMessage extends RawEvent {
|
|||||||
/**
|
/**
|
||||||
* Get the message plaintext, or undefined if this client is not the recipient.
|
* Get the message plaintext, or undefined if this client is not the recipient.
|
||||||
*/
|
*/
|
||||||
getMessage(recipient: PrivateKey): Promise<string | undefined>
|
getMessage(priv?: HexOrBechPrivateKey): Promise<string | undefined>
|
||||||
/**
|
/**
|
||||||
* Get the recipient pubkey.
|
* Get the recipient pubkey.
|
||||||
*/
|
*/
|
||||||
@ -129,7 +131,7 @@ export async function signEvent<T extends Event | RawEvent>(
|
|||||||
if (priv !== undefined) {
|
if (priv !== undefined) {
|
||||||
priv = parsePrivateKey(priv)
|
priv = parsePrivateKey(priv)
|
||||||
event.pubkey = getPublicKey(priv)
|
event.pubkey = getPublicKey(priv)
|
||||||
const id = await calculateEventId(event as UnsignedWithPubkey<T>)
|
const id = await serializeEventId(event as UnsignedWithPubkey<T>)
|
||||||
event.id = id
|
event.id = id
|
||||||
event.sig = await schnorrSign(id, priv)
|
event.sig = await schnorrSign(id, priv)
|
||||||
return event as T
|
return event as T
|
||||||
@ -158,16 +160,44 @@ export function createSetMetadata(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO This is incomplete
|
||||||
|
// TODO Since you already have the private key, maybe this should return the message already signed?
|
||||||
|
// Think about this more
|
||||||
|
// Perhaps the best option is for all these factory methods to have an overload which also accept a private
|
||||||
|
// key as last parameter and return the event already signed, whereas for this method that would be
|
||||||
|
// mandatory
|
||||||
|
export async function createDirectMessage({
|
||||||
|
message,
|
||||||
|
recipient,
|
||||||
|
priv,
|
||||||
|
}: {
|
||||||
|
message: string
|
||||||
|
recipient: PublicKey
|
||||||
|
priv: PrivateKey
|
||||||
|
}): Promise<Unsigned<DirectMessage>> {
|
||||||
|
recipient = parsePublicKey(recipient)
|
||||||
|
priv = parsePrivateKey(priv)
|
||||||
|
const { data, iv } = await aesEncryptBase64(priv, recipient, message)
|
||||||
|
return {
|
||||||
|
kind: EventKind.DirectMessage,
|
||||||
|
tags: [["p", recipient]],
|
||||||
|
content: `${data}?iv=${iv}`,
|
||||||
|
getMessage,
|
||||||
|
getRecipient,
|
||||||
|
getPrevious,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an event from its raw format.
|
* Parse an event from its raw format.
|
||||||
*/
|
*/
|
||||||
export async function parseEvent(event: RawEvent): Promise<Event> {
|
export async function parseEvent(event: RawEvent): Promise<Event> {
|
||||||
// TODO Validate all the fields. Lowercase hex fields, etc. Make sure everything is correct.
|
// TODO Validate all the fields. Lowercase hex fields, etc. Make sure everything is correct.
|
||||||
if (event.id !== (await calculateEventId(event))) {
|
if (event.id !== (await serializeEventId(event))) {
|
||||||
throw new ProtocolError(
|
throw new ProtocolError(
|
||||||
`invalid id ${event.id} for event ${JSON.stringify(
|
`invalid id ${event.id} for event ${JSON.stringify(
|
||||||
event
|
event
|
||||||
)}, expected ${await calculateEventId(event)}`
|
)}, expected ${await serializeEventId(event)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!(await schnorrVerify(event.sig, event.id, event.pubkey))) {
|
if (!(await schnorrVerify(event.sig, event.id, event.pubkey))) {
|
||||||
@ -207,7 +237,7 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function calculateEventId(
|
async function serializeEventId(
|
||||||
event: UnsignedWithPubkey<RawEvent>
|
event: UnsignedWithPubkey<RawEvent>
|
||||||
): Promise<EventId> {
|
): Promise<EventId> {
|
||||||
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
|
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
|
||||||
@ -237,8 +267,11 @@ function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
|
|||||||
|
|
||||||
async function getMessage(
|
async function getMessage(
|
||||||
this: UnsignedWithPubkey<DirectMessage>,
|
this: UnsignedWithPubkey<DirectMessage>,
|
||||||
priv?: PrivateKey
|
priv?: HexOrBechPrivateKey
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
|
if (priv !== undefined) {
|
||||||
|
priv = parsePrivateKey(priv)
|
||||||
|
}
|
||||||
const [data, iv] = this.content.split("?iv=")
|
const [data, iv] = this.content.split("?iv=")
|
||||||
if (data === undefined || iv === undefined) {
|
if (data === undefined || iv === undefined) {
|
||||||
throw new ProtocolError(`invalid direct message content ${this.content}`)
|
throw new ProtocolError(`invalid direct message content ${this.content}`)
|
||||||
|
125
packages/nostr/test/dm.ts
Normal file
125
packages/nostr/test/dm.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { createDirectMessage, EventKind, signEvent } from "../src/event"
|
||||||
|
import { parsePublicKey } from "../src/crypto"
|
||||||
|
import assert from "assert"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
|
||||||
|
describe("dm", async function () {
|
||||||
|
const message = "for your eyes only"
|
||||||
|
|
||||||
|
// Test that the intended recipient can receive and decrypt the direct message.
|
||||||
|
it("to intended recipient", (done) => {
|
||||||
|
setup(done).then(
|
||||||
|
({
|
||||||
|
publisher,
|
||||||
|
publisherPubkey,
|
||||||
|
publisherSecret,
|
||||||
|
subscriber,
|
||||||
|
subscriberPubkey,
|
||||||
|
subscriberSecret,
|
||||||
|
timestamp,
|
||||||
|
}) => {
|
||||||
|
// Expect the direct message.
|
||||||
|
subscriber.on(
|
||||||
|
"event",
|
||||||
|
async ({ event, subscriptionId: actualSubscriptionId }, nostr) => {
|
||||||
|
try {
|
||||||
|
assert.equal(nostr, subscriber)
|
||||||
|
assert.equal(event.kind, EventKind.DirectMessage)
|
||||||
|
assert.equal(event.pubkey, parsePublicKey(publisherPubkey))
|
||||||
|
assert.equal(actualSubscriptionId, subscriptionId)
|
||||||
|
assert.ok(event.created_at >= timestamp)
|
||||||
|
|
||||||
|
if (event.kind === EventKind.DirectMessage) {
|
||||||
|
assert.equal(
|
||||||
|
event.getRecipient(),
|
||||||
|
parsePublicKey(subscriberPubkey)
|
||||||
|
)
|
||||||
|
assert.equal(await event.getMessage(subscriberSecret), message)
|
||||||
|
}
|
||||||
|
|
||||||
|
done()
|
||||||
|
} catch (e) {
|
||||||
|
done(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionId = subscriber.subscribe([])
|
||||||
|
|
||||||
|
subscriber.on("eose", async () => {
|
||||||
|
// TODO No signEvent, do something more convenient
|
||||||
|
const event = await signEvent(
|
||||||
|
await createDirectMessage({
|
||||||
|
message,
|
||||||
|
recipient: subscriberPubkey,
|
||||||
|
priv: publisherSecret,
|
||||||
|
}),
|
||||||
|
publisherSecret
|
||||||
|
)
|
||||||
|
publisher.publish(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that an unintended recipient still receives the direct message event, but cannot decrypt it.
|
||||||
|
it.only("to unintended recipient", (done) => {
|
||||||
|
setup(done).then(
|
||||||
|
({
|
||||||
|
publisher,
|
||||||
|
publisherPubkey,
|
||||||
|
publisherSecret,
|
||||||
|
subscriber,
|
||||||
|
subscriberSecret,
|
||||||
|
timestamp,
|
||||||
|
}) => {
|
||||||
|
const recipientPubkey =
|
||||||
|
"npub1u2dl3scpzuwyd45flgtm3wcjgv20j4azuzgevdpgtsvvmqzvc63sz327gc"
|
||||||
|
|
||||||
|
// Expect the direct message.
|
||||||
|
subscriber.on(
|
||||||
|
"event",
|
||||||
|
async ({ event, subscriptionId: actualSubscriptionId }, nostr) => {
|
||||||
|
try {
|
||||||
|
assert.equal(nostr, subscriber)
|
||||||
|
assert.equal(event.kind, EventKind.DirectMessage)
|
||||||
|
assert.equal(event.pubkey, parsePublicKey(publisherPubkey))
|
||||||
|
assert.equal(actualSubscriptionId, subscriptionId)
|
||||||
|
assert.ok(event.created_at >= timestamp)
|
||||||
|
|
||||||
|
if (event.kind === EventKind.DirectMessage) {
|
||||||
|
assert.equal(
|
||||||
|
event.getRecipient(),
|
||||||
|
parsePublicKey(recipientPubkey)
|
||||||
|
)
|
||||||
|
assert.strictEqual(
|
||||||
|
await event.getMessage(subscriberSecret),
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
done()
|
||||||
|
} catch (e) {
|
||||||
|
done(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionId = subscriber.subscribe([])
|
||||||
|
|
||||||
|
subscriber.on("eose", async () => {
|
||||||
|
// TODO No signEvent, do something more convenient
|
||||||
|
const event = await signEvent(
|
||||||
|
await createDirectMessage({
|
||||||
|
message,
|
||||||
|
recipient: recipientPubkey,
|
||||||
|
priv: publisherSecret,
|
||||||
|
}),
|
||||||
|
publisherSecret
|
||||||
|
)
|
||||||
|
publisher.publish(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
70
packages/nostr/test/setup.ts
Normal file
70
packages/nostr/test/setup.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Nostr } from "../src/client"
|
||||||
|
import { unixTimestamp } from "../src/util"
|
||||||
|
|
||||||
|
export interface Setup {
|
||||||
|
publisher: Nostr
|
||||||
|
publisherSecret: string
|
||||||
|
publisherPubkey: string
|
||||||
|
subscriber: Nostr
|
||||||
|
subscriberSecret: string
|
||||||
|
subscriberPubkey: string
|
||||||
|
timestamp: number
|
||||||
|
url: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setup(done: jest.DoneCallback): Promise<Setup> {
|
||||||
|
await restartRelay()
|
||||||
|
const publisher = new Nostr()
|
||||||
|
const subscriber = new Nostr()
|
||||||
|
const url = new URL("ws://localhost:12648")
|
||||||
|
|
||||||
|
publisher.on("error", done)
|
||||||
|
subscriber.on("error", done)
|
||||||
|
|
||||||
|
publisher.open(url)
|
||||||
|
subscriber.open(url)
|
||||||
|
|
||||||
|
return {
|
||||||
|
publisher,
|
||||||
|
publisherSecret:
|
||||||
|
"nsec15fnff4uxlgyu79ua3l7327w0wstrd6x565cx6zze78zgkktmr8vs90j363",
|
||||||
|
publisherPubkey:
|
||||||
|
"npub1he978sxy7tgc7yfp2zra05v045kfuqnfl3gwr82jd00mzxjj9fjqzw2dg7",
|
||||||
|
subscriber,
|
||||||
|
subscriberSecret:
|
||||||
|
"nsec1fxvlyqn3rugvxwaz6dr5h8jcfn0fe0lxyp7pl4mgntxfzqr7dmgst7z9ps",
|
||||||
|
subscriberPubkey:
|
||||||
|
"npub1mtwskm558jugtj724nsgf3jf80c5adl39ttydngrn48250l6xmjqa00yxd",
|
||||||
|
timestamp: unixTimestamp(),
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartRelay() {
|
||||||
|
// Make a request to the endpoint which will crash the process and cause it to restart.
|
||||||
|
try {
|
||||||
|
await fetch("http://localhost:12649")
|
||||||
|
} catch (e) {
|
||||||
|
// Since the process exits, an error is expected.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the relay process is ready.
|
||||||
|
for (;;) {
|
||||||
|
const ok = await new Promise((resolve) => {
|
||||||
|
const nostr = new Nostr()
|
||||||
|
nostr.on("error", () => {
|
||||||
|
nostr.close()
|
||||||
|
resolve(false)
|
||||||
|
})
|
||||||
|
nostr.on("open", () => {
|
||||||
|
nostr.close()
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
nostr.open("ws://localhost:12648")
|
||||||
|
})
|
||||||
|
if (ok) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
}
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
import { Nostr } from "../src/client"
|
|
||||||
import { createTextNote, EventKind, signEvent } from "../src/event"
|
|
||||||
import { getPublicKey } from "../src/crypto"
|
|
||||||
import assert from "assert"
|
|
||||||
import { unixTimestamp } from "../src/util"
|
|
||||||
|
|
||||||
describe("simple communication", function () {
|
|
||||||
const secret =
|
|
||||||
"nsec1xlu55y6fqfgrq448xslt6a8j2rh7lj08hyhgs94ryq04yf6surwsjl0kzh"
|
|
||||||
const pubkey = getPublicKey(secret)
|
|
||||||
const note = "hello world"
|
|
||||||
const url = new URL("ws://localhost:12648")
|
|
||||||
const timestamp = unixTimestamp()
|
|
||||||
|
|
||||||
const publisher = new Nostr()
|
|
||||||
const subscriber = new Nostr()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
publisher.open(url)
|
|
||||||
subscriber.open(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
publisher.close()
|
|
||||||
subscriber.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("publish and receive", function (done) {
|
|
||||||
subscriber.on("error", done)
|
|
||||||
publisher.on("error", done)
|
|
||||||
|
|
||||||
// Expect the test event.
|
|
||||||
subscriber.on("event", ({ event }, nostr) => {
|
|
||||||
assert.equal(nostr, subscriber)
|
|
||||||
assert.equal(event.kind, EventKind.TextNote)
|
|
||||||
assert.equal(event.pubkey, pubkey)
|
|
||||||
assert.equal(event.created_at, timestamp)
|
|
||||||
assert.equal(event.content, note)
|
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
|
|
||||||
const subscriptionId = subscriber.subscribe([])
|
|
||||||
|
|
||||||
// After the subscription event sync is done, publish the test event.
|
|
||||||
subscriber.on("eose", (id, nostr) => {
|
|
||||||
assert.equal(nostr, subscriber)
|
|
||||||
assert.equal(id, subscriptionId)
|
|
||||||
|
|
||||||
signEvent(
|
|
||||||
{
|
|
||||||
...createTextNote(note),
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
secret
|
|
||||||
).then((event) => publisher.publish(event))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO Have a way to run the relay on-demand and then re-add this test
|
|
||||||
/*
|
|
||||||
it("publish and ok", function (done) {
|
|
||||||
signEvent(
|
|
||||||
{
|
|
||||||
...createTextNote(note),
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
secret
|
|
||||||
).then((event) => {
|
|
||||||
publisher.on("ok", (params, nostr) => {
|
|
||||||
assert.equal(nostr, publisher)
|
|
||||||
assert.equal(params.eventId, event.id)
|
|
||||||
assert.equal(params.relay.toString(), url.toString())
|
|
||||||
assert.equal(params.ok, true)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
publisher.on("error", done)
|
|
||||||
publisher.publish(event)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
})
|
|
75
packages/nostr/test/text-note.ts
Normal file
75
packages/nostr/test/text-note.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { createTextNote, EventKind, signEvent } from "../src/event"
|
||||||
|
import { parsePublicKey } from "../src/crypto"
|
||||||
|
import assert from "assert"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
|
||||||
|
describe("text note", async function () {
|
||||||
|
const note = "hello world"
|
||||||
|
|
||||||
|
it("publish and receive", (done) => {
|
||||||
|
// Test that a text note can be published by one client and received by the other.
|
||||||
|
setup(done).then(
|
||||||
|
({
|
||||||
|
publisher,
|
||||||
|
publisherSecret,
|
||||||
|
publisherPubkey,
|
||||||
|
subscriber,
|
||||||
|
timestamp,
|
||||||
|
}) => {
|
||||||
|
// Expect the test event.
|
||||||
|
subscriber.on(
|
||||||
|
"event",
|
||||||
|
({ event, subscriptionId: actualSubscriptionId }, nostr) => {
|
||||||
|
assert.strictEqual(nostr, subscriber)
|
||||||
|
assert.strictEqual(event.kind, EventKind.TextNote)
|
||||||
|
assert.strictEqual(event.pubkey, parsePublicKey(publisherPubkey))
|
||||||
|
assert.strictEqual(event.created_at, timestamp)
|
||||||
|
assert.strictEqual(event.content, note)
|
||||||
|
assert.strictEqual(actualSubscriptionId, subscriptionId)
|
||||||
|
|
||||||
|
subscriber.close()
|
||||||
|
publisher.close()
|
||||||
|
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionId = subscriber.subscribe([])
|
||||||
|
|
||||||
|
// After the subscription event sync is done, publish the test event.
|
||||||
|
subscriber.on("eose", (id, nostr) => {
|
||||||
|
assert.strictEqual(nostr, subscriber)
|
||||||
|
assert.strictEqual(id, subscriptionId)
|
||||||
|
|
||||||
|
// TODO No signEvent, have a convenient way to do this
|
||||||
|
signEvent(
|
||||||
|
{ ...createTextNote(note), created_at: timestamp },
|
||||||
|
publisherSecret
|
||||||
|
).then((event) => publisher.publish(event))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("publish and ok", function (done) {
|
||||||
|
// Test that a client interprets an "OK" message after publishing a text note.
|
||||||
|
setup(done).then(({ publisher, subscriber, publisherSecret, url }) => {
|
||||||
|
// TODO No signEvent, have a convenient way to do this
|
||||||
|
signEvent(createTextNote(note), publisherSecret).then((event) => {
|
||||||
|
publisher.on("ok", (params, nostr) => {
|
||||||
|
assert.equal(nostr, publisher)
|
||||||
|
assert.equal(params.eventId, event.id)
|
||||||
|
assert.equal(params.relay.toString(), url.toString())
|
||||||
|
assert.equal(params.ok, true)
|
||||||
|
|
||||||
|
publisher.close()
|
||||||
|
subscriber.close()
|
||||||
|
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
publisher.publish(event)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user