mirror of
https://github.com/nostr-connect/connect.git
synced 2025-12-18 21:44:20 +01:00
cleanup
This commit is contained in:
@@ -12,36 +12,6 @@ const App = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(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);
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
100
src/connect.ts
100
src/connect.ts
@@ -1,11 +1,11 @@
|
|||||||
import { Event, nip04, relayInit } from "nostr-tools";
|
import { Event, nip04, relayInit } from 'nostr-tools';
|
||||||
import { prepareRequest, prepareResponse } from "./event";
|
import { prepareRequest } from './event';
|
||||||
import { Session, SessionStatus } from "./session";
|
import { Session, SessionStatus } from './session';
|
||||||
|
|
||||||
export interface ConnectMessage {
|
export interface ConnectMessage {
|
||||||
type: ConnectMessageType,
|
type: ConnectMessageType;
|
||||||
value?: any
|
value?: any;
|
||||||
requestID?: string,
|
requestID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConnectMessageType {
|
export enum ConnectMessageType {
|
||||||
@@ -16,28 +16,30 @@ export enum ConnectMessageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PairingACK extends ConnectMessage {
|
export interface PairingACK extends ConnectMessage {
|
||||||
type: ConnectMessageType.PAIRED,
|
type: ConnectMessageType.PAIRED;
|
||||||
value: {
|
value: {
|
||||||
pubkey: string,
|
pubkey: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PairingNACK extends ConnectMessage {
|
export interface PairingNACK extends ConnectMessage {
|
||||||
type: ConnectMessageType.UNPAIRED
|
type: ConnectMessageType.UNPAIRED;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetPublicKeyRequest extends ConnectMessage {
|
export interface GetPublicKeyRequest extends ConnectMessage {
|
||||||
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST
|
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetPublicKeyResponse extends ConnectMessage {
|
export interface GetPublicKeyResponse extends ConnectMessage {
|
||||||
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE,
|
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE;
|
||||||
value: {
|
value: {
|
||||||
pubkey: string,
|
pubkey: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function responseTypeForRequestType(type: ConnectMessageType): ConnectMessageType {
|
export function responseTypeForRequestType(
|
||||||
|
type: ConnectMessageType
|
||||||
|
): ConnectMessageType {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ConnectMessageType.GET_PUBLIC_KEY_REQUEST:
|
case ConnectMessageType.GET_PUBLIC_KEY_REQUEST:
|
||||||
return ConnectMessageType.GET_PUBLIC_KEY_RESPONSE;
|
return ConnectMessageType.GET_PUBLIC_KEY_RESPONSE;
|
||||||
@@ -62,14 +64,20 @@ export class Connect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(message: ConnectMessage): Promise<ConnectMessage> {
|
async sendMessage(message: ConnectMessage): Promise<ConnectMessage> {
|
||||||
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.target) throw new Error('Target is required');
|
||||||
if (!this.session.remote) throw new Error('Remote is required');
|
if (!this.session.remote) throw new Error('Remote is required');
|
||||||
|
|
||||||
const { target, remote } = this.session;
|
const { target, remote } = this.session;
|
||||||
|
|
||||||
// send request to remote
|
// 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}`);
|
console.log(`sending message ${message.type} with requestID ${requestID}`);
|
||||||
const id = await this.session.sendEvent(event, this.targetPrivateKey);
|
const id = await this.session.sendEvent(event, this.targetPrivateKey);
|
||||||
if (!id) throw new Error('Failed to send message ' + message.type);
|
if (!id) throw new Error('Failed to send message ' + message.type);
|
||||||
@@ -79,48 +87,55 @@ export class Connect {
|
|||||||
await relay.connect();
|
await relay.connect();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
relay.on('error', () => {
|
relay.on('error', () => {
|
||||||
reject(`failed to connect to ${relay.url}`);
|
reject(`failed to connect to ${relay.url}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// waiting for response from remote
|
// waiting for response from remote
|
||||||
let sub = relay.sub([{
|
let sub = relay.sub([
|
||||||
kinds: [4],
|
{
|
||||||
authors: [remote],
|
kinds: [4],
|
||||||
//since: now,
|
authors: [remote],
|
||||||
"#p": [target],
|
//since: now,
|
||||||
limit: 1,
|
'#p': [target],
|
||||||
}]);
|
limit: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
sub.on('event', async (event: Event) => {
|
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('plaintext', plaintext);
|
||||||
console.log('requestID', requestID);
|
console.log('requestID', requestID);
|
||||||
const payload = JSON.parse(plaintext);
|
const payload = JSON.parse(plaintext);
|
||||||
if (!payload) return;
|
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;
|
if (payload.requestID !== requestID) return;
|
||||||
const msg = payload.message as ConnectMessage;
|
const msg = payload.message as ConnectMessage;
|
||||||
const responseType = responseTypeForRequestType(msg.type);
|
const responseType = responseTypeForRequestType(msg.type);
|
||||||
if (msg.type !== responseType) return;
|
if (msg.type !== responseType) return;
|
||||||
resolve(msg);
|
resolve(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
sub.on('eose', () => {
|
sub.on('eose', () => {
|
||||||
sub.unsub();
|
sub.unsub();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async getPublicKey(): Promise<string> {
|
async getPublicKey(): Promise<string> {
|
||||||
const response: ConnectMessage = await this.sendMessage({
|
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;
|
return response.value.pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +143,9 @@ export class Connect {
|
|||||||
throw new Error('Not implemented');
|
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');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +155,10 @@ export class Connect {
|
|||||||
},
|
},
|
||||||
decrypt: async (_pubkey: string, _ciphertext: string): Promise<string> => {
|
decrypt: async (_pubkey: string, _ciphertext: string): Promise<string> => {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
async request(_opts: { method: string, params: any }): Promise<any> {
|
async request(_opts: { method: string; params: any }): Promise<any> {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
69
src/event.ts
69
src/event.ts
@@ -1,36 +1,23 @@
|
|||||||
import { nip04, Event } from "nostr-tools";
|
import { nip04, Event } from 'nostr-tools';
|
||||||
import { ConnectMessage } from "./connect";
|
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 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(
|
const cipherText = await nip04.encrypt(
|
||||||
fromSecretKey,
|
fromSecretKey,
|
||||||
to,
|
to,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
requestID: requestID,
|
requestID: requestID,
|
||||||
request,
|
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 = {
|
const event: Event = {
|
||||||
kind: 4,
|
kind: 4,
|
||||||
@@ -40,4 +27,30 @@ export async function prepareResponse(requestID: string, from: string, to: strin
|
|||||||
content: cipherText,
|
content: cipherText,
|
||||||
};
|
};
|
||||||
return { event, requestID };
|
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,
|
||||||
|
created_at: now,
|
||||||
|
pubkey: from,
|
||||||
|
tags: [['p', to]],
|
||||||
|
content: cipherText,
|
||||||
|
};
|
||||||
|
return { event, requestID };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './session';
|
export * from './session';
|
||||||
export * from './connect';
|
export * from './connect';
|
||||||
|
|||||||
314
src/request.js
314
src/request.js
@@ -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;
|
|
||||||
205
src/request.ts
205
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 {
|
export interface NostrRPCRequest {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,37 +22,39 @@ export interface NostrRPCResponse {
|
|||||||
error: any;
|
error: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class NostrRPC {
|
export class NostrRPC {
|
||||||
relay: Relay;
|
relay: Relay;
|
||||||
self: { pubkey: string, secret: string };
|
self: { pubkey: string; secret: string };
|
||||||
target: string;
|
// this is for implementing the response handlers for each method
|
||||||
|
[key: string]: any;
|
||||||
|
|
||||||
constructor(opts: { relay?: string, target: string, secretKey: string }) {
|
constructor(opts: { relay?: string; secretKey: string }) {
|
||||||
this.relay = relayInit(opts.relay || "wss://nostr.vulpem.com");
|
this.relay = relayInit(opts.relay || 'wss://nostr.vulpem.com');
|
||||||
this.target = opts.target;
|
|
||||||
this.self = {
|
this.self = {
|
||||||
pubkey: getPublicKey(opts.secretKey),
|
pubkey: getPublicKey(opts.secretKey),
|
||||||
secret: opts.secretKey,
|
secret: opts.secretKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async call({
|
async call({
|
||||||
id = randomID(),
|
target,
|
||||||
method,
|
request: { id = randomID(), method, params = [] },
|
||||||
params = [],
|
}: {
|
||||||
} : {
|
target: string;
|
||||||
id?: string,
|
request: {
|
||||||
method: string,
|
id?: string;
|
||||||
params?: any[],
|
method: string;
|
||||||
|
params?: any[];
|
||||||
|
};
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
|
|
||||||
// connect to relay
|
// connect to relay
|
||||||
await this.relay.connect();
|
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
|
// prepare request to be sent
|
||||||
const body = prepareRequest(id, method, params);
|
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
|
// send request via relay
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -51,35 +63,38 @@ export class NostrRPC {
|
|||||||
pub.on('seen', resolve);
|
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<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
|
||||||
// waiting for response from remote
|
// waiting for response from remote
|
||||||
// TODO: reject after a timeout
|
// TODO: reject after a timeout
|
||||||
let sub = this.relay.sub([{
|
let sub = this.relay.sub([
|
||||||
kinds: [4],
|
{
|
||||||
authors: [this.target],
|
kinds: [4],
|
||||||
"#p": [this.self.pubkey],
|
authors: [this.target],
|
||||||
}]);
|
'#p': [this.self.pubkey],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
sub.on('event', async (event: Event) => {
|
sub.on('event', async (event: Event) => {
|
||||||
let plaintext;
|
|
||||||
let payload;
|
let payload;
|
||||||
try {
|
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);
|
payload = JSON.parse(plaintext);
|
||||||
} catch(ignore) {
|
} catch (ignore) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore all the events that are not NostrRPCResponse events
|
// ignore all the events that are not NostrRPCResponse events
|
||||||
if (!plaintext) return;
|
if (!isValidResponse(payload)) 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);
|
||||||
|
|
||||||
console.log(`received response from nostr id: ${event.id}`, payload)
|
|
||||||
|
|
||||||
// ignore all the events that are not for this request
|
// ignore all the events that are not for this request
|
||||||
if (payload.id !== id) return;
|
if (payload.id !== id) return;
|
||||||
|
|
||||||
@@ -93,26 +108,13 @@ export class NostrRPC {
|
|||||||
resolve(payload.result);
|
resolve(payload.result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sub.on('eose', () => {
|
sub.on('eose', () => {
|
||||||
sub.unsub();
|
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<Sub> {
|
async listen(): Promise<Sub> {
|
||||||
await this.relay.connect();
|
await this.relay.connect();
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -120,33 +122,45 @@ export class NostrRPCServer {
|
|||||||
this.relay.on('error', reject);
|
this.relay.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
let sub = this.relay.sub([{
|
let sub = this.relay.sub([
|
||||||
kinds: [4],
|
{
|
||||||
"#p": [this.self.pubkey],
|
kinds: [4],
|
||||||
since: now(),
|
'#p': [this.self.pubkey],
|
||||||
}]);
|
since: now(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
sub.on('event', async (event: Event) => {
|
sub.on('event', async (event: Event) => {
|
||||||
let plaintext;
|
|
||||||
let payload;
|
let payload;
|
||||||
try {
|
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);
|
payload = JSON.parse(plaintext);
|
||||||
} catch(ignore) {
|
} catch (ignore) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore all the events that are not NostrRPCRequest events
|
// ignore all the events that are not NostrRPCRequest events
|
||||||
if (!plaintext) return;
|
if (!isValidRequest(payload)) return;
|
||||||
if (!payload) return;
|
|
||||||
if (!Object.keys(payload).includes('id') || !Object.keys(payload).includes('method') || !Object.keys(payload).includes('params')) return;
|
|
||||||
|
|
||||||
// handle request
|
// handle request
|
||||||
const response = await this.handleRequest(payload);
|
const response = await this.handleRequest(payload);
|
||||||
const body = prepareResponse(response.id, response.result, response.error);
|
const body = prepareResponse(
|
||||||
const responseEvent = await prepareEvent(this.self.secret, event.pubkey, body);
|
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
|
// send response via relay
|
||||||
const pub = this.relay.publish(responseEvent);
|
const pub = this.relay.publish(responseEvent);
|
||||||
pub.on('failed', console.error);
|
pub.on('failed', console.error);
|
||||||
@@ -158,17 +172,20 @@ export class NostrRPCServer {
|
|||||||
|
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
async handleRequest(request: NostrRPCRequest): Promise<NostrRPCResponse> {
|
|
||||||
|
private async handleRequest(
|
||||||
|
request: NostrRPCRequest
|
||||||
|
): Promise<NostrRPCResponse> {
|
||||||
const { id, method, params } = request;
|
const { id, method, params } = request;
|
||||||
let result = null;
|
let result = null;
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
result = await this[method](...params);
|
result = await this[method](...params);
|
||||||
} catch(e: any) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
} else {
|
} else {
|
||||||
error = 'unknown error'
|
error = 'unknown error';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -183,9 +200,15 @@ export function now(): number {
|
|||||||
return Math.floor(Date.now() / 1000);
|
return Math.floor(Date.now() / 1000);
|
||||||
}
|
}
|
||||||
export function randomID(): string {
|
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({
|
return JSON.stringify({
|
||||||
id,
|
id,
|
||||||
method: method,
|
method: method,
|
||||||
@@ -199,12 +222,12 @@ export function prepareResponse(id: string, result: any, error: any): string {
|
|||||||
error: error,
|
error: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export async function prepareEvent(secretKey: string, pubkey: string, content: string): Promise<Event> {
|
export async function prepareEvent(
|
||||||
const cipherText = await nip04.encrypt(
|
secretKey: string,
|
||||||
secretKey,
|
pubkey: string,
|
||||||
pubkey,
|
content: string
|
||||||
content,
|
): Promise<Event> {
|
||||||
);
|
const cipherText = await nip04.encrypt(secretKey, pubkey, content);
|
||||||
|
|
||||||
const event: Event = {
|
const event: Event = {
|
||||||
kind: 4,
|
kind: 4,
|
||||||
@@ -212,7 +235,7 @@ export async function prepareEvent(secretKey: string, pubkey: string, content: s
|
|||||||
pubkey: getPublicKey(secretKey),
|
pubkey: getPublicKey(secretKey),
|
||||||
tags: [['p', pubkey]],
|
tags: [['p', pubkey]],
|
||||||
content: cipherText,
|
content: cipherText,
|
||||||
}
|
};
|
||||||
|
|
||||||
const id = getEventHash(event);
|
const id = getEventHash(event);
|
||||||
const sig = signEvent(event, secretKey);
|
const sig = signEvent(event, secretKey);
|
||||||
@@ -226,3 +249,31 @@ export async function prepareEvent(secretKey: string, pubkey: string, content: s
|
|||||||
|
|
||||||
return signedEvent;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import {
|
|||||||
relayInit,
|
relayInit,
|
||||||
nip04,
|
nip04,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
} from 'nostr-tools'
|
} from 'nostr-tools';
|
||||||
import { ConnectMessage, ConnectMessageType, PairingACK } from './connect';
|
import { ConnectMessage, ConnectMessageType, PairingACK } from './connect';
|
||||||
import { prepareResponse } from './event';
|
import { prepareResponse } from './event';
|
||||||
|
|
||||||
export interface Metadata {
|
export interface Metadata {
|
||||||
name: string,
|
name: string;
|
||||||
url: string,
|
url: string;
|
||||||
description?: string
|
description?: string;
|
||||||
icons?: string[],
|
icons?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SessionStatus {
|
export enum SessionStatus {
|
||||||
@@ -56,7 +56,6 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
target,
|
target,
|
||||||
metadata,
|
metadata,
|
||||||
@@ -64,20 +63,21 @@ export class Session {
|
|||||||
}: {
|
}: {
|
||||||
target: string;
|
target: string;
|
||||||
relay: string;
|
relay: string;
|
||||||
metadata: Metadata
|
metadata: Metadata;
|
||||||
}) {
|
}) {
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.metadata = metadata;
|
this.metadata = metadata;
|
||||||
this.relay = relay;
|
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(
|
on(type: ConnectMessageType, cb: (value: any) => any): void {
|
||||||
type: ConnectMessageType,
|
const id = Math.random()
|
||||||
cb: (value: any) => any
|
.toString()
|
||||||
): void {
|
.slice(2);
|
||||||
const id = Math.random().toString().slice(2);
|
|
||||||
this.listeners[id] = this.listeners[id] || emptyListeners();
|
this.listeners[id] = this.listeners[id] || emptyListeners();
|
||||||
this.listeners[id][type].push(cb);
|
this.listeners[id][type].push(cb);
|
||||||
}
|
}
|
||||||
@@ -96,22 +96,34 @@ export class Session {
|
|||||||
if (!secretKey) throw new Error('secret key is required');
|
if (!secretKey) throw new Error('secret key is required');
|
||||||
const pubkey = getPublicKey(secretKey);
|
const pubkey = getPublicKey(secretKey);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const relay = relayInit(this.relay);
|
const relay = relayInit(this.relay);
|
||||||
await relay.connect();
|
await relay.connect();
|
||||||
relay.on('connect', () => { console.log(`connected to ${relay.url}`) });
|
relay.on('connect', () => {
|
||||||
relay.on('error', () => { console.error(`failed to connect to ${relay.url}`) });
|
console.log(`connected to ${relay.url}`);
|
||||||
|
});
|
||||||
|
relay.on('error', () => {
|
||||||
|
console.error(`failed to connect to ${relay.url}`);
|
||||||
|
});
|
||||||
|
|
||||||
let sub = relay.sub([{
|
let sub = relay.sub([
|
||||||
kinds: [4],
|
{
|
||||||
"#p": [pubkey],
|
kinds: [4],
|
||||||
}]);
|
'#p': [pubkey],
|
||||||
|
},
|
||||||
|
]);
|
||||||
sub.on('event', async (event: Event) => {
|
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);
|
const payload = JSON.parse(plaintext);
|
||||||
if (!payload) return;
|
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;
|
const msg = payload.message as ConnectMessage;
|
||||||
|
|
||||||
@@ -143,25 +155,24 @@ export class Session {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
})
|
|
||||||
|
|
||||||
sub.on('eose', () => {
|
sub.on('eose', () => {
|
||||||
sub.unsub()
|
sub.unsub();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(type: ConnectMessageType, value?: any): void {
|
emit(type: ConnectMessageType, value?: any): void {
|
||||||
Object.values(this.listeners).forEach((listeners) => {
|
Object.values(this.listeners).forEach(listeners => {
|
||||||
if (listeners[type]) {
|
if (listeners[type]) {
|
||||||
listeners[type].forEach(cb => cb(value));
|
listeners[type].forEach(cb => cb(value));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async pair(remoteSignerPrivateKey: string): Promise<void> {
|
async pair(remoteSignerPrivateKey: string): Promise<void> {
|
||||||
if (!remoteSignerPrivateKey) throw new Error('Signer private key is required');
|
if (!remoteSignerPrivateKey)
|
||||||
|
throw new Error('Signer private key is required');
|
||||||
const remoteSignerPubKey = getPublicKey(remoteSignerPrivateKey);
|
const remoteSignerPubKey = getPublicKey(remoteSignerPrivateKey);
|
||||||
this.remote = remoteSignerPubKey;
|
this.remote = remoteSignerPubKey;
|
||||||
|
|
||||||
@@ -169,13 +180,20 @@ export class Session {
|
|||||||
type: ConnectMessageType.PAIRED,
|
type: ConnectMessageType.PAIRED,
|
||||||
value: { pubkey: this.remote },
|
value: { pubkey: this.remote },
|
||||||
};
|
};
|
||||||
const randomID = Math.random().toString().slice(2);
|
const randomID = Math.random()
|
||||||
const {event} = await prepareResponse(randomID, this.remote, this.target, message, remoteSignerPrivateKey);
|
.toString()
|
||||||
|
.slice(2);
|
||||||
|
const { event } = await prepareResponse(
|
||||||
|
randomID,
|
||||||
|
this.remote,
|
||||||
|
this.target,
|
||||||
|
message,
|
||||||
|
remoteSignerPrivateKey
|
||||||
|
);
|
||||||
const id = await this.sendEvent(event, remoteSignerPrivateKey);
|
const id = await this.sendEvent(event, remoteSignerPrivateKey);
|
||||||
console.log('sent pairing response from mobile', id);
|
console.log('sent pairing response from mobile', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async sendEvent(event: Event, secretKey: string): Promise<string> {
|
async sendEvent(event: Event, secretKey: string): Promise<string> {
|
||||||
const id = getEventHash(event);
|
const id = getEventHash(event);
|
||||||
const sig = signEvent(event, secretKey);
|
const sig = signEvent(event, secretKey);
|
||||||
@@ -189,7 +207,9 @@ export class Session {
|
|||||||
|
|
||||||
const relay = relayInit(this.relay);
|
const relay = relayInit(this.relay);
|
||||||
await relay.connect();
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const pub = relay.publish(signedEvent);
|
const pub = relay.publish(signedEvent);
|
||||||
@@ -201,8 +221,8 @@ export class Session {
|
|||||||
|
|
||||||
function emptyListeners(): {} {
|
function emptyListeners(): {} {
|
||||||
let data: any = {};
|
let data: any = {};
|
||||||
Object.values(ConnectMessageType).forEach((type) => {
|
Object.values(ConnectMessageType).forEach(type => {
|
||||||
data[type] = [];
|
data[type] = [];
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { getPublicKey } from "nostr-tools";
|
import { getPublicKey } from 'nostr-tools';
|
||||||
import { Connect, ConnectMessageType, GetPublicKeyResponse, Session } from "../src/index";
|
import {
|
||||||
|
Connect,
|
||||||
|
ConnectMessageType,
|
||||||
|
GetPublicKeyResponse,
|
||||||
|
Session,
|
||||||
|
} from '../src/index';
|
||||||
|
|
||||||
jest.setTimeout(5000);
|
jest.setTimeout(5000);
|
||||||
|
|
||||||
describe('Nostr Connect', () => {
|
describe('Nostr Connect', () => {
|
||||||
it('connect', async () => {
|
it('connect', async () => {
|
||||||
let resolvePaired: (arg0: boolean) => void;
|
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)
|
// web app (this is ephemeral and represents the currention session)
|
||||||
const webSK = "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3";
|
const webSK =
|
||||||
|
'5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3';
|
||||||
const webPK = getPublicKey(webSK);
|
const webPK = getPublicKey(webSK);
|
||||||
console.log('webPk', webPK);
|
console.log('webPk', webPK);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const sessionWeb = new Session({
|
const sessionWeb = new Session({
|
||||||
target: webPK,
|
target: webPK,
|
||||||
relay: 'wss://nostr.vulpem.com',
|
relay: 'wss://nostr.vulpem.com',
|
||||||
@@ -22,7 +26,7 @@ describe('Nostr Connect', () => {
|
|||||||
description: 'lorem ipsum dolor sit amet',
|
description: 'lorem ipsum dolor sit amet',
|
||||||
url: 'https://vulpem.com',
|
url: 'https://vulpem.com',
|
||||||
icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'],
|
icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
sessionWeb.on(ConnectMessageType.PAIRED, (msg: any) => {
|
sessionWeb.on(ConnectMessageType.PAIRED, (msg: any) => {
|
||||||
expect(msg).toBeDefined();
|
expect(msg).toBeDefined();
|
||||||
@@ -31,8 +35,9 @@ describe('Nostr Connect', () => {
|
|||||||
await sessionWeb.listen(webSK);
|
await sessionWeb.listen(webSK);
|
||||||
|
|
||||||
// mobile app (this can be a child key)
|
// mobile app (this can be a child key)
|
||||||
const sessionMobile = Session.fromConnectURI(sessionWeb.connectURI);// 'nostr://connect?target=...&metadata=...'
|
const sessionMobile = Session.fromConnectURI(sessionWeb.connectURI); // 'nostr://connect?target=...&metadata=...'
|
||||||
const mobileSK = "ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c";
|
const mobileSK =
|
||||||
|
'ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c';
|
||||||
const mobilePK = getPublicKey(mobileSK);
|
const mobilePK = getPublicKey(mobileSK);
|
||||||
console.log('mobilePK', mobilePK);
|
console.log('mobilePK', mobilePK);
|
||||||
await sessionMobile.pair(mobileSK);
|
await sessionMobile.pair(mobileSK);
|
||||||
@@ -43,16 +48,13 @@ describe('Nostr Connect', () => {
|
|||||||
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE,
|
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE,
|
||||||
value: {
|
value: {
|
||||||
pubkey: mobilePK,
|
pubkey: mobilePK,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const event = await sessionMobile.eventToBeSentToTarget(
|
const event = await sessionMobile.eventToBeSentToTarget(
|
||||||
message,
|
message,
|
||||||
mobileSK
|
mobileSK
|
||||||
);
|
);
|
||||||
await sessionMobile.sendEvent(
|
await sessionMobile.sendEvent(event, mobileSK);
|
||||||
event,
|
|
||||||
mobileSK
|
|
||||||
);
|
|
||||||
resolveGetPublicKey(true);
|
resolveGetPublicKey(true);
|
||||||
});
|
});
|
||||||
await sessionMobile.listen(mobileSK);
|
await sessionMobile.listen(mobileSK);
|
||||||
@@ -71,13 +73,13 @@ describe('Nostr Connect', () => {
|
|||||||
return expect(
|
return expect(
|
||||||
Promise.all([
|
Promise.all([
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
resolvePaired = resolve
|
resolvePaired = resolve;
|
||||||
}),
|
}),
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
resolveGetPublicKey = resolve
|
resolveGetPublicKey = resolve;
|
||||||
})
|
}),
|
||||||
])
|
])
|
||||||
).resolves.toEqual([true, true])
|
).resolves.toEqual([true, true]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
expect(handler).toBeCalledTimes(1);
|
expect(handler).toBeCalledTimes(1);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { nip04, Event } from 'nostr-tools';
|
import { NostrRPC } from '../src/request';
|
||||||
import { NostrRPC, NostrRPCServer, prepareEvent, prepareResponse } from '../src/request';
|
|
||||||
|
|
||||||
class Example extends NostrRPCServer {
|
class Server extends NostrRPC {
|
||||||
async ping(): Promise<string> {
|
async ping(): Promise<string> {
|
||||||
return 'pong';
|
return 'pong';
|
||||||
}
|
}
|
||||||
@@ -11,57 +10,28 @@ jest.setTimeout(10000);
|
|||||||
|
|
||||||
describe('Nostr RPC', () => {
|
describe('Nostr RPC', () => {
|
||||||
it('starts a server', async () => {
|
it('starts a server', async () => {
|
||||||
const server = new Example({
|
const server = new Server({
|
||||||
secretKey: "ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c",
|
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();
|
|
||||||
});
|
});
|
||||||
|
await server.listen();
|
||||||
|
|
||||||
const client = new NostrRPC({
|
const client = new NostrRPC({
|
||||||
secretKey: "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3",
|
secretKey:
|
||||||
target: server.self.pubkey,
|
'5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3',
|
||||||
});
|
});
|
||||||
console.log(`from: ` + client.self.pubkey, `to: ` + server.self.pubkey);
|
console.log(`from: ` + client.self.pubkey, `to: ` + server.self.pubkey);
|
||||||
|
|
||||||
await sleep(2000);
|
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);
|
console.log(result);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
//setupTests.tsx
|
//setupTests.tsx
|
||||||
import crypto from "crypto";
|
import crypto from 'crypto';
|
||||||
import { TextEncoder, TextDecoder } from "util";
|
import { TextEncoder, TextDecoder } from 'util';
|
||||||
|
|
||||||
global.TextEncoder = TextEncoder;
|
global.TextEncoder = TextEncoder;
|
||||||
(global as any).TextDecoder = TextDecoder;
|
(global as any).TextDecoder = TextDecoder;
|
||||||
|
|
||||||
Object.defineProperty(global.self, 'crypto', {
|
Object.defineProperty(global.self, 'crypto', {
|
||||||
value: crypto.webcrypto
|
value: crypto.webcrypto,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user