add request & test

This commit is contained in:
tiero
2023-01-02 12:49:31 +01:00
parent bc8c7264a2
commit cd20b32f87
18 changed files with 1320 additions and 349 deletions

171
README.md
View File

@@ -1,160 +1,31 @@
# TSDX React User Guide
# Nostr Connect
Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Lets get you oriented with whats here and how to use it.
> This TSDX setup is meant for developing React component libraries (not apps!) that can be published to NPM. If youre looking to build a React-based app, you should use `create-react-app`, `razzle`, `nextjs`, `gatsby`, or `react-static`.
## Nostr Connect
PAIRING
> If youre new to TypeScript and React, checkout [this handy cheatsheet](https://github.com/sw-yx/react-typescript-cheatsheet/)
1. User clicks on "Connect" button on a website or scan it with a QR code
2. It will show an URI to open a "nostr-connect" enabled Wallet
3. In the URI there is a pubkey of the App ie. nc://<pubkey>
4. The Wallet will send a kind 4 encrypted message to ACK the pairing request, along with his public key
## Commands
TSDX scaffolds your new library inside `/src`, and also sets up a [Parcel-based](https://parceljs.org) playground for it inside `/example`.
ENABLE
The recommended workflow is to run TSDX in one terminal:
1. The App will send a kind 4 encrypted message with metadata to the Wallet with a Enable Request
2. The Wallet will show a popup to the user to enable the App to requesta data or remote siging requests
3. The Wallet will send a kind 4 encrypted message to ACK the Enable Request or reject it
4. All others subsequent Enabled Requests will be ACKed automatically
```bash
npm start # or yarn start
```
DELEGATE
This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`.
1. The App will send a kind 4 encrypted message with metadata to the Wallet with a Delegate Request
2. The Wallet will show a popup to the user to delegate the App to sign with a child key
3. The Wallet will send a kind 4 encrypted message to ACK the Delegate Request with the child private key or reject it
4. All others subsequent Delegate Requests will be ACKed automatically
Then run the example inside another:
REMOTE SIGNING
```bash
cd example
npm i # or yarn to install dependencies
npm start # or yarn start
```
The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, we use [Parcel's aliasing](https://parceljs.org/module_resolution.html#aliases).
To do a one-off build, use `npm run build` or `yarn build`.
To run tests, use `npm test` or `yarn test`.
## Configuration
Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly.
### Jest
Jest tests are set up to run with `npm test` or `yarn test`.
### Bundle analysis
Calculates the real cost of your library using [size-limit](https://github.com/ai/size-limit) with `npm run size` and visulize it with `npm run analyze`.
#### Setup Files
This is the folder structure we set up for you:
```txt
/example
index.html
index.tsx # test your component here in a demo app
package.json
tsconfig.json
/src
index.tsx # EDIT THIS
/test
blah.test.tsx # EDIT THIS
.gitignore
package.json
README.md # EDIT THIS
tsconfig.json
```
#### React Testing Library
We do not set up `react-testing-library` for you yet, we welcome contributions and documentation on this.
### Rollup
TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details.
### TypeScript
`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs.
## Continuous Integration
### GitHub Actions
Two actions are added by default:
- `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix
- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit)
## Optimizations
Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations:
```js
// ./types/index.d.ts
declare var __DEV__: boolean;
// inside your code...
if (__DEV__) {
console.log('foo');
}
```
You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions.
## Module Formats
CJS, ESModules, and UMD module formats are supported.
The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found.
## Deploying the Example Playground
The Playground is just a simple [Parcel](https://parceljs.org) app, you can deploy it anywhere you would normally deploy that. Here are some guidelines for **manually** deploying with the Netlify CLI (`npm i -g netlify-cli`):
```bash
cd example # if not already in the example folder
npm run build # builds to dist
netlify deploy # deploy the dist folder
```
Alternatively, if you already have a git repo connected, you can set up continuous deployment with Netlify:
```bash
netlify init
# build command: yarn build && cd example && yarn && yarn build
# directory to deploy: example/dist
# pick yes for netlify.toml
```
## Named Exports
Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library.
## Including Styles
There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like.
For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader.
## Publishing to NPM
We recommend using [np](https://github.com/sindresorhus/np).
## Usage with Lerna
When creating a new package with TSDX within a project set up with Lerna, you might encounter a `Cannot resolve dependency` error when trying to run the `example` project. To fix that you will need to make changes to the `package.json` file _inside the `example` directory_.
The problem is that due to the nature of how dependencies are installed in Lerna projects, the aliases in the example project's `package.json` might not point to the right place, as those dependencies might have been installed in the root of your Lerna project.
Change the `alias` to point to where those packages are actually installed. This depends on the directory structure of your Lerna project, so the actual path might be different from the diff below.
```diff
"alias": {
- "react": "../node_modules/react",
- "react-dom": "../node_modules/react-dom"
+ "react": "../../../node_modules/react",
+ "react-dom": "../../../node_modules/react-dom"
},
```
An alternative to fixing this problem would be to remove aliases altogether and define the dependencies referenced as aliases as dev dependencies instead. [However, that might cause other problems.](https://github.com/palmerhq/tsdx/issues/64)
1. The App will send a kind 4 encrypted message with metadata to the Wallet with a Sign Event Request
2. The Wallet will show a popup to the user to sign the event
3. The Wallet will send a kind 4 encrypted message to ACK the Sign Event Request with the signed event or reject it

97
example/index.old.tsx Normal file
View File

@@ -0,0 +1,97 @@
import 'react-app-polyfill/ie11';
import * as React from 'react';
import { useEffect } from 'react';
import * as ReactDOM from 'react-dom';
import { Session } from '../src/index';
import { generatePrivateKey, getPublicKey, nip04, relayInit } from 'nostr-tools'
const App = () => {
/* useEffect(() => {
(async () => {
})();
}, []); */
const [walletKey, setWalletKey] = React.useState<{ pk: string; sk: string }>();
const [sessionHolder, setSession] = React.useState<Session>();
const [request, setRequest] = React.useState<any>();
const newWallet = () => {
//this is the wallet public key
let sk = generatePrivateKey()
let pk = getPublicKey(sk)
setWalletKey({ pk, sk });
}
const newSession = async () => {
const session = new Session({
name: 'Auth',
description: 'lorem ipsum dolor sit amet',
url: 'https://vulpem.com',
icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'],
});
await session.pair(walletKey?.pk);
setSession(session);
};
const listen = async () => {
if (!sessionHolder) return;
// let's query for an event to this wallet pub key
const relay = relayInit('wss://nostr.vulpem.com');
await relay.connect()
relay.on('connect', () => {
console.log(`wallet: connected to ${relay.url}`)
})
relay.on('error', () => {
console.log(`wallet: failed to connect to ${relay.url}`)
})
let sub = relay.sub([{ kinds: [4] }])
// on the receiver side
sub.on('event', async (event) => {
if (!walletKey) return;
const mention = event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1]
if (mention !== walletKey.pk) return;
const plaintext = await nip04.decrypt(walletKey?.sk, sessionHolder.pubKey, event.content);
console.log('wallet', event.id, event.pubkey, JSON.parse(plaintext));
setRequest(JSON.parse(plaintext));
})
sub.on('eose', () => {
sub.unsub()
})
}
return (
<div>
<h1>💸 Wallet</h1>
{walletKey && <p> 🔎 Wallet Pub: {walletKey?.pk} </p>}
<button onClick={newWallet}>Create Wallet</button>
<button disabled={!sessionHolder} onClick={listen}>Listen</button>
{request && <div>
<h1>📨 Incoming Request</h1>
<img height={80} src={request?.icons[0]} />
<p>
Name <b>{request?.name}</b>
</p>
<p>
Description <b>{request?.description}</b>
</p>
<p>
URL: <b>{request?.url}</b>
</p>
</div>}
<hr />
<h1> App </h1>
<button disabled={!walletKey} onClick={newSession}>New Session</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));

View File

@@ -1,97 +1,56 @@
import 'react-app-polyfill/ie11';
import { getPublicKey } from 'nostr-tools';
import * as React from 'react';
import { useEffect } from 'react';
import * as ReactDOM from 'react-dom';
import { Session } from '../src/index';
import { generatePrivateKey, getPublicKey, nip04, relayInit } from 'nostr-tools'
import { Connect, ConnectMessageType, GetPublicKeyResponse, Session } from '../src';
import { prepareResponse } from '../src/event';
const App = () => {
/* useEffect(() => {
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);
*/
})();
}, []); */
}, []);
const [walletKey, setWalletKey] = React.useState<{ pk: string; sk: string }>();
const [sessionHolder, setSession] = React.useState<Session>();
const [request, setRequest] = React.useState<any>();
const newWallet = () => {
//this is the wallet public key
let sk = generatePrivateKey()
let pk = getPublicKey(sk)
setWalletKey({ pk, sk });
}
const newSession = async () => {
const session = new Session({
name: 'Auth',
description: 'lorem ipsum dolor sit amet',
url: 'https://vulpem.com',
icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'],
});
await session.startPairing(walletKey?.pk);
setSession(session);
};
const listen = async () => {
if (!sessionHolder) return;
// let's query for an event to this wallet pub key
const relay = relayInit('wss://nostr.vulpem.com');
await relay.connect()
relay.on('connect', () => {
console.log(`wallet: connected to ${relay.url}`)
})
relay.on('error', () => {
console.log(`wallet: failed to connect to ${relay.url}`)
})
let sub = relay.sub([{ kinds: [4] }])
// on the receiver side
sub.on('event', async (event) => {
if (!walletKey) return;
const mention = event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1]
if (mention !== walletKey.pk) return;
const plaintext = await nip04.decrypt(walletKey?.sk, sessionHolder.pubKey, event.content);
console.log('wallet', event.id, event.pubkey, JSON.parse(plaintext));
setRequest(JSON.parse(plaintext));
})
sub.on('eose', () => {
sub.unsub()
})
}
return (
<div>
<h1>💸 Wallet</h1>
{walletKey && <p> 🔎 Wallet Pub: {walletKey?.pk} </p>}
<button onClick={newWallet}>Create Wallet</button>
<button disabled={!sessionHolder} onClick={listen}>Listen</button>
{request && <div>
<h1>📨 Incoming Request</h1>
<img height={80} src={request?.icons[0]} />
<p>
Name <b>{request?.name}</b>
</p>
<p>
Description <b>{request?.description}</b>
</p>
<p>
URL: <b>{request?.url}</b>
</p>
</div>}
<hr />
<h1> App </h1>
<button disabled={!walletKey} onClick={newSession}>New Session</button>
<h1>Check you console</h1>
</div>
);
};
)
}
ReactDOM.render(<App />, document.getElementById('root'));

3
jest.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
};

View File

@@ -48,6 +48,7 @@
],
"devDependencies": {
"@size-limit/preset-small-lib": "^8.1.0",
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"husky": "^8.0.2",

148
src/connect.ts Normal file
View File

@@ -0,0 +1,148 @@
import { Event, nip04, relayInit } from "nostr-tools";
import { prepareRequest, prepareResponse } from "./event";
import { Session, SessionStatus } from "./session";
export interface ConnectMessage {
type: ConnectMessageType,
value?: any
requestID?: string,
}
export enum ConnectMessageType {
PAIRED = 'paired',
UNPAIRED = 'unpaired',
GET_PUBLIC_KEY_REQUEST = 'getPublicKeyRequest',
GET_PUBLIC_KEY_RESPONSE = 'getPublicKeyResponse',
}
export interface PairingACK extends ConnectMessage {
type: ConnectMessageType.PAIRED,
value: {
pubkey: string,
}
}
export interface PairingNACK extends ConnectMessage {
type: ConnectMessageType.UNPAIRED
}
export interface GetPublicKeyRequest extends ConnectMessage {
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST
}
export interface GetPublicKeyResponse extends ConnectMessage {
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE,
value: {
pubkey: string,
}
}
export function responseTypeForRequestType(type: ConnectMessageType): ConnectMessageType {
switch (type) {
case ConnectMessageType.GET_PUBLIC_KEY_REQUEST:
return ConnectMessageType.GET_PUBLIC_KEY_RESPONSE;
default:
throw new Error('Invalid request type');
}
}
export class Connect {
session: Session;
private targetPrivateKey: string;
constructor({
session,
targetPrivateKey,
}: {
session: Session;
targetPrivateKey: string;
}) {
this.session = session;
this.targetPrivateKey = targetPrivateKey;
}
async sendMessage(message: ConnectMessage): Promise<ConnectMessage> {
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);
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);
console.log('sent message with nostr id', id);
const relay = relayInit(this.session.relay);
await relay.connect();
return new Promise((resolve, reject) => {
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,
}]);
sub.on('event', async (event: Event) => {
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 (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<string> {
const response: ConnectMessage = await this.sendMessage({
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST
});
if (response.type !== ConnectMessageType.GET_PUBLIC_KEY_RESPONSE) throw new Error('Invalid response type');
return response.value.pubkey;
}
async signEvent(_event: Event): Promise<Event> {
throw new Error('Not implemented');
}
async getRelays(): Promise<{ [url: string]: { read: boolean, write: boolean } }> {
throw new Error('Not implemented');
}
nip04 = {
encrypt: async (_pubkey: string, _plaintext: string): Promise<string> => {
throw new Error('Not implemented');
},
decrypt: async (_pubkey: string, _ciphertext: string): Promise<string> => {
throw new Error('Not implemented');
}
}
async request(_opts: { method: string, params: any }): Promise<any> {
throw new Error('Not implemented');
}
}

43
src/event.ts Normal file
View File

@@ -0,0 +1,43 @@
import { nip04, Event } from "nostr-tools";
import { ConnectMessage } from "./connect";
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 cipherText = await nip04.encrypt(
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,
created_at: now,
pubkey: from,
tags: [['p', to]],
content: cipherText,
};
return { event, requestID };
}

2
src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './session';
export * from './connect';

View File

@@ -1,103 +0,0 @@
import {
generatePrivateKey,
getPublicKey,
validateEvent,
verifySignature,
signEvent,
getEventHash,
Event,
relayInit,
nip04
} from 'nostr-tools'
const KEY_MANAGER = "40852bb98e00d3e242d6a9717ede49574168ecef83841ce88f56699cc83f3085";
export class Session {
request: {
name: string;
description: any;
url: any;
icons: any;
};
pubKey: string;
private secretKey: string;
constructor({
name,
description,
url,
icons,
}: {
name: string,
description?: string
url: string,
icons: string[],
}) {
// Generate an ephemeral identity for this session in the app
let sk = generatePrivateKey()
let pk = getPublicKey(sk)
this.secretKey = sk;
this.pubKey = pk;
this.request = {
name,
description,
url,
icons,
};
console.log(`Session with pubKey: ${this.pubKey}`)
}
async startPairing(walletPubKey: string = KEY_MANAGER) {
const payload = JSON.stringify(this.request);
const cipherText = await nip04.encrypt(this.secretKey, walletPubKey, payload);
let event: Event = {
kind: 4,
created_at: Math.floor(Date.now() / 1000),
pubkey: this.pubKey,
tags: [['p', walletPubKey]],
content: cipherText,
}
await this.sendEvent(event)
}
async sendEvent(event: Event) {
const id = getEventHash(event)
const sig = signEvent(event, this.secretKey)
const signedEvent = { ...event, id, sig };
let ok = validateEvent(signedEvent)
let veryOk = verifySignature(signedEvent)
console.log(ok, veryOk)
if (!ok || !veryOk) {
throw new Error('Event is not valid')
}
const relay = relayInit('wss://nostr.vulpem.com')
await relay.connect();
relay.on('connect', () => {
console.log(`connected to ${relay.url}`)
})
relay.on('error', () => {
throw new Error(`failed to connect to ${relay.url}`);
})
let pub = relay.publish(signedEvent);
console.log(signedEvent);
pub.on('ok', () => {
console.log(`${relay.url} has accepted our event`)
})
pub.on('seen', () => {
console.log(`we saw the event on ${relay.url}`)
})
pub.on('failed', (reason: any) => {
console.error(reason);
console.log(`failed to publish to ${relay.url}: ${reason}`)
})
}
}

314
src/request.js Normal file
View File

@@ -0,0 +1,314 @@
"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;

228
src/request.ts Normal file
View File

@@ -0,0 +1,228 @@
import { relayInit, Relay, getEventHash, signEvent, validateEvent, verifySignature, getPublicKey, nip04, Event, Sub } from "nostr-tools";
export interface NostrRPCRequest {
id: string;
method: string;
params: any[];
}
export interface NostrRPCResponse {
id: string;
result: any;
error: any;
}
export class NostrRPC {
relay: Relay;
self: { pubkey: string, secret: string };
target: string;
constructor(opts: { relay?: string, target: string, secretKey: string }) {
this.relay = relayInit(opts.relay || "wss://nostr.vulpem.com");
this.target = opts.target;
this.self = {
pubkey: getPublicKey(opts.secretKey),
secret: opts.secretKey,
};
}
async call({
id = randomID(),
method,
params = [],
} : {
id?: string,
method: string,
params?: any[],
}): Promise<any> {
// connect to relay
await this.relay.connect();
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);
// send request via relay
await new Promise<void>((resolve, reject) => {
const pub = this.relay.publish(event);
pub.on('failed', reject);
pub.on('seen', resolve);
});
console.log(`sent request to nostr id: ${event.id}`, { id, method, params })
return new Promise<void>((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],
}]);
sub.on('event', async (event: Event) => {
let plaintext;
let payload;
try {
plaintext = await nip04.decrypt(this.self.secret, event.pubkey, event.content);
payload = JSON.parse(plaintext);
} 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)
// ignore all the events that are not for this request
if (payload.id !== id) 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);
}
});
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<Sub> {
await this.relay.connect();
await new Promise<void>((resolve, reject) => {
this.relay.on('connect', resolve);
this.relay.on('error', reject);
});
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);
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 = await this.handleRequest(payload);
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)
// send response via relay
const pub = this.relay.publish(responseEvent);
pub.on('failed', console.error);
});
sub.on('eose', () => {
sub.unsub();
});
return sub;
}
async handleRequest(request: NostrRPCRequest): Promise<NostrRPCResponse> {
const { id, method, params } = request;
let result = null;
let error = null;
try {
result = await this[method](...params);
} catch(e: any) {
if (e instanceof Error) {
error = e.message;
} else {
error = 'unknown error'
}
}
return {
id,
result,
error,
};
}
}
export function now(): number {
return Math.floor(Date.now() / 1000);
}
export function randomID(): string {
return Math.random().toString().slice(2);
}
export function prepareRequest(id: string, method: string, params: any[]): string {
return JSON.stringify({
id,
method: method,
params: params,
});
}
export function prepareResponse(id: string, result: any, error: any): string {
return JSON.stringify({
id: id,
result: result,
error: error,
});
}
export async function prepareEvent(secretKey: string, pubkey: string, content: string): Promise<Event> {
const cipherText = await nip04.encrypt(
secretKey,
pubkey,
content,
);
const event: Event = {
kind: 4,
created_at: now(),
pubkey: getPublicKey(secretKey),
tags: [['p', pubkey]],
content: cipherText,
}
const id = getEventHash(event);
const sig = signEvent(event, secretKey);
const signedEvent = { ...event, id, sig };
let ok = validateEvent(signedEvent);
let veryOk = verifySignature(signedEvent);
if (!ok || !veryOk) {
throw new Error('Event is not valid');
}
return signedEvent;
}

208
src/session.ts Normal file
View File

@@ -0,0 +1,208 @@
import {
validateEvent,
verifySignature,
signEvent,
getEventHash,
Event,
relayInit,
nip04,
getPublicKey,
} from 'nostr-tools'
import { ConnectMessage, ConnectMessageType, PairingACK } from './connect';
import { prepareResponse } from './event';
export interface Metadata {
name: string,
url: string,
description?: string
icons?: string[],
}
export enum SessionStatus {
PROPOSED = 'PROPOSED',
PAIRED = 'PAIRED',
UNPAIRED = 'UNPAIRED',
}
export class Session {
metadata: Metadata;
relay: string;
target: string;
remote?: string;
connectURI: string;
status: SessionStatus = SessionStatus.PROPOSED;
listeners: Record<string, { [type: string]: Array<(value: any) => any> }>;
static fromConnectURI(uri: string): Session {
const url = new URL(uri);
const target = url.searchParams.get('target');
if (!target) {
throw new Error('Invalid connect URI: missing target');
}
const relay = url.searchParams.get('relay');
if (!relay) {
throw new Error('Invalid connect URI: missing relay');
}
const metadata = url.searchParams.get('metadata');
if (!metadata) {
throw new Error('Invalid connect URI: missing metadata');
}
try {
const md = JSON.parse(metadata);
return new Session({ target: target, metadata: md, relay });
} catch (ignore) {
throw new Error('Invalid connect URI: metadata is not valid JSON');
}
}
constructor({
target,
metadata,
relay,
}: {
target: string;
relay: string;
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}`;
}
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);
}
off(type: ConnectMessageType, cb: (value: any) => any): void {
for (const id in this.listeners) {
const idx = this.listeners[id][type].indexOf(cb);
if (idx > -1) {
this.listeners[id][type].splice(idx, 1);
}
}
}
// this is used to process messages to the given key and emit to listeners
async listen(secretKey: string): Promise<void> {
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}`) });
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 payload = JSON.parse(plaintext);
if (!payload) return;
if (!Object.keys(payload).includes('requestID') || !Object.keys(payload).includes('message')) return;
const msg = payload.message as ConnectMessage;
switch (msg.type) {
case ConnectMessageType.PAIRED: {
if (this.status === SessionStatus.PAIRED) return;
if (!msg.value) return;
if (!Object.keys(msg.value).includes('pubkey')) return;
this.status = SessionStatus.PAIRED;
const remote = msg.value.pubkey;
this.remote = remote;
this.emit(ConnectMessageType.PAIRED, msg);
break;
}
case ConnectMessageType.UNPAIRED: {
if (this.status !== SessionStatus.PAIRED) return;
this.status = SessionStatus.UNPAIRED;
this.emit(ConnectMessageType.UNPAIRED);
break;
}
case ConnectMessageType.GET_PUBLIC_KEY_REQUEST: {
if (this.status !== SessionStatus.PAIRED) return;
this.emit(ConnectMessageType.GET_PUBLIC_KEY_REQUEST, msg);
break;
}
case ConnectMessageType.GET_PUBLIC_KEY_RESPONSE: {
this.emit(ConnectMessageType.GET_PUBLIC_KEY_RESPONSE, msg);
break;
}
}
})
sub.on('eose', () => {
sub.unsub()
})
}
emit(type: ConnectMessageType, value?: any): void {
Object.values(this.listeners).forEach((listeners) => {
if (listeners[type]) {
listeners[type].forEach(cb => cb(value));
}
});
}
async pair(remoteSignerPrivateKey: string): Promise<void> {
if (!remoteSignerPrivateKey) throw new Error('Signer private key is required');
const remoteSignerPubKey = getPublicKey(remoteSignerPrivateKey);
this.remote = remoteSignerPubKey;
const message: PairingACK = {
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 id = await this.sendEvent(event, remoteSignerPrivateKey);
console.log('sent pairing response from mobile', id);
}
async sendEvent(event: Event, secretKey: string): Promise<string> {
const id = getEventHash(event);
const sig = signEvent(event, secretKey);
const signedEvent = { ...event, id, sig };
let ok = validateEvent(signedEvent);
let veryOk = verifySignature(signedEvent);
if (!ok || !veryOk) {
throw new Error('Event is not valid');
}
const relay = relayInit(this.relay);
await relay.connect();
relay.on('error', () => { throw new Error(`failed to connect to ${relay.url}`) });
return new Promise((resolve, reject) => {
const pub = relay.publish(signedEvent);
pub.on('failed', (reason: any) => reject(reason));
pub.on('seen', () => resolve(id));
});
}
}
function emptyListeners(): {} {
let data: any = {};
Object.values(ConnectMessageType).forEach((type) => {
data[type] = [];
});
return data;
}

View File

@@ -1,11 +0,0 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Thing } from '../src';
describe('it', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<Thing />, div);
ReactDOM.unmountComponentAtNode(div);
});
});

116
test/connect.test.ts Normal file
View File

@@ -0,0 +1,116 @@
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;
// web app (this is ephemeral and represents the currention session)
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) => {
expect(msg).toBeDefined();
resolvePaired(true);
});
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);
await sessionMobile.pair(mobileSK);
// we define the behavior of the mobile app for each requests
sessionMobile.on(ConnectMessageType.GET_PUBLIC_KEY_REQUEST, async () => {
const message: GetPublicKeyResponse = {
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE,
value: {
pubkey: mobilePK,
}
};
const event = await sessionMobile.eventToBeSentToTarget(
message,
mobileSK
);
await sessionMobile.sendEvent(
event,
mobileSK
);
resolveGetPublicKey(true);
});
await sessionMobile.listen(mobileSK);
// The WebApp send the request and wait for the response
// The WebApp fetch the public key sending request via session
const connect = new Connect({
session: sessionWeb,
targetPrivateKey: webSK,
});
const response = await connect.sendMessage({
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST,
});
expect(response).toBeDefined();
return expect(
Promise.all([
new Promise(resolve => {
resolvePaired = resolve
}),
new Promise(resolve => {
resolveGetPublicKey = resolve
})
])
).resolves.toEqual([true, true])
/*
expect(handler).toBeCalledTimes(1);
expect(handler).toBeCalledWith({
type: ConnectMessageType.PAIRED,
value: {
pubkey: mobilePK,
}
});
const pubkey = await connect.getPublicKey();
expect(pubkey).toBe(mobilePK);
const signedEvt = await connect.signEvent({});
const relays = await connect.getRelays();
const plainText = "hello 🌍";
const cipherText = await connect.nip04.encrypt(childPK, plainText);
const plainText2 = await connect.nip04.decrypt(childPK, cipherText);
expect(plainText === plainText2).toBeTruthy();
await connect.request({
method: 'signSchnorr',
params: [
'0x000000',
'0x000000'
]
});
sessionWeb.on(ConnectMessageType.UNPAIRED, () => {
console.log('unpaired');
});
*/
});
});

67
test/request.test.ts Normal file
View File

@@ -0,0 +1,67 @@
import { nip04, Event } from 'nostr-tools';
import { NostrRPC, NostrRPCServer, prepareEvent, prepareResponse } from '../src/request';
class Example extends NostrRPCServer {
async ping(): Promise<string> {
return 'pong';
}
}
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 client = new NostrRPC({
secretKey: "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3",
target: server.self.pubkey,
});
console.log(`from: ` + client.self.pubkey, `to: ` + server.self.pubkey);
await sleep(2000);
const result = await client.call({ method: 'ping' });
console.log(result);
})
})
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

18
test/server.js Normal file
View File

@@ -0,0 +1,18 @@
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');
}

10
test/setupTests.ts Normal file
View File

@@ -0,0 +1,10 @@
//setupTests.tsx
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
});

View File

@@ -1424,7 +1424,7 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/node@*":
"@types/node@*", "@types/node@^18.11.18":
version "18.11.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==