Replace nostr-dev-kit -> nostr-tools
The package nostr-dev-kit was using a dependency (tseep) which downstream would flag as not CSP-safe. Additionally nostr-tools footprint is much smaller.
This commit is contained in:
parent
95bbf96065
commit
fcfcd64d14
4 changed files with 140 additions and 940 deletions
782
package-lock.json
generated
782
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -35,7 +35,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@bch-wc2/interfaces": "^0.0.8",
|
||||
"@nostr-dev-kit/ndk": "^2.18.1",
|
||||
"nostr-tools": "^2.23.0",
|
||||
"@bitauth/libauth": "^3.1.0-next.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
export { RelayClient } from "./relay-client.js";
|
||||
export type { RelayClientConfig } from "./relay-client.js";
|
||||
export { SimplePool } from "nostr-tools/pool";
|
||||
export { RelayStatus, initiateRelay } from "./relay-handler.js";
|
||||
export type {
|
||||
RelayUpdatePayload,
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
||||
|
||||
import { throwUnless, unwrap } from "./primitives.js";
|
||||
import NDK, {
|
||||
NDKPrivateKeySigner,
|
||||
NDKUser,
|
||||
NDKEvent,
|
||||
NDKKind,
|
||||
giftWrap,
|
||||
giftUnwrap,
|
||||
NDKSubscription,
|
||||
} from "@nostr-dev-kit/ndk";
|
||||
import {
|
||||
SimplePool,
|
||||
useWebSocketImplementation,
|
||||
type SubCloser,
|
||||
} from "nostr-tools/pool";
|
||||
import { wrapEvent, unwrapEvent } from "nostr-tools/nip59";
|
||||
import type { NostrEvent } from "nostr-tools/core";
|
||||
import WebSocket from "isomorphic-ws";
|
||||
import { binToHex, hash256, secp256k1 } from "@bitauth/libauth";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import {
|
||||
|
|
@ -23,6 +22,11 @@ import { deriveNostrPublicKey } from "./utilnostr.js";
|
|||
import { MessageQueue } from "./message-queue.js";
|
||||
import { debug, error as logError, Scope } from "./log.js";
|
||||
|
||||
useWebSocketImplementation(WebSocket);
|
||||
|
||||
const KIND_GIFT_WRAP = 1059;
|
||||
const KIND_PRIVATE_DIRECT_MESSAGE = 14;
|
||||
|
||||
export interface RelayClientConfig {
|
||||
explicitRelayUrls: string[];
|
||||
signerPrivateKey: Uint8Array;
|
||||
|
|
@ -31,14 +35,16 @@ export interface RelayClientConfig {
|
|||
}
|
||||
|
||||
export class RelayClient extends EventEmitter {
|
||||
private ndk: NDK;
|
||||
private paired: NDKUser;
|
||||
private pool: SimplePool;
|
||||
private sharedPool: boolean;
|
||||
private pairedPubkeyHex: string;
|
||||
private config: RelayClientConfig;
|
||||
private messageSubscription: NDKSubscription | null = null;
|
||||
private subscription: SubCloser | null = null;
|
||||
private myPubkey: Uint8Array;
|
||||
private myPubkeyHex: string;
|
||||
private lastProcessedTimestamp: number = 0;
|
||||
private messageQueue: MessageQueue;
|
||||
private readyTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
private sequence: number = Math.floor(
|
||||
Math.random() * (Number.MAX_SAFE_INTEGER - 500_000),
|
||||
|
|
@ -60,18 +66,14 @@ export class RelayClient extends EventEmitter {
|
|||
}
|
||||
>();
|
||||
|
||||
constructor(config: RelayClientConfig) {
|
||||
constructor(config: RelayClientConfig, pool?: SimplePool) {
|
||||
super();
|
||||
this.config = {
|
||||
logNetworkActivity: true,
|
||||
...config,
|
||||
};
|
||||
this.ndk = new NDK({
|
||||
explicitRelayUrls: this.config.explicitRelayUrls,
|
||||
signer: new NDKPrivateKeySigner(this.config.signerPrivateKey),
|
||||
enableOutboxModel: false,
|
||||
autoConnectUserRelays: false,
|
||||
});
|
||||
this.pool = pool ?? new SimplePool();
|
||||
this.sharedPool = pool !== undefined;
|
||||
|
||||
this.messageQueue = new MessageQueue({
|
||||
logActivity: this.config.logNetworkActivity,
|
||||
|
|
@ -87,9 +89,9 @@ export class RelayClient extends EventEmitter {
|
|||
this.config.pairedPublicKey.length === 33
|
||||
? this.config.pairedPublicKey.slice(1)
|
||||
: this.config.pairedPublicKey;
|
||||
this.paired = new NDKUser({ pubkey: binToHex(pairedNostrPubkey) });
|
||||
this.pairedPubkeyHex = binToHex(pairedNostrPubkey);
|
||||
} else {
|
||||
this.paired = new NDKUser({ pubkey: "" });
|
||||
this.pairedPubkeyHex = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ export class RelayClient extends EventEmitter {
|
|||
pairedPublicKey.length === 33
|
||||
? pairedPublicKey.slice(1)
|
||||
: pairedPublicKey;
|
||||
this.paired = new NDKUser({ pubkey: binToHex(pairedNostrPubkey) });
|
||||
this.pairedPubkeyHex = binToHex(pairedNostrPubkey);
|
||||
this.emit("paired");
|
||||
}
|
||||
|
||||
|
|
@ -128,119 +130,49 @@ export class RelayClient extends EventEmitter {
|
|||
}
|
||||
|
||||
try {
|
||||
await this.ndk.connect();
|
||||
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(Scope.Relay, `NDK connect() resolved`);
|
||||
}
|
||||
|
||||
this.messageSubscription = this.ndk.subscribe(
|
||||
this.subscription = this.pool.subscribeMany(
|
||||
this.config.explicitRelayUrls,
|
||||
{ kinds: [KIND_GIFT_WRAP], "#p": [this.myPubkeyHex] },
|
||||
{
|
||||
kinds: [NDKKind.GiftWrap],
|
||||
"#p": [this.myPubkeyHex],
|
||||
onevent: (event: NostrEvent) => this.handleWrappedEvent(event),
|
||||
oneose: () => {
|
||||
if (this.readyTimeoutId) {
|
||||
clearTimeout(this.readyTimeoutId);
|
||||
this.readyTimeoutId = null;
|
||||
}
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(Scope.Relay, `EOSE received, relay connected`);
|
||||
}
|
||||
this.messageQueue.setReady((msg) => this.publishMessage(msg));
|
||||
this.emit("connection");
|
||||
},
|
||||
onclose: (reasons: string[]) => {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(Scope.Relay, `Subscription closed: ${reasons.join(", ")}`);
|
||||
}
|
||||
this.emit("disconnect", new Error("Subscription closed"));
|
||||
},
|
||||
},
|
||||
{ closeOnEose: false },
|
||||
);
|
||||
|
||||
this.messageSubscription.on("event", async (wrappedEvent: NDKEvent) => {
|
||||
try {
|
||||
const signer = this.ndk.signer;
|
||||
if (!signer) {
|
||||
throw new Error("No signer available");
|
||||
}
|
||||
|
||||
const rumor = await giftUnwrap(wrappedEvent, undefined, signer);
|
||||
|
||||
if (rumor.kind === NDKKind.PrivateDirectMessage) {
|
||||
let payload: ProtocolMessage;
|
||||
try {
|
||||
payload = JSON.parse(rumor.content);
|
||||
} catch (e) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
logError(
|
||||
Scope.Relay,
|
||||
"Failed to parse message content as JSON:",
|
||||
e,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!payload.time ||
|
||||
(this.lastProcessedTimestamp > 0 &&
|
||||
payload.time < this.lastProcessedTimestamp)
|
||||
) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Ignoring already-processed message (time: ${payload.time}, action: ${payload.action}, last processed: ${this.lastProcessedTimestamp})`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// wallet_ready carries the key exchange data (public_key + secret) so it
|
||||
// must bypass the peer filter — the dapp doesn't know the wallet's pubkey yet.
|
||||
const isKeyExchangeMessage =
|
||||
payload.action === RelayMsgAction.WalletReady;
|
||||
|
||||
if (!isKeyExchangeMessage && this.config.pairedPublicKey) {
|
||||
const pairedNostrPubkey =
|
||||
this.config.pairedPublicKey.length === 33
|
||||
? binToHex(this.config.pairedPublicKey.slice(1))
|
||||
: binToHex(this.config.pairedPublicKey);
|
||||
if (rumor.pubkey !== pairedNostrPubkey) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Ignoring '${payload.action}' message from unknown peer: ${rumor.pubkey} (expected: ${pairedNostrPubkey})`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Received message ${payload.action} from relay`,
|
||||
);
|
||||
}
|
||||
this.handleRelayMessage(payload);
|
||||
} else {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Ignoring non-PrivateDirectMessage, kind: ${rumor.kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: if EOSE doesn't arrive within 5 seconds, assume ready
|
||||
this.readyTimeoutId = setTimeout(() => {
|
||||
if (!this.messageQueue.getReady()) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
logError(Scope.Relay, "Error handling incoming message:", error);
|
||||
debug(Scope.Relay, `EOSE timeout, assuming ready`);
|
||||
}
|
||||
this.emitError(error as Error);
|
||||
this.messageQueue.setReady((msg) => this.publishMessage(msg));
|
||||
this.emit("connection");
|
||||
}
|
||||
});
|
||||
|
||||
this.messageSubscription.on("close", () => {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(Scope.Relay, "Subscription closed, connection may be stale");
|
||||
}
|
||||
this.emit("disconnect", new Error("Subscription closed"));
|
||||
});
|
||||
|
||||
this.waitForRelaysReady();
|
||||
this.readyTimeoutId = null;
|
||||
}, 5000);
|
||||
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Subscription created and handler set up, emitting connection event`,
|
||||
`Subscription created, waiting for relay connection`,
|
||||
);
|
||||
}
|
||||
|
||||
this.emit("connection");
|
||||
} catch (error) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
logError(Scope.Relay, `Connection failed:`, error);
|
||||
|
|
@ -253,9 +185,18 @@ export class RelayClient extends EventEmitter {
|
|||
this.lastProcessedTimestamp = Math.floor(Date.now() / 1000);
|
||||
this.messageQueue.setNotReady();
|
||||
|
||||
if (this.messageSubscription) {
|
||||
this.messageSubscription.stop();
|
||||
this.messageSubscription = null;
|
||||
if (this.readyTimeoutId) {
|
||||
clearTimeout(this.readyTimeoutId);
|
||||
this.readyTimeoutId = null;
|
||||
}
|
||||
|
||||
if (this.subscription) {
|
||||
this.subscription.close();
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
if (!this.sharedPool) {
|
||||
this.pool.close(this.config.explicitRelayUrls);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,21 +225,21 @@ export class RelayClient extends EventEmitter {
|
|||
private async publishMessage(message: ProtocolMessage): Promise<void> {
|
||||
this.netlog("send", message.action);
|
||||
|
||||
const signer = this.ndk.signer;
|
||||
if (!signer) {
|
||||
throw new Error("No signer available");
|
||||
}
|
||||
|
||||
const rumor = new NDKEvent(this.ndk);
|
||||
rumor.kind = NDKKind.PrivateDirectMessage;
|
||||
rumor.content = JSON.stringify(message);
|
||||
rumor.created_at = Math.floor(Date.now() / 1000);
|
||||
rumor.tags = [["p", this.paired.pubkey]];
|
||||
|
||||
const wrappedEvent = await giftWrap(rumor, this.paired, signer);
|
||||
const wrapped = wrapEvent(
|
||||
{
|
||||
kind: KIND_PRIVATE_DIRECT_MESSAGE,
|
||||
content: JSON.stringify(message),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [["p", this.pairedPubkeyHex]],
|
||||
},
|
||||
this.config.signerPrivateKey,
|
||||
this.pairedPubkeyHex,
|
||||
);
|
||||
|
||||
try {
|
||||
await wrappedEvent.publish();
|
||||
await Promise.any(
|
||||
this.pool.publish(this.config.explicitRelayUrls, wrapped),
|
||||
);
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(Scope.Relay, `Published message ${message.action} to relay`);
|
||||
}
|
||||
|
|
@ -314,46 +255,78 @@ export class RelayClient extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
private async waitForRelaysReady(): Promise<void> {
|
||||
const maxWaitTime = 5000;
|
||||
const checkInterval = 100;
|
||||
const startTime = Date.now();
|
||||
private handleWrappedEvent(wrappedEvent: NostrEvent): void {
|
||||
try {
|
||||
const rumor = unwrapEvent(wrappedEvent, this.config.signerPrivateKey);
|
||||
|
||||
const checkRelays = async (): Promise<void> => {
|
||||
const pool = (this.ndk as any).pool;
|
||||
if (pool) {
|
||||
const relays = pool.relays || [];
|
||||
// NDKRelayStatus: DISCONNECTED=1, CONNECTED=5, AUTHENTICATED=8
|
||||
const connectedRelays = Array.from(relays.values()).filter(
|
||||
(relay: any) => relay.status >= 5,
|
||||
);
|
||||
if (rumor.kind === KIND_PRIVATE_DIRECT_MESSAGE) {
|
||||
let payload: ProtocolMessage;
|
||||
try {
|
||||
payload = JSON.parse(rumor.content);
|
||||
} catch (e) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
logError(
|
||||
Scope.Relay,
|
||||
"Failed to parse message content as JSON:",
|
||||
e,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectedRelays.length > 0) {
|
||||
if (
|
||||
!payload.time ||
|
||||
(this.lastProcessedTimestamp > 0 &&
|
||||
payload.time < this.lastProcessedTimestamp)
|
||||
) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Relays ready (${connectedRelays.length} connected), processing queued messages`,
|
||||
`Ignoring already-processed message (time: ${payload.time}, action: ${payload.action}, last processed: ${this.lastProcessedTimestamp})`,
|
||||
);
|
||||
}
|
||||
await this.messageQueue.setReady((msg) => this.publishMessage(msg));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Date.now() - startTime < maxWaitTime) {
|
||||
setTimeout(checkRelays, checkInterval);
|
||||
// wallet_ready carries the key exchange data (public_key + secret) so it
|
||||
// must bypass the peer filter — the dapp doesn't know the wallet's pubkey yet.
|
||||
const isKeyExchangeMessage =
|
||||
payload.action === RelayMsgAction.WalletReady;
|
||||
|
||||
if (!isKeyExchangeMessage && this.config.pairedPublicKey) {
|
||||
const pairedNostrPubkey =
|
||||
this.config.pairedPublicKey.length === 33
|
||||
? binToHex(this.config.pairedPublicKey.slice(1))
|
||||
: binToHex(this.config.pairedPublicKey);
|
||||
if (rumor.pubkey !== pairedNostrPubkey) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
`Ignoring '${payload.action}' message from unknown peer: ${rumor.pubkey} (expected: ${pairedNostrPubkey})`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(Scope.Relay, `Received message ${payload.action} from relay`);
|
||||
}
|
||||
this.handleRelayMessage(payload);
|
||||
} else {
|
||||
if (this.config.logNetworkActivity) {
|
||||
debug(
|
||||
Scope.Relay,
|
||||
"Relay ready check timeout, assuming ready and processing queued messages",
|
||||
`Ignoring non-PrivateDirectMessage, kind: ${rumor.kind}`,
|
||||
);
|
||||
}
|
||||
await this.messageQueue.setReady((msg) => this.publishMessage(msg));
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(checkRelays, 200);
|
||||
} catch (error) {
|
||||
if (this.config.logNetworkActivity) {
|
||||
logError(Scope.Relay, "Error handling incoming message:", error);
|
||||
}
|
||||
this.emitError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue