diff --git a/example/index.tsx b/example/index.tsx index 4b25a92..5aaf5b2 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -12,36 +12,6 @@ const App = () => { useEffect(() => { (async () => { -/* const webSK = "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3"; - const webPK = getPublicKey(webSK); - console.log('webPk', webPK); - - const sessionWeb = new Session({ - target: webPK, - relay: 'wss://nostr.vulpem.com', - metadata: { - name: 'My Website', - description: 'lorem ipsum dolor sit amet', - url: 'https://vulpem.com', - icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'], - } - }); - sessionWeb.on(ConnectMessageType.PAIRED, (msg: any) => { - console.log('paired event', msg); - }); - await sessionWeb.listen(webSK); - - // mobile app (this can be a child key) - const sessionMobile = Session.fromConnectURI(sessionWeb.connectURI);// 'nostr://connect?target=...&metadata=...' - const mobileSK = "ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c"; - const mobilePK = getPublicKey(mobileSK); - console.log('mobilePK', mobilePK); - - // we define the behavior of the mobile app for each requests - await sessionMobile.listen(mobileSK); - await sessionMobile.pair(mobileSK); - - */ })(); }, []); diff --git a/src/connect.ts b/src/connect.ts index db7e340..8522f10 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -1,11 +1,11 @@ -import { Event, nip04, relayInit } from "nostr-tools"; -import { prepareRequest, prepareResponse } from "./event"; -import { Session, SessionStatus } from "./session"; +import { Event, nip04, relayInit } from 'nostr-tools'; +import { prepareRequest } from './event'; +import { Session, SessionStatus } from './session'; export interface ConnectMessage { - type: ConnectMessageType, - value?: any - requestID?: string, + type: ConnectMessageType; + value?: any; + requestID?: string; } export enum ConnectMessageType { @@ -16,28 +16,30 @@ export enum ConnectMessageType { } export interface PairingACK extends ConnectMessage { - type: ConnectMessageType.PAIRED, + type: ConnectMessageType.PAIRED; value: { - pubkey: string, - } + pubkey: string; + }; } export interface PairingNACK extends ConnectMessage { - type: ConnectMessageType.UNPAIRED + type: ConnectMessageType.UNPAIRED; } export interface GetPublicKeyRequest extends ConnectMessage { - type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST + type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST; } export interface GetPublicKeyResponse extends ConnectMessage { - type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE, + type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE; value: { - pubkey: string, - } + pubkey: string; + }; } -export function responseTypeForRequestType(type: ConnectMessageType): ConnectMessageType { +export function responseTypeForRequestType( + type: ConnectMessageType +): ConnectMessageType { switch (type) { case ConnectMessageType.GET_PUBLIC_KEY_REQUEST: return ConnectMessageType.GET_PUBLIC_KEY_RESPONSE; @@ -62,14 +64,20 @@ export class Connect { } async sendMessage(message: ConnectMessage): Promise { - if (this.session.status !== SessionStatus.PAIRED) throw new Error('Session is not paired'); + if (this.session.status !== SessionStatus.PAIRED) + throw new Error('Session is not paired'); if (!this.session.target) throw new Error('Target is required'); if (!this.session.remote) throw new Error('Remote is required'); const { target, remote } = this.session; // send request to remote - const {event, requestID} = await prepareRequest(target, remote, message, this.targetPrivateKey); + const { event, requestID } = await prepareRequest( + target, + remote, + message, + this.targetPrivateKey + ); console.log(`sending message ${message.type} with requestID ${requestID}`); const id = await this.session.sendEvent(event, this.targetPrivateKey); if (!id) throw new Error('Failed to send message ' + message.type); @@ -79,48 +87,55 @@ export class Connect { await relay.connect(); return new Promise((resolve, reject) => { - relay.on('error', () => { + relay.on('error', () => { reject(`failed to connect to ${relay.url}`); }); - + // waiting for response from remote - let sub = relay.sub([{ - kinds: [4], - authors: [remote], - //since: now, - "#p": [target], - limit: 1, - }]); - - + let sub = relay.sub([ + { + kinds: [4], + authors: [remote], + //since: now, + '#p': [target], + limit: 1, + }, + ]); + sub.on('event', async (event: Event) => { - const plaintext = await nip04.decrypt(this.targetPrivateKey, event.pubkey, event.content); + const plaintext = await nip04.decrypt( + this.targetPrivateKey, + event.pubkey, + event.content + ); console.log('plaintext', plaintext); console.log('requestID', requestID); const payload = JSON.parse(plaintext); if (!payload) return; - if (!Object.keys(payload).includes('requestID') || !Object.keys(payload).includes('message')) return; + if ( + !Object.keys(payload).includes('requestID') || + !Object.keys(payload).includes('message') + ) + return; if (payload.requestID !== requestID) return; const msg = payload.message as ConnectMessage; const responseType = responseTypeForRequestType(msg.type); if (msg.type !== responseType) return; resolve(msg); }); - + sub.on('eose', () => { sub.unsub(); }); }); - } - - async getPublicKey(): Promise { const response: ConnectMessage = await this.sendMessage({ - type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST + type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST, }); - if (response.type !== ConnectMessageType.GET_PUBLIC_KEY_RESPONSE) throw new Error('Invalid response type'); + if (response.type !== ConnectMessageType.GET_PUBLIC_KEY_RESPONSE) + throw new Error('Invalid response type'); return response.value.pubkey; } @@ -128,7 +143,9 @@ export class Connect { throw new Error('Not implemented'); } - async getRelays(): Promise<{ [url: string]: { read: boolean, write: boolean } }> { + async getRelays(): Promise<{ + [url: string]: { read: boolean; write: boolean }; + }> { throw new Error('Not implemented'); } @@ -138,11 +155,10 @@ export class Connect { }, decrypt: async (_pubkey: string, _ciphertext: string): Promise => { throw new Error('Not implemented'); - } - } + }, + }; - async request(_opts: { method: string, params: any }): Promise { + async request(_opts: { method: string; params: any }): Promise { throw new Error('Not implemented'); } - -} \ No newline at end of file +} diff --git a/src/event.ts b/src/event.ts index 09c3a56..29d1bc5 100644 --- a/src/event.ts +++ b/src/event.ts @@ -1,36 +1,23 @@ -import { nip04, Event } from "nostr-tools"; -import { ConnectMessage } from "./connect"; +import { nip04, Event } from 'nostr-tools'; +import { ConnectMessage } from './connect'; -export async function prepareRequest(from: string, to: string, request: ConnectMessage, fromSecretKey: string) { +export async function prepareRequest( + from: string, + to: string, + request: ConnectMessage, + fromSecretKey: string +) { const now = Math.floor(Date.now() / 1000); - const requestID = Math.random().toString().slice(2); + const requestID = Math.random() + .toString() + .slice(2); const cipherText = await nip04.encrypt( - fromSecretKey, - to, + fromSecretKey, + to, JSON.stringify({ requestID: requestID, request, - }), - ); - const event: Event = { - kind: 4, - created_at: now, - pubkey: from, - tags: [['p', to]], - content: cipherText, - }; - return {event, requestID}; -} - -export async function prepareResponse(requestID: string, from: string, to: string, response: ConnectMessage, fromSecretKey: string) { - const now = Math.floor(Date.now() / 1000); - const cipherText = await nip04.encrypt( - fromSecretKey, - to, - JSON.stringify({ - requestID: requestID, - response, - }), + }) ); const event: Event = { kind: 4, @@ -40,4 +27,30 @@ export async function prepareResponse(requestID: string, from: string, to: strin content: cipherText, }; return { event, requestID }; -} \ No newline at end of file +} + +export async function prepareResponse( + requestID: string, + from: string, + to: string, + response: ConnectMessage, + fromSecretKey: string +) { + const now = Math.floor(Date.now() / 1000); + const cipherText = await nip04.encrypt( + fromSecretKey, + to, + JSON.stringify({ + requestID: requestID, + response, + }) + ); + const event: Event = { + kind: 4, + created_at: now, + pubkey: from, + tags: [['p', to]], + content: cipherText, + }; + return { event, requestID }; +} diff --git a/src/index.ts b/src/index.ts index d291a61..fd3ee90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ export * from './session'; -export * from './connect'; \ No newline at end of file +export * from './connect'; diff --git a/src/request.js b/src/request.js deleted file mode 100644 index b6cfa68..0000000 --- a/src/request.js +++ /dev/null @@ -1,314 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -exports.__esModule = true; -exports.prepareEvent = exports.prepareResponse = exports.prepareRequest = exports.randomID = exports.now = exports.NostrRPCServer = exports.NostrRPC = void 0; -var nostr_tools_1 = require("nostr-tools"); -var NostrRPC = /** @class */ (function () { - function NostrRPC(opts) { - this.relay = (0, nostr_tools_1.relayInit)(opts.relay || "wss://nostr.vulpem.com"); - this.target = opts.target; - this.self = { - pubkey: (0, nostr_tools_1.getPublicKey)(opts.secretKey), - secret: opts.secretKey - }; - } - NostrRPC.prototype.call = function (_a) { - var _b = _a.id, id = _b === void 0 ? randomID() : _b, method = _a.method, _c = _a.params, params = _c === void 0 ? [] : _c; - return __awaiter(this, void 0, void 0, function () { - var body, event; - var _this = this; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - // connect to relay - return [4 /*yield*/, this.relay.connect()]; - case 1: - // connect to relay - _d.sent(); - this.relay.on('error', function () { throw new Error("failed to connect to ".concat(_this.relay.url)); }); - body = prepareRequest(id, method, params); - return [4 /*yield*/, prepareEvent(this.self.secret, this.target, body)]; - case 2: - event = _d.sent(); - // send request via relay - return [4 /*yield*/, new Promise(function (resolve, reject) { - var pub = _this.relay.publish(event); - pub.on('failed', reject); - pub.on('seen', resolve); - })]; - case 3: - // send request via relay - _d.sent(); - console.log("sent request to nostr id: ".concat(event.id), { id: id, method: method, params: params }); - return [2 /*return*/, new Promise(function (resolve, reject) { - // waiting for response from remote - // TODO: reject after a timeout - var sub = _this.relay.sub([{ - kinds: [4], - authors: [_this.target], - "#p": [_this.self.pubkey] - }]); - sub.on('event', function (event) { return __awaiter(_this, void 0, void 0, function () { - var plaintext, payload, ignore_1; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 2, , 3]); - return [4 /*yield*/, nostr_tools_1.nip04.decrypt(this.self.secret, event.pubkey, event.content)]; - case 1: - plaintext = _a.sent(); - payload = JSON.parse(plaintext); - return [3 /*break*/, 3]; - case 2: - ignore_1 = _a.sent(); - return [2 /*return*/]; - case 3: - // ignore all the events that are not NostrRPCResponse events - if (!plaintext) - return [2 /*return*/]; - if (!payload) - return [2 /*return*/]; - if (!Object.keys(payload).includes('id') || !Object.keys(payload).includes('error') || !Object.keys(payload).includes('result')) - return [2 /*return*/]; - // ignore all the events that are not for this request - if (payload.id !== id) - return [2 /*return*/]; - // if the response is an error, reject the promise - if (payload.error) { - reject(payload.error); - } - // if the response is a result, resolve the promise - if (payload.result) { - resolve(payload.result); - } - return [2 /*return*/]; - } - }); - }); }); - sub.on('eose', function () { - sub.unsub(); - }); - })]; - } - }); - }); - }; - return NostrRPC; -}()); -exports.NostrRPC = NostrRPC; -var NostrRPCServer = /** @class */ (function () { - function NostrRPCServer(opts) { - this.relay = (0, nostr_tools_1.relayInit)((opts === null || opts === void 0 ? void 0 : opts.relay) || "wss://nostr.vulpem.com"); - this.self = { - pubkey: (0, nostr_tools_1.getPublicKey)(opts.secretKey), - secret: opts.secretKey - }; - } - NostrRPCServer.prototype.listen = function () { - return __awaiter(this, void 0, void 0, function () { - var sub; - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, this.relay.connect()]; - case 1: - _a.sent(); - return [4 /*yield*/, new Promise(function (resolve, reject) { - _this.relay.on('connect', resolve); - _this.relay.on('error', reject); - })]; - case 2: - _a.sent(); - sub = this.relay.sub([{ - kinds: [4], - "#p": [this.self.pubkey], - since: now() - }]); - sub.on('event', function (event) { return __awaiter(_this, void 0, void 0, function () { - var plaintext, payload, ignore_2, response, body, responseEvent; - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - _a.trys.push([0, 2, , 3]); - return [4 /*yield*/, nostr_tools_1.nip04.decrypt(this.self.secret, event.pubkey, event.content)]; - case 1: - plaintext = _a.sent(); - payload = JSON.parse(plaintext); - return [3 /*break*/, 3]; - case 2: - ignore_2 = _a.sent(); - return [2 /*return*/]; - case 3: - // ignore all the events that are not NostrRPCRequest events - if (!plaintext) - return [2 /*return*/]; - if (!payload) - return [2 /*return*/]; - if (!Object.keys(payload).includes('id') || !Object.keys(payload).includes('method') || !Object.keys(payload).includes('params')) - return [2 /*return*/]; - return [4 /*yield*/, this.handleRequest(payload)]; - case 4: - response = _a.sent(); - body = prepareResponse(response.id, response.result, response.error); - return [4 /*yield*/, prepareEvent(this.self.secret, event.pubkey, body)]; - case 5: - responseEvent = _a.sent(); - console.log('response to be sent', responseEvent); - // send response via relay - return [4 /*yield*/, new Promise(function (resolve, reject) { - var pub = _this.relay.publish(responseEvent); - pub.on('failed', reject); - pub.on('seen', function () { return resolve(); }); - })]; - case 6: - // send response via relay - _a.sent(); - return [2 /*return*/]; - } - }); - }); }); - sub.on('eose', function () { - sub.unsub(); - }); - return [2 /*return*/]; - } - }); - }); - }; - NostrRPCServer.prototype.handleRequest = function (request) { - return __awaiter(this, void 0, void 0, function () { - var id, method, params, result, error, e_1; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - id = request.id, method = request.method, params = request.params; - result = null; - error = null; - _a.label = 1; - case 1: - _a.trys.push([1, 3, , 4]); - return [4 /*yield*/, this[method].apply(this, params)]; - case 2: - result = _a.sent(); - return [3 /*break*/, 4]; - case 3: - e_1 = _a.sent(); - if (e_1 instanceof Error) { - error = e_1.message; - } - else { - error = 'unknown error'; - } - return [3 /*break*/, 4]; - case 4: return [2 /*return*/, { - id: id, - result: result, - error: error - }]; - } - }); - }); - }; - return NostrRPCServer; -}()); -exports.NostrRPCServer = NostrRPCServer; -function now() { - return Math.floor(Date.now() / 1000); -} -exports.now = now; -function randomID() { - return Math.random().toString().slice(2); -} -exports.randomID = randomID; -function prepareRequest(id, method, params) { - return JSON.stringify({ - id: id, - method: method, - params: params - }); -} -exports.prepareRequest = prepareRequest; -function prepareResponse(id, result, error) { - return JSON.stringify({ - id: id, - result: result, - error: error - }); -} -exports.prepareResponse = prepareResponse; -function prepareEvent(secretKey, pubkey, content) { - return __awaiter(this, void 0, void 0, function () { - var cipherText, event, id, sig, signedEvent, ok, veryOk; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, nostr_tools_1.nip04.encrypt(secretKey, pubkey, content)]; - case 1: - cipherText = _a.sent(); - event = { - kind: 4, - created_at: now(), - pubkey: (0, nostr_tools_1.getPublicKey)(secretKey), - tags: [['p', pubkey]], - content: cipherText - }; - id = (0, nostr_tools_1.getEventHash)(event); - sig = (0, nostr_tools_1.signEvent)(event, secretKey); - signedEvent = __assign(__assign({}, event), { id: id, sig: sig }); - ok = (0, nostr_tools_1.validateEvent)(signedEvent); - veryOk = (0, nostr_tools_1.verifySignature)(signedEvent); - if (!ok || !veryOk) { - throw new Error('Event is not valid'); - } - return [2 /*return*/, signedEvent]; - } - }); - }); -} -exports.prepareEvent = prepareEvent; diff --git a/src/request.ts b/src/request.ts index d5ccbfe..af64459 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,5 +1,15 @@ -import { relayInit, Relay, getEventHash, signEvent, validateEvent, verifySignature, getPublicKey, nip04, Event, Sub } from "nostr-tools"; - +import { + relayInit, + Relay, + getEventHash, + signEvent, + validateEvent, + verifySignature, + getPublicKey, + nip04, + Event, + Sub, +} from 'nostr-tools'; export interface NostrRPCRequest { id: string; @@ -12,37 +22,39 @@ export interface NostrRPCResponse { error: any; } - export class NostrRPC { relay: Relay; - self: { pubkey: string, secret: string }; - target: string; + self: { pubkey: string; secret: string }; + // this is for implementing the response handlers for each method + [key: string]: any; - constructor(opts: { relay?: string, target: string, secretKey: string }) { - this.relay = relayInit(opts.relay || "wss://nostr.vulpem.com"); - this.target = opts.target; + constructor(opts: { relay?: string; secretKey: string }) { + this.relay = relayInit(opts.relay || 'wss://nostr.vulpem.com'); this.self = { pubkey: getPublicKey(opts.secretKey), secret: opts.secretKey, }; } - async call({ - id = randomID(), - method, - params = [], - } : { - id?: string, - method: string, - params?: any[], + async call({ + target, + request: { id = randomID(), method, params = [] }, + }: { + target: string; + request: { + id?: string; + method: string; + params?: any[]; + }; }): Promise { - // connect to relay await this.relay.connect(); - this.relay.on('error', () => { throw new Error(`failed to connect to ${this.relay.url}`) }); + this.relay.on('error', () => { + throw new Error(`failed to connect to ${this.relay.url}`); + }); // prepare request to be sent const body = prepareRequest(id, method, params); - const event = await prepareEvent(this.self.secret, this.target, body); + const event = await prepareEvent(this.self.secret, target, body); // send request via relay await new Promise((resolve, reject) => { @@ -51,35 +63,38 @@ export class NostrRPC { pub.on('seen', resolve); }); - console.log(`sent request to nostr id: ${event.id}`, { id, method, params }) + console.log(`request: nostr id: ${event.id}`, { id, method, params }); return new Promise((resolve, reject) => { - // waiting for response from remote // TODO: reject after a timeout - let sub = this.relay.sub([{ - kinds: [4], - authors: [this.target], - "#p": [this.self.pubkey], - }]); - + let sub = this.relay.sub([ + { + kinds: [4], + authors: [this.target], + '#p': [this.self.pubkey], + }, + ]); + sub.on('event', async (event: Event) => { - let plaintext; let payload; try { - plaintext = await nip04.decrypt(this.self.secret, event.pubkey, event.content); + const plaintext = await nip04.decrypt( + this.self.secret, + event.pubkey, + event.content + ); + if (!plaintext) throw new Error('failed to decrypt event'); payload = JSON.parse(plaintext); - } catch(ignore) { + } catch (ignore) { return; } // ignore all the events that are not NostrRPCResponse events - if (!plaintext) return; - if (!payload) return; - if (!Object.keys(payload).includes('id') || !Object.keys(payload).includes('error') || !Object.keys(payload).includes('result')) return; - - console.log(`received response from nostr id: ${event.id}`, payload) - + if (!isValidResponse(payload)) return; + + console.log(`received response from nostr id: ${event.id}`, payload); + // ignore all the events that are not for this request if (payload.id !== id) return; @@ -93,26 +108,13 @@ export class NostrRPC { resolve(payload.result); } }); - + sub.on('eose', () => { sub.unsub(); }); }); } -} -export class NostrRPCServer { - relay: Relay; - self: { pubkey: string, secret: string }; - [key: string]: any; // TODO: remove this [key: string] - - constructor(opts: { relay?: string, secretKey: string }) { - this.relay = relayInit(opts?.relay || "wss://nostr.vulpem.com"); - this.self = { - pubkey: getPublicKey(opts.secretKey), - secret: opts.secretKey, - }; - } async listen(): Promise { await this.relay.connect(); await new Promise((resolve, reject) => { @@ -120,33 +122,45 @@ export class NostrRPCServer { this.relay.on('error', reject); }); - let sub = this.relay.sub([{ - kinds: [4], - "#p": [this.self.pubkey], - since: now(), - }]); + let sub = this.relay.sub([ + { + kinds: [4], + '#p': [this.self.pubkey], + since: now(), + }, + ]); sub.on('event', async (event: Event) => { - let plaintext; let payload; try { - plaintext = await nip04.decrypt(this.self.secret, event.pubkey, event.content); + const plaintext = await nip04.decrypt( + this.self.secret, + event.pubkey, + event.content + ); + if (!plaintext) throw new Error('failed to decrypt event'); payload = JSON.parse(plaintext); - } catch(ignore) { + } catch (ignore) { return; } // ignore all the events that are not NostrRPCRequest events - if (!plaintext) return; - if (!payload) return; - if (!Object.keys(payload).includes('id') || !Object.keys(payload).includes('method') || !Object.keys(payload).includes('params')) return; - + if (!isValidRequest(payload)) return; + // handle request const response = await this.handleRequest(payload); - const body = prepareResponse(response.id, response.result, response.error); - const responseEvent = await prepareEvent(this.self.secret, event.pubkey, body); + const body = prepareResponse( + response.id, + response.result, + response.error + ); + const responseEvent = await prepareEvent( + this.self.secret, + event.pubkey, + body + ); - console.log('response to be sent', responseEvent) + console.log('response to be sent', responseEvent); // send response via relay const pub = this.relay.publish(responseEvent); pub.on('failed', console.error); @@ -158,17 +172,20 @@ export class NostrRPCServer { return sub; } - async handleRequest(request: NostrRPCRequest): Promise { + + private async handleRequest( + request: NostrRPCRequest + ): Promise { const { id, method, params } = request; let result = null; let error = null; try { result = await this[method](...params); - } catch(e: any) { + } catch (e) { if (e instanceof Error) { error = e.message; } else { - error = 'unknown error' + error = 'unknown error'; } } return { @@ -183,9 +200,15 @@ export function now(): number { return Math.floor(Date.now() / 1000); } export function randomID(): string { - return Math.random().toString().slice(2); + return Math.random() + .toString() + .slice(2); } -export function prepareRequest(id: string, method: string, params: any[]): string { +export function prepareRequest( + id: string, + method: string, + params: any[] +): string { return JSON.stringify({ id, method: method, @@ -199,12 +222,12 @@ export function prepareResponse(id: string, result: any, error: any): string { error: error, }); } -export async function prepareEvent(secretKey: string, pubkey: string, content: string): Promise { - const cipherText = await nip04.encrypt( - secretKey, - pubkey, - content, - ); +export async function prepareEvent( + secretKey: string, + pubkey: string, + content: string +): Promise { + const cipherText = await nip04.encrypt(secretKey, pubkey, content); const event: Event = { kind: 4, @@ -212,7 +235,7 @@ export async function prepareEvent(secretKey: string, pubkey: string, content: s pubkey: getPublicKey(secretKey), tags: [['p', pubkey]], content: cipherText, - } + }; const id = getEventHash(event); const sig = signEvent(event, secretKey); @@ -226,3 +249,31 @@ export async function prepareEvent(secretKey: string, pubkey: string, content: s return signedEvent; } + +function isValidRequest(payload: any): boolean { + if (!payload) return false; + + const keys = Object.keys(payload); + if ( + !keys.includes('id') || + !keys.includes('method') || + !keys.includes('params') + ) + return false; + + return true; +} + +function isValidResponse(payload: any): boolean { + if (!payload) return false; + + const keys = Object.keys(payload); + if ( + !keys.includes('id') || + !keys.includes('result') || + !keys.includes('error') + ) + return false; + + return true; +} diff --git a/src/session.ts b/src/session.ts index d430d94..0b15a38 100644 --- a/src/session.ts +++ b/src/session.ts @@ -7,15 +7,15 @@ import { relayInit, nip04, getPublicKey, -} from 'nostr-tools' +} from 'nostr-tools'; import { ConnectMessage, ConnectMessageType, PairingACK } from './connect'; import { prepareResponse } from './event'; export interface Metadata { - name: string, - url: string, - description?: string - icons?: string[], + name: string; + url: string; + description?: string; + icons?: string[]; } export enum SessionStatus { @@ -56,7 +56,6 @@ export class Session { } } - constructor({ target, metadata, @@ -64,20 +63,21 @@ export class Session { }: { target: string; relay: string; - metadata: Metadata + metadata: Metadata; }) { this.listeners = {}; this.target = target; this.metadata = metadata; this.relay = relay; - this.connectURI = `nostr://connect?target=${this.target}&metadata=${JSON.stringify(this.metadata)}&relay=${this.relay}`; + this.connectURI = `nostr://connect?target=${ + this.target + }&metadata=${JSON.stringify(this.metadata)}&relay=${this.relay}`; } - on( - type: ConnectMessageType, - cb: (value: any) => any - ): void { - const id = Math.random().toString().slice(2); + on(type: ConnectMessageType, cb: (value: any) => any): void { + const id = Math.random() + .toString() + .slice(2); this.listeners[id] = this.listeners[id] || emptyListeners(); this.listeners[id][type].push(cb); } @@ -96,22 +96,34 @@ export class Session { if (!secretKey) throw new Error('secret key is required'); const pubkey = getPublicKey(secretKey); - - const relay = relayInit(this.relay); await relay.connect(); - relay.on('connect', () => { console.log(`connected to ${relay.url}`) }); - relay.on('error', () => { console.error(`failed to connect to ${relay.url}`) }); + relay.on('connect', () => { + console.log(`connected to ${relay.url}`); + }); + relay.on('error', () => { + console.error(`failed to connect to ${relay.url}`); + }); - let sub = relay.sub([{ - kinds: [4], - "#p": [pubkey], - }]); + let sub = relay.sub([ + { + kinds: [4], + '#p': [pubkey], + }, + ]); sub.on('event', async (event: Event) => { - const plaintext = await nip04.decrypt(secretKey, event.pubkey, event.content); + const plaintext = await nip04.decrypt( + secretKey, + event.pubkey, + event.content + ); const payload = JSON.parse(plaintext); if (!payload) return; - if (!Object.keys(payload).includes('requestID') || !Object.keys(payload).includes('message')) return; + if ( + !Object.keys(payload).includes('requestID') || + !Object.keys(payload).includes('message') + ) + return; const msg = payload.message as ConnectMessage; @@ -143,25 +155,24 @@ export class Session { break; } } - - }) + }); sub.on('eose', () => { - sub.unsub() - }) + sub.unsub(); + }); } emit(type: ConnectMessageType, value?: any): void { - Object.values(this.listeners).forEach((listeners) => { + Object.values(this.listeners).forEach(listeners => { if (listeners[type]) { listeners[type].forEach(cb => cb(value)); } }); } - async pair(remoteSignerPrivateKey: string): Promise { - if (!remoteSignerPrivateKey) throw new Error('Signer private key is required'); + if (!remoteSignerPrivateKey) + throw new Error('Signer private key is required'); const remoteSignerPubKey = getPublicKey(remoteSignerPrivateKey); this.remote = remoteSignerPubKey; @@ -169,13 +180,20 @@ export class Session { type: ConnectMessageType.PAIRED, value: { pubkey: this.remote }, }; - const randomID = Math.random().toString().slice(2); - const {event} = await prepareResponse(randomID, this.remote, this.target, message, remoteSignerPrivateKey); + const randomID = Math.random() + .toString() + .slice(2); + const { event } = await prepareResponse( + randomID, + this.remote, + this.target, + message, + remoteSignerPrivateKey + ); const id = await this.sendEvent(event, remoteSignerPrivateKey); console.log('sent pairing response from mobile', id); } - async sendEvent(event: Event, secretKey: string): Promise { const id = getEventHash(event); const sig = signEvent(event, secretKey); @@ -189,7 +207,9 @@ export class Session { const relay = relayInit(this.relay); await relay.connect(); - relay.on('error', () => { throw new Error(`failed to connect to ${relay.url}`) }); + relay.on('error', () => { + throw new Error(`failed to connect to ${relay.url}`); + }); return new Promise((resolve, reject) => { const pub = relay.publish(signedEvent); @@ -201,8 +221,8 @@ export class Session { function emptyListeners(): {} { let data: any = {}; - Object.values(ConnectMessageType).forEach((type) => { + Object.values(ConnectMessageType).forEach(type => { data[type] = []; }); return data; -} \ No newline at end of file +} diff --git a/test/connect.test.ts b/test/connect.test.ts index a22080c..a254f3d 100644 --- a/test/connect.test.ts +++ b/test/connect.test.ts @@ -1,19 +1,23 @@ -import { getPublicKey } from "nostr-tools"; -import { Connect, ConnectMessageType, GetPublicKeyResponse, Session } from "../src/index"; +import { getPublicKey } from 'nostr-tools'; +import { + Connect, + ConnectMessageType, + GetPublicKeyResponse, + Session, +} from '../src/index'; jest.setTimeout(5000); describe('Nostr Connect', () => { it('connect', async () => { let resolvePaired: (arg0: boolean) => void; - let resolveGetPublicKey: (arg0: boolean) => void; + let resolveGetPublicKey: (arg0: boolean) => void; // web app (this is ephemeral and represents the currention session) - const webSK = "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3"; + const webSK = + '5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3'; const webPK = getPublicKey(webSK); console.log('webPk', webPK); - - const sessionWeb = new Session({ target: webPK, relay: 'wss://nostr.vulpem.com', @@ -22,7 +26,7 @@ describe('Nostr Connect', () => { description: 'lorem ipsum dolor sit amet', url: 'https://vulpem.com', icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'], - } + }, }); sessionWeb.on(ConnectMessageType.PAIRED, (msg: any) => { expect(msg).toBeDefined(); @@ -31,8 +35,9 @@ describe('Nostr Connect', () => { await sessionWeb.listen(webSK); // mobile app (this can be a child key) - const sessionMobile = Session.fromConnectURI(sessionWeb.connectURI);// 'nostr://connect?target=...&metadata=...' - const mobileSK = "ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c"; + const sessionMobile = Session.fromConnectURI(sessionWeb.connectURI); // 'nostr://connect?target=...&metadata=...' + const mobileSK = + 'ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c'; const mobilePK = getPublicKey(mobileSK); console.log('mobilePK', mobilePK); await sessionMobile.pair(mobileSK); @@ -43,16 +48,13 @@ describe('Nostr Connect', () => { type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE, value: { pubkey: mobilePK, - } + }, }; const event = await sessionMobile.eventToBeSentToTarget( message, mobileSK ); - await sessionMobile.sendEvent( - event, - mobileSK - ); + await sessionMobile.sendEvent(event, mobileSK); resolveGetPublicKey(true); }); await sessionMobile.listen(mobileSK); @@ -71,13 +73,13 @@ describe('Nostr Connect', () => { return expect( Promise.all([ new Promise(resolve => { - resolvePaired = resolve + resolvePaired = resolve; }), new Promise(resolve => { - resolveGetPublicKey = resolve - }) + resolveGetPublicKey = resolve; + }), ]) - ).resolves.toEqual([true, true]) + ).resolves.toEqual([true, true]); /* expect(handler).toBeCalledTimes(1); diff --git a/test/request.test.ts b/test/request.test.ts index b6d9d7e..0564c1e 100644 --- a/test/request.test.ts +++ b/test/request.test.ts @@ -1,7 +1,6 @@ -import { nip04, Event } from 'nostr-tools'; -import { NostrRPC, NostrRPCServer, prepareEvent, prepareResponse } from '../src/request'; +import { NostrRPC } from '../src/request'; -class Example extends NostrRPCServer { +class Server extends NostrRPC { async ping(): Promise { return 'pong'; } @@ -11,57 +10,28 @@ jest.setTimeout(10000); describe('Nostr RPC', () => { it('starts a server', async () => { - const server = new Example({ - secretKey: "ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c", - }); - const sub = await server.listen(); - sub.on('event', async (event: Event) => { - let plaintext; - let payload; - try { - plaintext = await nip04.decrypt(server.self.secret, event.pubkey, event.content); - payload = JSON.parse(plaintext); - } catch(ignore) { - return; - } - - // ignore all the events that are not NostrRPCRequest events - if (!plaintext) return; - if (!payload) return; - if (!Object.keys(payload).includes('id') || !Object.keys(payload).includes('method') || !Object.keys(payload).includes('params')) return; - - // handle request - const response = { - id: payload.id, - result: "pong", - error: null, - } - const body = prepareResponse(response.id, response.result, response.error); - const responseEvent = await prepareEvent(server.self.secret, event.pubkey, body); - - console.log('response to be sent', responseEvent) - // send response via relay - const pub = server.relay.publish(responseEvent); - pub.on('failed', console.error); - }); - - sub.on('eose', () => { - sub.unsub(); + const server = new Server({ + secretKey: + 'ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c', }); + await server.listen(); const client = new NostrRPC({ - secretKey: "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3", - target: server.self.pubkey, + secretKey: + '5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3', }); console.log(`from: ` + client.self.pubkey, `to: ` + server.self.pubkey); await sleep(2000); - const result = await client.call({ method: 'ping' }); + const result = await client.call({ + target: server.self.pubkey, + request: { method: 'ping' }, + }); console.log(result); - }) -}) + }); +}); function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); -} \ No newline at end of file +} diff --git a/test/server.js b/test/server.js deleted file mode 100644 index c45379f..0000000 --- a/test/server.js +++ /dev/null @@ -1,18 +0,0 @@ -const {NostrRPCServer} = require('../src/request.js'); - -class Example extends NostrRPCServer { - constructor(opts) { - super(opts); - } - async ping() { - return 'pong'; - } -} - -async function main() { - const server = new Example({ - secretKey: "ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c", - }); - await server.listen(); - console.log('Server listening on port 3000'); -} \ No newline at end of file diff --git a/test/setupTests.ts b/test/setupTests.ts index 6a64c96..a278ca7 100644 --- a/test/setupTests.ts +++ b/test/setupTests.ts @@ -1,10 +1,10 @@ //setupTests.tsx -import crypto from "crypto"; -import { TextEncoder, TextDecoder } from "util"; +import crypto from 'crypto'; +import { TextEncoder, TextDecoder } from 'util'; global.TextEncoder = TextEncoder; (global as any).TextDecoder = TextDecoder; Object.defineProperty(global.self, 'crypto', { - value: crypto.webcrypto -}); \ No newline at end of file + value: crypto.webcrypto, +});