diff --git a/core/clients.ts b/core/clients.ts index e911611..e84fe0d 100644 --- a/core/clients.ts +++ b/core/clients.ts @@ -4,45 +4,37 @@ import type { RelayToClientMessage, SubscriptionId, } from "./protocol.ts"; -import { - NostrNode, - NostrNodeBase, - NostrNodeConfig, - NostrNodeModule, -} from "./nodes.ts"; +import { Node, NodeConfig } from "./nodes.ts"; -// ---------------------- -// Interfaces -// ---------------------- - -export type ClientConfig = NostrNodeConfig< - RelayToClientMessage, - ClientEventTypeRecord ->; +export type ClientConfig = NodeConfig; export type ClientOptions = Partial; +export interface ClientEventTypeRecord { + message: ClientToRelayMessage; +} + /** * A class that represents a remote Nostr client. */ -export class Client extends NostrNodeBase< +export class Client extends Node< RelayToClientMessage, ClientEventTypeRecord -> implements NostrNode { - /** - * The WebSocket connection to the client. - */ +> { declare ws: WebSocket; + declare config: ClientConfig; - /** - * Writable interface for the subscriptions. - */ + /** Writable interface for the subscriptions. */ readonly subscriptions: Map< SubscriptionId, WritableStream > = new Map(); - constructor(ws: WebSocket, opts?: ClientOptions) { - super(ws, opts); + constructor(ws: WebSocket, options?: ClientOptions) { + super(ws, options); + this.config = { + ...this.config, + ...options, + }; this.ws.addEventListener("message", (ev: MessageEvent) => { const message = JSON.parse(ev.data) as ClientToRelayMessage; // TODO: Validate the message. @@ -50,23 +42,3 @@ export class Client extends NostrNodeBase< }); } } - -// ------------------------------ -// Events -// ------------------------------ - -export interface ClientEventTypeRecord { - "message": ClientToRelayMessage; -} - -export type ClientEventType = keyof ClientEventTypeRecord; - -// ------------------------------ -// Modules -// ------------------------------ - -export type ClientModule = NostrNodeModule< - RelayToClientMessage, - ClientEventTypeRecord, - Client ->; diff --git a/core/deno.json b/core/deno.json index 8beff79..ffe0eb2 100644 --- a/core/deno.json +++ b/core/deno.json @@ -6,5 +6,11 @@ "./clients": "./clients.ts", "./protocol": "./protocol.ts", "./relays": "./relays.ts" + }, + "imports": { + "@lophus/lib": "jsr:@lophus/lib@0.0.14" + }, + "publish": { + "exclude": ["*_test.ts"] } } diff --git a/core/mod.ts b/core/mod.ts index 584b190..c73f17f 100644 --- a/core/mod.ts +++ b/core/mod.ts @@ -1,3 +1,3 @@ +export * from "./clients.ts"; export * from "./protocol.ts"; export * from "./relays.ts"; -export * from "./clients.ts"; diff --git a/core/nodes.ts b/core/nodes.ts index 48f4655..c01b2f6 100644 --- a/core/nodes.ts +++ b/core/nodes.ts @@ -1,95 +1,35 @@ -import type { NostrMessage } from "./protocol.ts"; -import { WebSocketLike } from "./websockets.ts"; - -export interface NostrNodeConfig< - W extends NostrMessage = NostrMessage, - R extends EventTypeRecord = EventTypeRecord, -> { - modules: NostrNodeModule[]; +import type { WebSocketLike } from "@lophus/lib/websockets"; +import type { InterNodeMessage } from "./protocol.ts"; + +export interface NodeConfig { nbuffer: number; } -export type NostrNodeOptions< - W extends NostrMessage = NostrMessage, - R extends EventTypeRecord = EventTypeRecord, -> = Partial>; - -/** - * Common interface for relays and clients, which extends `EventTarget`. - */ -export interface NostrNode< - W extends NostrMessage = NostrMessage, - R extends EventTypeRecord = EventTypeRecord, -> { - readonly config: Readonly>; - readonly ws: WebSocketLike; - readonly writable: WritableStream; - - status: WebSocketLike["readyState"]; - send(msg: W): void | Promise; - close(): Promise; - - install(mod: NostrNodeModule): void; - - addEventListener>( - type: T, - listener: - | NostrNodeEventListenerOrEventListenerObject - | null, - options?: AddEventListenerOptions, - ): void; - - removeEventListener>( - type: T, - listener: - | NostrNodeEventListenerOrEventListenerObject - | null, - options?: boolean | EventListenerOptions, - ): void; - - dispatchEvent>(event: NostrNodeEvent): boolean; - - /** - * A convenience method to dispatch a `NostrNodeEvent` with the given `type` - * and `data`. - */ - dispatch>(type: T, data: R[T]): void; - - /** - * A convenience method to add an event listener for the given `type` that - * calls the given `listener` when the event is dispatched. - */ - on>( - type: T, - // deno-lint-ignore no-explicit-any - listener: (data: R[T]) => any, - ): void; -} +export type NodeOptions = Partial; /** * Common base class for relays and clients. */ -export class NostrNodeBase< - W extends NostrMessage = NostrMessage, - R extends EventTypeRecord = EventTypeRecord, -> extends EventTarget implements NostrNode { - readonly writable: WritableStream; - readonly config: Readonly>; +export class Node< + M extends InterNodeMessage = InterNodeMessage, + R = AnyEventTypeRecord, +> extends EventTarget { + readonly writable: WritableStream; + readonly config: Readonly; constructor( readonly ws: WebSocketLike, - opts: NostrNodeOptions = {}, + options: NodeOptions = {}, ) { super(); this.writable = new WritableStream({ write: (msg) => this.ws.send(JSON.stringify(msg)), close: () => this.ws.close(), }); - this.config = { modules: [], nbuffer: 10, ...opts }; - this.config.modules.forEach((m) => this.install(m)); + this.config = { nbuffer: 10, ...options }; } - send(msg: W): void | Promise { + send(msg: M): void | Promise { return this.ws.send(JSON.stringify(msg)); } @@ -107,19 +47,31 @@ export class NostrNodeBase< } } - install(mod: NostrNodeModule): void { - mod.install(this); - } + declare addEventListener: >( + type: T, + listener: + | NodeEventListenerOrEventListenerObject + | null, + options?: AddEventListenerOptions, + ) => void; + + declare removeEventListener: >( + type: T, + listener: + | NodeEventListenerOrEventListenerObject + | null, + options?: boolean | EventListenerOptions, + ) => void; - declare addEventListener: NostrNode["addEventListener"]; - declare removeEventListener: NostrNode["removeEventListener"]; - declare dispatchEvent: NostrNode["dispatchEvent"]; + declare dispatchEvent: >( + event: NodeEvent, + ) => boolean; dispatch>( type: T, data: R[T], ) { - this.dispatchEvent(new NostrNodeEvent(type, data)); + this.dispatchEvent(new NodeEvent(type, data)); } on>( @@ -131,31 +83,18 @@ export class NostrNodeBase< } } -// ------------------------------ -// Extensions -// ------------------------------ - -export interface NostrNodeModule< - W extends NostrMessage = NostrMessage, - R extends EventTypeRecord = EventTypeRecord, - N extends NostrNode = NostrNode, -> { - // deno-lint-ignore no-explicit-any - install(node: N): any; -} - // ------------------------------ // Events // ------------------------------ -// deno-lint-ignore no-empty-interface -export interface EventTypeRecord {} +// deno-lint-ignore no-explicit-any +type AnyEventTypeRecord = any; -type EventType = keyof R & string; +type EventType = keyof R & string; -export class NostrNodeEvent< - R extends EventTypeRecord, - T extends EventType, +export class NodeEvent< + R = AnyEventTypeRecord, + T extends EventType = EventType, > extends MessageEvent { declare type: T; constructor(type: T, data: R[T]) { @@ -163,24 +102,26 @@ export class NostrNodeEvent< } } -type NostrNodeEventListenerOrEventListenerObject< - W extends NostrMessage, - R extends EventTypeRecord, - T extends EventType, -> = NostrNodeEventListener | NostrNodeEventListenerObject; - -type NostrNodeEventListener< - W extends NostrMessage, - R extends EventTypeRecord, - T extends EventType, +type NodeEventListenerOrEventListenerObject< + M extends InterNodeMessage, + R = AnyEventTypeRecord, + T extends EventType = EventType, +> = + | NodeEventListener + | NodeEventListenerObject; + +type NodeEventListener< + W extends InterNodeMessage, + R = AnyEventTypeRecord, + T extends EventType = EventType, > // deno-lint-ignore no-explicit-any - = (this: NostrNode, ev: MessageEvent) => any; + = (this: Node, ev: MessageEvent) => any; -type NostrNodeEventListenerObject< - W extends NostrMessage, - R extends EventTypeRecord, - T extends EventType, +type NodeEventListenerObject< + W extends InterNodeMessage, + R = AnyEventTypeRecord, + T extends EventType = EventType, > = { // deno-lint-ignore no-explicit-any - handleEvent(this: NostrNode, ev: MessageEvent): any; + handleEvent(this: Node, ev: MessageEvent): any; }; diff --git a/core/nodes_test.ts b/core/nodes_test.ts index 57b4b4e..f1997a4 100644 --- a/core/nodes_test.ts +++ b/core/nodes_test.ts @@ -1,33 +1,27 @@ import { assertEquals } from "@std/assert"; -import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; +import { describe, it } from "@std/testing/bdd"; import { MockWebSocket } from "@lophus/lib/testing"; -import { NostrNode, NostrNodeBase } from "./nodes.ts"; +import { Node } from "./nodes.ts"; -describe("NostrNodeBase", () => { - let node: NostrNode; - let writer: WritableStreamDefaultWriter; +describe("NostrNode", () => { + describe("writable", () => { + const node = new Node(new MockWebSocket()); + let writer: WritableStreamDefaultWriter; - beforeAll(() => { - node = new NostrNodeBase(new MockWebSocket()); - }); - - afterAll(async () => { - await node.close().catch((err) => { - if (err.message !== "Writable stream is closed or errored.") { - throw err; - } + it("should open the WebSocket connection", async () => { + writer = node.writable.getWriter(); + await writer.write(["NOTICE", "test"]); + writer.releaseLock(); + assertEquals(node.status, WebSocket.OPEN); }); }); - it("should be connected to the WebSocket after a message is sent", async () => { - writer = node.writable.getWriter(); - await writer.write(["NOTICE", "test"]); - writer.releaseLock(); - assertEquals(node.status, WebSocket.OPEN); - }); + describe("close", () => { + const node = new Node(new MockWebSocket()); - it("should close the WebSocket when the node is closed", async () => { - await node.close(); - assertEquals(node.status, WebSocket.CLOSED); + it("should close the WebSocket connection", async () => { + await node.close(); + assertEquals(node.status, WebSocket.CLOSED); + }); }); }); diff --git a/core/protocol.ts b/core/protocol.ts index a3c7131..50ce9e5 100644 --- a/core/protocol.ts +++ b/core/protocol.ts @@ -1,34 +1,15 @@ // deno-lint-ignore-file no-empty-interface /** - * Implementation of unextendable part of NIP-01 (Nostr basic protocol): + * Implementation of the fundamental part of NIP-01 (Nostr basic protocol): * https://github.com/nostr-protocol/nips/blob/master/01.md * - * See also `../../nips/01/protocol.d.ts` for the extendable part. - * + * See also `../../nips/01/protocol.ts` for the remaining "optional" part. * @module */ import type { AlphabetLetter, Brand, Stringified } from "@lophus/lib/types"; -// ---------------------- -// Extendable interfaces -// ---------------------- - -export interface NipRecord {} - -export interface EventKindRecord {} - -export interface TagRecord {} - -export interface ClientToRelayMessageRecord< - K extends EventKind = EventKind, -> {} - -export interface RelayToClientMessageRecord< - K extends EventKind = EventKind, -> {} - // ---------------------- // Events and signatures // ---------------------- @@ -59,10 +40,25 @@ export type EventSerializePrecursor = [ content: NostrEvent["content"], ]; +/** An event that has not been signed. */ +export type UnsignedEvent = Omit< + NostrEvent, + "id" | "pubkey" | "sig" +>; + +/** A precursor to NostrEvent required for users to fill */ +export interface EventInit { + kind: NostrEvent["kind"]; + tags?: NostrEvent["tags"]; + content: EventContent | Stringified>; +} + // ---------------------- // Tags // ---------------------- +export interface TagRecord {} + export type TagType = keyof TagRecord & string; export type TagParam = string | undefined; @@ -93,7 +89,9 @@ export type OptionalTag = EventKindRecord[K] extends export type RelayUrl = `wss://${string}` | `ws://${string}`; export type SubscriptionId = Brand; -export type NostrMessage = ClientToRelayMessage | RelayToClientMessage; +export interface ClientToRelayMessageRecord< + K extends EventKind = EventKind, +> {} export type ClientToRelayMessage< T extends ClientToRelayMessageType = ClientToRelayMessageType, @@ -103,6 +101,10 @@ export type ClientToRelayMessage< }[T]; export type ClientToRelayMessageType = keyof ClientToRelayMessageRecord; +export interface RelayToClientMessageRecord< + K extends EventKind = EventKind, +> {} + export type RelayToClientMessage< T extends RelayToClientMessageType = RelayToClientMessageType, K extends EventKind = EventKind, @@ -140,10 +142,14 @@ export type SubscriptionFilter< limit?: number; }; +export type InterNodeMessage = ClientToRelayMessage | RelayToClientMessage; + // ---------------------- // Events // ---------------------- +export interface EventKindRecord {} + export type EventKind = keyof EventKindRecord & number; export interface EventKindRecordEntry { @@ -161,24 +167,3 @@ export type EventContent = EventKindRecord[K] extends export type ResponsePrefix = EventKindRecord[K] extends { ResponsePrefix: infer P extends string } ? P : DefaultResponsePrefix; - -export type RegularEventKind = Brand; -export type ReplaceableEventKind = Brand; -export type EphemeralEventKind = Brand; -export type ParameterizedReplaceableEventKind = Brand< - EventKind, - "ParameterizedReplaceable" ->; - -// ---------------------- -// NIPs -// ---------------------- - -export type NIP = keyof NipRecord & number; - -export interface NipRecordEntry { - ClientToRelayMessage: ClientToRelayMessageType; - RelayToClientMessage: RelayToClientMessageType; - EventKind: EventKind; - Tag: TagType; -} diff --git a/core/relays.ts b/core/relays.ts index 66a92b9..2556f70 100644 --- a/core/relays.ts +++ b/core/relays.ts @@ -1,4 +1,5 @@ -import type { Stringified } from "@lophus/lib/types"; +import type { PromiseCallbackRecord } from "@lophus/lib/types"; +import { LazyWebSocket } from "@lophus/lib/websockets"; import type { ClientToRelayMessage, EventId, @@ -10,44 +11,81 @@ import type { SubscriptionFilter, SubscriptionId, } from "./protocol.ts"; -import { LazyWebSocket } from "./websockets.ts"; -import { NostrNodeBase, NostrNodeConfig, NostrNodeModule } from "./nodes.ts"; +import { Node, NodeConfig } from "./nodes.ts"; -// ---------------------- +//--------- // Errors -// ---------------------- +//--------- -export class EventRejected extends Error {} export class ConnectionClosed extends Error {} +export class EventRejected extends Error {} +export class SubscriptionClosed extends Error {} -// ---------------------- +//------------- // Interfaces -// ---------------------- +//------------- -export interface RelayConfig - extends NostrNodeConfig { - url: RelayUrl; +export interface RelayConfig extends NodeConfig { name: string; read: boolean; + url: RelayUrl; write: boolean; } export type RelayOptions = Partial; -export interface RelayInit extends RelayOptions { - url: RelayUrl; -} - export interface SubscriptionOptions { id?: string; - realtime?: boolean; nbuffer?: number; + realtime?: boolean; +} + +//------------------------ +// Messages and contexts +//------------------------ + +export interface SubscriptionContext { + id: SubscriptionId; + filters: SubscriptionFilter[]; + realtime: boolean; +} + +export interface PublicationContext extends PromiseCallbackRecord { + event: NostrEvent; +} + +type SubscriptionMessage = { + [T in RelayToClientMessageType]: RelayToClientMessage[1] extends + SubscriptionId ? RelayToClientMessage + : never; +}[RelayToClientMessageType]; + +type PublicationMessage = { + [T in RelayToClientMessageType]: RelayToClientMessage[1] extends EventId + ? RelayToClientMessage + : never; +}[RelayToClientMessageType]; + +//--------- +// Events +//--------- + +export interface RelayEventTypeRecord { + message: RelayToClientMessage; + subscribe: SubscriptionContext & { + controller: ReadableStreamDefaultController; + }; + resubscribe: SubscriptionContext; + unsubscribe: SubscriptionContext & { reason: unknown }; + publish: PublicationContext; + [id: SubscriptionId]: SubscriptionMessage; + [id: EventId]: PublicationMessage; } /** * A class that represents a remote Nostr Relay. */ -export class Relay extends NostrNodeBase< +export class Relay extends Node< ClientToRelayMessage, RelayEventTypeRecord > { @@ -55,57 +93,54 @@ export class Relay extends NostrNodeBase< declare config: RelayConfig; constructor( - init: RelayUrl | RelayInit, + url: RelayUrl, options?: RelayOptions, ) { - const url = typeof init === "string" ? init : init.url; - const config = { nbuffer: 64, modules: [], ...options }; - super(new LazyWebSocket(url), config); + super(new LazyWebSocket(url), options); this.config = { - url, + ...this.config, name: new URL(url).hostname, read: true, + url, write: true, - ...config, + ...options, }; - this.ws.addEventListener( - "message", - (ev: MessageEvent>) => { - const message = JSON.parse(ev.data) as RelayToClientMessage; - // TODO: Validate the message. - return this.dispatch("message", message); - }, - ); + this.ws.addEventListener("message", (ev: MessageEvent) => { + const message = JSON.parse(ev.data) as RelayToClientMessage; + // TODO: Validate the message. + this.dispatch("message", message); + }); } subscribe( filter: SubscriptionFilter | SubscriptionFilter[], options: Partial = {}, ): ReadableStream> { - options.realtime ??= true; - options.nbuffer ??= this.config.nbuffer; const context = { id: (options.id ?? crypto.randomUUID()) as SubscriptionId, filters: [filter].flat(), - options, + realtime: options.realtime ?? true, }; - const resubscribe = () => this.dispatch("resubscribe", { ...context }); - return new ReadableStream>({ - start: (controller) => { - this.addEventListener( - context.id, - () => this.ws.removeEventListener("close", resubscribe), - ); - this.dispatch("subscribe", { ...context, controller }); - }, - pull: () => { - this.ws.addEventListener("close", resubscribe, { once: true }); - this.ws.ready(); + const resubscribe = () => this.dispatch("resubscribe", context); + return new ReadableStream>( + { + start: (controller) => { + this.addEventListener( + context.id, + () => this.ws.removeEventListener("close", resubscribe), + ); + this.dispatch("subscribe", { ...context, controller }); + }, + pull: () => { + this.ws.addEventListener("close", resubscribe, { once: true }); + this.ws.ready(); + }, + cancel: (reason) => { + this.dispatch("unsubscribe", { ...context, reason }); + }, }, - cancel: (reason) => { - this.dispatch("unsubscribe", { ...context, reason }); - }, - }, new CountQueuingStrategy({ highWaterMark: options.nbuffer })); + { highWaterMark: options.nbuffer ?? this.config.nbuffer }, + ); } /** @@ -115,7 +150,7 @@ export class Relay extends NostrNodeBase< * @throws {ConnectionClosed} If the WebSocket connection to the relay is closed */ publish(event: NostrEvent): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.dispatch("publish", { event, resolve, reject }); this.ws.addEventListener( "close", @@ -126,9 +161,9 @@ export class Relay extends NostrNodeBase< } } -// ---------------------- +//------------- // RelayLikes -// ---------------------- +//------------- export interface RelayLike extends Pick { @@ -137,60 +172,3 @@ export interface RelayLike export type RelayLikeConfig = Pick; export type RelayLikeOptions = Partial; - -// ---------------------- -// Events -// ---------------------- - -export interface SubscriptionContext { - id: SubscriptionId; - filters: SubscriptionFilter[]; - options: SubscriptionOptions; -} - -export interface SubscriptionContextWithController extends SubscriptionContext { - controller: ReadableStreamDefaultController; -} - -export interface SubscriptionContextWithReason extends SubscriptionContext { - reason: unknown; -} - -export interface PublicationContext { - event: NostrEvent; - resolve: () => void; - reject: (reason: unknown) => void; -} - -type SubscriptionMessage = { - [T in RelayToClientMessageType]: RelayToClientMessage[1] extends - SubscriptionId ? RelayToClientMessage : never; -}[RelayToClientMessageType]; - -type PublicationMessage = { - [T in RelayToClientMessageType]: RelayToClientMessage[1] extends EventId - ? RelayToClientMessage - : never; -}[RelayToClientMessageType]; - -export interface RelayEventTypeRecord { - message: RelayToClientMessage; - subscribe: SubscriptionContextWithController; - resubscribe: SubscriptionContext; - unsubscribe: SubscriptionContextWithReason; - publish: PublicationContext; - [id: SubscriptionId]: SubscriptionMessage; - [id: EventId]: PublicationMessage; -} - -export type RelayEventType = keyof RelayEventTypeRecord; - -// ---------------------- -// Modules -// ---------------------- - -export type RelayModule = NostrNodeModule< - ClientToRelayMessage, - RelayEventTypeRecord, - Relay ->; diff --git a/core/relays_test.ts b/core/relays_test.ts index c358940..a7d334d 100644 --- a/core/relays_test.ts +++ b/core/relays_test.ts @@ -1,18 +1,12 @@ import { assert, assertEquals, assertObjectMatch } from "@std/assert"; -import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; +import { describe, it } from "@std/testing/bdd"; import { Relay } from "./relays.ts"; -const url = "wss://localhost:8080"; - describe("Relay", () => { - let relay: Relay; + const url = "wss://localhost:8080"; describe("constructed with url only", () => { - beforeAll(() => { - relay = new Relay(url); - }); - - afterAll(() => relay.close()); + const relay = new Relay(url); it("should be constructable", () => { assert(relay instanceof Relay); @@ -39,17 +33,11 @@ describe("Relay", () => { }); describe("constructed with url and options", () => { - beforeAll(() => { - relay = new Relay(url, { - name: "test", - read: false, - write: false, - nbuffer: 20, - }); - }); - - afterAll(() => { - relay.close(); + const relay = new Relay(url, { + name: "test", + read: false, + write: false, + nbuffer: 20, }); it("should be constructable", () => { diff --git a/deno.json b/deno.json index 13e26ca..332f7eb 100644 --- a/deno.json +++ b/deno.json @@ -5,12 +5,22 @@ "imports": { "@noble/curves": "npm:@noble/curves@^1.3.0", "@noble/hashes": "npm:@noble/hashes@^1.3.3", - "@lophus/core/clients": "./core/clients.ts", - "@lophus/core/protocol": "./core/protocol.ts", - "@lophus/core/relays": "./core/relays.ts", "@lophus/lib/types": "./lib/types.ts", "@lophus/lib/testing": "./lib/testing.ts", "@lophus/lib/streams": "./lib/streams.ts", + "@lophus/lib/websockets": "./lib/websockets.ts", + "@lophus/core": "./core/mod.ts", + "@lophus/core/clients": "./core/clients.ts", + "@lophus/core/nodes": "./core/nodes.ts", + "@lophus/core/protocol": "./core/protocol.ts", + "@lophus/core/relays": "./core/relays.ts", + "@lophus/nips": "./nips/protocol.ts", + "@lophus/nips/clients": "./nips/clients.ts", + "@lophus/nips/relays": "./nips/relays.ts", + "@lophus/nips/01": "./nips/01/protocol.ts", + "@lophus/nips/02": "./nips/02/protocol.ts", + "@lophus/nips/07": "./nips/07/protocol.ts", + "@lophus/nips/42": "./nips/42/protocol.ts", "@lophus/std/env": "./std/env.ts", "@lophus/std/events": "./std/events.ts", "@lophus/std/notes": "./std/notes.ts", @@ -18,7 +28,6 @@ "@lophus/std/signs": "./std/signs.ts", "@lophus/std/times": "./std/times.ts", "@std/assert": "jsr:@std/assert@^0.219.1", - "@std/dotenv": "jsr:@std/dotenv@^0.219.1", "@std/streams": "jsr:@std/streams@^0.219.1", "@std/testing": "jsr:@std/testing@^0.219.1" }, @@ -26,7 +35,7 @@ "build": "mkdir -p ./dist && deno run -A ./bin/bundle.ts", "cache": "deno cache ./**/*.ts --lock", "check": "deno check ./**/*.ts", - "lock": "deno task cache --lock-write && git add deno.lock", + "lock": "deno task cache --lock-write", "pre-commit": "deno fmt && deno lint && deno task -q check && deno task -q lock && deno task -q test", "test": "deno test -A --no-check", "update": "deno run --allow-read --allow-env --allow-write --allow-net=registry.npmjs.org,jsr.io --allow-run=deno,git https://deno.land/x/molt@0.17.1/cli.ts deno.json --unstable-lock", diff --git a/deno.lock b/deno.lock index 5abc4f5..fc1b47e 100644 --- a/deno.lock +++ b/deno.lock @@ -4,7 +4,6 @@ "specifiers": { "jsr:@std/assert@^0.219.1": "jsr:@std/assert@0.219.1", "jsr:@std/bytes@^0.219.1": "jsr:@std/bytes@0.219.1", - "jsr:@std/dotenv@^0.219.1": "jsr:@std/dotenv@0.219.1", "jsr:@std/fmt@^0.219.1": "jsr:@std/fmt@0.219.1", "jsr:@std/io@^0.219.1": "jsr:@std/io@0.219.1", "jsr:@std/streams@^0.219.1": "jsr:@std/streams@0.219.1", @@ -22,9 +21,6 @@ "@std/bytes@0.219.1": { "integrity": "693e5f3f7796b33a448fb16c448b75d7d6e62914b55e2f1f715f9a04f77853b4" }, - "@std/dotenv@0.219.1": { - "integrity": "061a8d284c85ac6f47ce6df7b90f950ae3f89de61e94cf4523cc4a1ba5175050" - }, "@std/fmt@0.219.1": { "integrity": "2432152e927df249a207177aa048a6d9465956ea0047653ee6abd4f514db504f" }, @@ -118,15 +114,25 @@ "workspace": { "dependencies": [ "jsr:@std/assert@^0.219.1", - "jsr:@std/dotenv@^0.219.1", "jsr:@std/streams@^0.219.1", "jsr:@std/testing@^0.219.1", "npm:@noble/curves@^1.3.0", "npm:@noble/hashes@^1.3.3" ], "members": { - "@lophus/core": {}, - "@lophus/lib": {} + "@lophus/core": { + "dependencies": [ + "jsr:@lophus/lib@0.0.14" + ] + }, + "@lophus/lib": {}, + "@lophus/std": { + "dependencies": [ + "jsr:@lophus/core@0.0.14", + "jsr:@lophus/lib@0.0.14", + "jsr:@std/streams@^0.219.1" + ] + } } } } diff --git a/lib/deno.json b/lib/deno.json index 5bb8ad2..7fa4468 100644 --- a/lib/deno.json +++ b/lib/deno.json @@ -4,6 +4,10 @@ "exports": { "./streams": "./streams.ts", "./testing": "./testing.ts", - "./types": "./types.ts" + "./types": "./types.ts", + "./websockets": "./websockets.ts" + }, + "publish": { + "exclude": ["*_test.ts"] } } diff --git a/lib/types.ts b/lib/types.ts index 5f6a6fa..66da97f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,19 +1,22 @@ // ---------------------- // Branded types // ---------------------- + export type Brand = T & { __brand: B }; // ---------------------- // Promises // ---------------------- -export type PromiseCallbacks = { + +export interface PromiseCallbackRecord { resolve: (value: T | PromiseLike) => void; reject: (reason?: unknown) => void; -}; +} // ---------------------- // Strings // ---------------------- + export type Url = `https://${string}` | `http://${string}`; export type Stringified = string & { __content: T }; @@ -31,6 +34,7 @@ export type AlphabetLetter = // ---------------------- // Records and maps // ---------------------- + export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; export type Require = Expand< diff --git a/core/websockets.ts b/lib/websockets.ts similarity index 100% rename from core/websockets.ts rename to lib/websockets.ts diff --git a/core/websockets_test.ts b/lib/websockets_test.ts similarity index 100% rename from core/websockets_test.ts rename to lib/websockets_test.ts diff --git a/nips/01/clients.ts b/nips/01/clients.ts index b3b4cf2..a4684a2 100644 --- a/nips/01/clients.ts +++ b/nips/01/clients.ts @@ -1,20 +1,20 @@ -import type { NostrEvent } from "../../core/protocol.ts"; -import { ClientModule } from "../../core/clients.ts"; +import type { NostrEvent } from "@lophus/core/protocol"; +import { Client } from "@lophus/core/clients"; +import { NIPModule } from "../nodes.ts"; interface EventValidationContext { data: NostrEvent; resolve: (value: unknown) => void; - // deno-lint-ignore no-explicit-any - reject: (reason?: any) => void; + reject: (reason?: unknown) => void; } -declare module "../../core/clients.ts" { +declare module "@lophus/core/clients" { interface ClientEventTypeRecord { validate: EventValidationContext; } } -export const install: ClientModule["install"] = (client) => { +const M: NIPModule = (client) => { client.on("message", (message) => { switch (message[0]) { case "EVENT": { @@ -54,4 +54,4 @@ export const install: ClientModule["install"] = (client) => { }); }; -export default { install }; +export default M; diff --git a/nips/01/clients_test.ts b/nips/01/clients_test.ts index 251cc87..f57de7a 100644 --- a/nips/01/clients_test.ts +++ b/nips/01/clients_test.ts @@ -1,16 +1,15 @@ import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; import { assert, assertEquals } from "@std/assert"; -import { MockWebSocket } from "../../lib/testing.ts"; +import { MockWebSocket } from "@lophus/lib/testing"; import { ClientToRelayMessage, NostrEvent, RelayToClientMessage, SubscriptionId, -} from "../../core/protocol.ts"; -import { Client } from "../../core/clients.ts"; -import nip_01 from "../01/clients.ts"; +} from "@lophus/core/protocol"; +import { Client } from "../clients.ts"; -describe("NIP-01/Client", () => { +describe("Client (NIP-01)", () => { let ws: MockWebSocket; let client: Client; let subid: SubscriptionId; @@ -18,7 +17,7 @@ describe("NIP-01/Client", () => { beforeAll(() => { ws = new MockWebSocket(); - client = new Client(ws, { modules: [nip_01] }); + client = new Client(ws); }); afterAll(() => { client.close(); diff --git a/nips/01/protocol.ts b/nips/01/protocol.ts index f098b3d..f91aaf5 100644 --- a/nips/01/protocol.ts +++ b/nips/01/protocol.ts @@ -1,16 +1,17 @@ /** - * Implementation of extendable part of NIP-01 (Nostr basic protocol): + * Implementation of the "optional" part of NIP-01 (Nostr basic protocol): * https://github.com/nostr-protocol/nips/blob/master/01.md * - * See also `../../core/protocol.d.ts` for the unextendable part. + * See also `../../core/protocol.ts` for the unextendable part. * * @module */ -import "../../core/protocol.ts"; -import type { Url } from "../../lib/types.ts"; +import type { Url } from "@lophus/lib/types"; +import "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "../../core/protocol.ts" { +declare module "../protocol.ts" { interface NipRecord { 1: { ClientToRelayMessage: "EVENT" | "REQ" | "CLOSE"; @@ -19,6 +20,9 @@ declare module "../../core/protocol.ts" { Tag: "e" | "p" | "a" | "d"; }; } +} + +declare module "@lophus/core/protocol" { interface EventKindRecord { 0: { OptionalTag: Tag; @@ -33,6 +37,7 @@ declare module "../../core/protocol.ts" { Content: string; }; } + interface TagRecord { /** Event ID */ "e": [EventId, RelayUrl?]; @@ -45,6 +50,7 @@ declare module "../../core/protocol.ts" { /** Identifier */ "d": [string]; } + interface ClientToRelayMessageRecord< K extends EventKind = EventKind, > { @@ -52,6 +58,7 @@ declare module "../../core/protocol.ts" { REQ: [SubscriptionId, ...SubscriptionFilter[]]; CLOSE: [SubscriptionId]; } + interface RelayToClientMessageRecord< K extends EventKind = EventKind, > { diff --git a/nips/01/protocol_test.ts b/nips/01/protocol_test.ts index 911bfb6..ae4b45d 100644 --- a/nips/01/protocol_test.ts +++ b/nips/01/protocol_test.ts @@ -5,8 +5,8 @@ import type { PublicKey, SubscriptionFilter, Tag, -} from "../../core/protocol.ts"; -import { Timestamp } from "../../std/times.ts"; +} from "@lophus/core/protocol"; +import { Timestamp } from "@lophus/std/times"; import "./protocol.ts"; describe("Tag", () => { diff --git a/nips/01/relays.ts b/nips/01/relays.ts index ac89492..16eddbb 100644 --- a/nips/01/relays.ts +++ b/nips/01/relays.ts @@ -1,8 +1,7 @@ -import { EventRejected, RelayModule } from "../../core/relays.ts"; +import { EventRejected, Relay, SubscriptionClosed } from "@lophus/core/relays"; +import { NIPModule } from "../nodes.ts"; -export class SubscriptionClosed extends Error {} - -export const install: RelayModule["install"] = (relay) => { +export const M: NIPModule = (relay) => { relay.on("message", (message) => { switch (message[0]) { case "EVENT": @@ -15,7 +14,7 @@ export const install: RelayModule["install"] = (relay) => { } }); - relay.on("subscribe", ({ id, filters, options, controller }) => { + relay.on("subscribe", ({ id, filters, realtime, controller }) => { relay.on(id, (message) => { switch (message[0]) { case "EVENT": { @@ -23,10 +22,10 @@ export const install: RelayModule["install"] = (relay) => { return controller.enqueue(event); } case "EOSE": { - if (!options.realtime) { - controller.close(); + if (realtime) { + return; } - break; + return controller.close(); } case "CLOSED": { return controller.error(new SubscriptionClosed(message[2])); @@ -62,4 +61,4 @@ export const install: RelayModule["install"] = (relay) => { }); }; -export default { install }; +export default M; diff --git a/nips/01/relays_test.ts b/nips/01/relays_test.ts index f6d5c33..f905961 100644 --- a/nips/01/relays_test.ts +++ b/nips/01/relays_test.ts @@ -1,17 +1,19 @@ import { assert, assertEquals, assertInstanceOf } from "@std/assert"; import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; -import { MockWebSocket } from "../../lib/testing.ts"; -import { +import { MockWebSocket } from "@lophus/lib/testing"; +import type { ClientToRelayMessage, EventId, NostrEvent, RelayToClientMessage, SubscriptionId, -} from "../../core/protocol.ts"; -import { ConnectionClosed, EventRejected, Relay } from "../../core/relays.ts"; -import { SubscriptionClosed } from "./relays.ts"; -import nip_01 from "./relays.ts"; -import "./protocol.ts"; +} from "@lophus/core/protocol"; +import { + ConnectionClosed, + EventRejected, + SubscriptionClosed, +} from "@lophus/core/relays"; +import { Relay } from "../relays.ts"; function getRemoteSocket() { return MockWebSocket.instances[0].remote; @@ -25,7 +27,7 @@ describe("Relay (NIP-01)", () => { beforeAll(() => { globalThis.WebSocket = MockWebSocket; - relay = new Relay(url, { modules: [nip_01] }); + relay = new Relay(url); }); afterAll(() => { @@ -34,8 +36,8 @@ describe("Relay (NIP-01)", () => { } }); - it("should have loaded NIP-01 module", () => { - assertEquals(relay.config.modules.length, 1); + it("should not have any nips configured", () => { + assertEquals(relay.config.nips.length, 0); }); it("should create a subscription", () => { diff --git a/nips/02/protocol.ts b/nips/02/protocol.ts index 50878d7..4861dec 100644 --- a/nips/02/protocol.ts +++ b/nips/02/protocol.ts @@ -1,11 +1,15 @@ -import "../../core/protocol.ts"; +import "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "../../core/protocol.ts" { +declare module "../protocol.ts" { interface NipRecord { 2: { Tag: "p"; }; } +} + +declare module "@lophus/core/protocol" { interface EventKindRecord { 3: { Tags: ContactTag[]; diff --git a/nips/02/protocol_test.ts b/nips/02/protocol_test.ts index 6329096..3ab1f8b 100644 --- a/nips/02/protocol_test.ts +++ b/nips/02/protocol_test.ts @@ -1,7 +1,6 @@ import { describe, it } from "@std/testing/bdd"; import { assertType, Has } from "@std/testing/types"; -import { EventInit } from "../../std/events.ts"; -import type { EventId, PublicKey } from "../../core/protocol.ts"; +import type { EventId, EventInit, PublicKey } from "@lophus/core/protocol"; import "./protocol.ts"; describe("EventInit<3>", () => { diff --git a/nips/07/protocol.ts b/nips/07/protocol.ts index 3ecd70c..2063dda 100644 --- a/nips/07/protocol.ts +++ b/nips/07/protocol.ts @@ -1,20 +1,17 @@ -import { EventKind, NostrEvent, PublicKey } from "../../core/protocol.ts"; -import type { Optional } from "../../lib/types.ts"; +import { + EventKind, + NostrEvent, + PublicKey, + UnsignedEvent, +} from "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "../../core/protocol.ts" { +declare module "../protocol.ts" { interface NipRecord { 7: Record; } } -/** - * An event that has not been signed. - */ -export type UnsignedEvent = Optional< - NostrEvent, - "id" | "pubkey" | "sig" ->; - declare global { interface Window { nostr?: { diff --git a/nips/07/signs.ts b/nips/07/signs.ts deleted file mode 100644 index 3e8a56a..0000000 --- a/nips/07/signs.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Stringified } from "../../lib/types.ts"; -import { EventContent, EventKind, NostrEvent } from "../../core/protocol.ts"; -import { EventInit } from "../../std/events.ts"; -import { Timestamp } from "../../std/times.ts"; -import { UnsignedEvent } from "./protocol.ts"; - -/** - * A transform stream that signs events. - */ -export class Signer extends TransformStream { - constructor() { - // deno-lint-ignore no-window - if (!window.nostr) { - throw new Error("NIP-07 extension not installed"); - } - super({ - transform: (init, controller) => { - controller.enqueue(this.sign(init)); - }, - }); - } - sign(init: EventInit): NostrEvent { - const unsigned = { - created_at: Timestamp.now, - tags: [], - ...init, - content: JSON.stringify(init.content) as Stringified>, - // TODO: Can we avoid this type assertion? - } as UnsignedEvent; - // deno-lint-ignore no-window - return window.nostr!.signEvent(unsigned); - } -} diff --git a/nips/42/protocol.ts b/nips/42/protocol.ts index 38ec14a..56653b9 100644 --- a/nips/42/protocol.ts +++ b/nips/42/protocol.ts @@ -1,7 +1,7 @@ -import "../../core/protocol.ts"; -import "../../core/relays.ts"; +import "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "../../core/protocol.ts" { +declare module "../protocol.ts" { interface NipRecord { 42: { ClientToRelayMessage: "AUTH"; @@ -10,6 +10,9 @@ declare module "../../core/protocol.ts" { Tag: "relay" | "challenge"; }; } +} + +declare module "@lophus/core/protocol" { interface RelayToClientMessageRecord { AUTH: [challenge: string]; } diff --git a/nips/42/protocol_test.ts b/nips/42/protocol_test.ts index be50c37..6ff31d0 100644 --- a/nips/42/protocol_test.ts +++ b/nips/42/protocol_test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@std/testing/bdd"; import { assertType, Has } from "@std/testing/types"; -import { EventInit } from "../../std/events.ts"; +import { EventInit } from "@lophus/core/protocol"; import "./protocol.ts"; describe("EventInit<22242>", () => { diff --git a/nips/42/relays.ts b/nips/42/relays.ts index fb6590b..9f4da5f 100644 --- a/nips/42/relays.ts +++ b/nips/42/relays.ts @@ -1,16 +1,16 @@ -import type { Stringified } from "../../lib/types.ts"; -import type { RelayModule } from "../../core/relays.ts"; -import type { EventInit } from "../../std/events.ts"; -import type { Signer } from "../../std/signs.ts"; -import "./protocol.ts"; +import type { Stringified } from "@lophus/lib/types"; +import type { EventInit } from "@lophus/core/protocol"; +import type { Signer } from "@lophus/std/signs"; +import type { NIPModule } from "../nodes.ts"; +import { Relay } from "@lophus/core/relays"; -declare module "../../core/relays.ts" { +declare module "@lophus/core/relays" { interface RelayConfig { signer?: Signer; } } -const install: RelayModule["install"] = (relay) => { +const M: NIPModule = (relay) => { relay.on("message", (message) => { if (message[0] !== "AUTH") { // This NIP only handles AUTH messages @@ -32,4 +32,4 @@ const install: RelayModule["install"] = (relay) => { }); }; -export default { install }; +export default M; diff --git a/nips/clients.ts b/nips/clients.ts new file mode 100644 index 0000000..d0f09f8 --- /dev/null +++ b/nips/clients.ts @@ -0,0 +1,5 @@ +import { NIPsEnabled } from "./nodes.ts"; +import { Client as CoreClient } from "@lophus/core/clients"; +import base from "./01/clients.ts"; + +export class Client extends NIPsEnabled(CoreClient, base, "clients.ts", []) {} diff --git a/nips/deno.json b/nips/deno.json new file mode 100644 index 0000000..5a2d21d --- /dev/null +++ b/nips/deno.json @@ -0,0 +1,24 @@ +{ + "name": "@lophus/nips", + "version": "0.0.14", + "exports": { + ".": "./protocol.ts", + "./clients": "./clients.ts", + "./relays": "./relays.ts", + "./01": "./01/protocol.ts", + "./01/clients": "./01/clients.ts", + "./01/relays": "./01/relays.ts", + "./02": "./02/protocol.ts", + "./07": "./07/protocol.ts", + "./42": "./42/protocol.ts", + "./42/clients": "./42/clients.ts", + "./42/relays": "./42/relays.ts" + }, + "imports": { + "@lophus/lib": "jsr:@lophus/lib@0.0.14", + "@lophus/core": "jsr:@lophus/core@0.0.14" + }, + "publish": { + "exclude": ["*_test.ts"] + } +} diff --git a/nips/nodes.ts b/nips/nodes.ts new file mode 100644 index 0000000..2fc2d9e --- /dev/null +++ b/nips/nodes.ts @@ -0,0 +1,69 @@ +import { Node, NodeOptions } from "@lophus/core/nodes"; +import { NIP } from "./protocol.ts"; + +interface NodeConstructor { + // deno-lint-ignore no-explicit-any + new (init: any, options: NodeOptions): Node; +} + +// @ts-ignore 2430 FIXME +interface WithNIPs extends T { + config: T["config"] & { nips: N[] }; + readonly ready: Promise; +} + +interface NIPsEnabled { + new ( + init: ConstructorParameters[0], + options?: ConstructorParameters[1] & { nips?: N[] }, + ): WithNIPs, N>; +} + +export interface NIPModule { + (node: WithNIPs, N>): void; +} + +/** Convert a NIP to a string. If the NIP is less than 10, a leading zero is added. */ +function nipToString(nip: NIP | number) { + return nip > 9 ? nip.toString() : "0" + nip.toString(); +} + +export function NIPsEnabled( + Node: T, + base: NIPModule, + moduleBaseName: string, + moduleNIPs: Readonly, +): NIPsEnabled { + // @ts-ignore allow concrete arguments + return class extends Node { + declare config: InstanceType["config"] & { nips: N[] }; + readonly ready: Promise; + + constructor( + readonly init: ConstructorParameters[0], + options: ConstructorParameters[1] & { nips?: N[] } = {}, + ) { + super(init, options); + this.config.nips = options.nips ?? []; + this.ready = Promise.all( + this.config.nips.filter((nip) => moduleNIPs.includes(nip)).map( + async (nip) => { + const mod = await import( + `./${nipToString(nip)}/${moduleBaseName}` + ) as { default: NIPModule }; + mod.default(this); + }, + ), + ).then(); + base(this); + } + + override async dispatch( + type: Parameters["dispatch"]>[0], + data: Parameters["dispatch"]>[1], + ): Promise { + await this.ready; + super.dispatch(type, data); + } + }; +} diff --git a/nips/protocol.ts b/nips/protocol.ts new file mode 100644 index 0000000..9a71fb1 --- /dev/null +++ b/nips/protocol.ts @@ -0,0 +1,4 @@ +// deno-lint-ignore no-empty-interface +export interface NipRecord {} + +export type NIP = keyof NipRecord & number; diff --git a/nips/relays.ts b/nips/relays.ts new file mode 100644 index 0000000..d847d27 --- /dev/null +++ b/nips/relays.ts @@ -0,0 +1,5 @@ +import { NIPsEnabled } from "./nodes.ts"; +import { Relay as CoreRelay } from "@lophus/core/relays"; +import base from "./01/relays.ts"; + +export class Relay extends NIPsEnabled(CoreRelay, base, "relays.ts", [42]) {} diff --git a/std/deno.json b/std/deno.json index 8655c6b..40c434e 100644 --- a/std/deno.json +++ b/std/deno.json @@ -4,10 +4,16 @@ "exports": { ".": "./mod.ts", "./env": "./env.ts", - "./events": "./events.ts", - "./notes": "./notes.ts", "./pools": "./pools.ts", "./signs": "./signs.ts", "./times": "./times.ts" + }, + "imports": { + "@std/streams": "jsr:@std/streams@^0.219.1", + "@lophus/lib": "jsr:@lophus/lib@0.0.14", + "@lophus/core": "jsr:@lophus/core@0.0.14" + }, + "publish": { + "exclude": ["*_test.ts"] } } diff --git a/std/env.ts b/std/env.ts index e315641..9a36294 100644 --- a/std/env.ts +++ b/std/env.ts @@ -1,4 +1,3 @@ -import "@std/dotenv"; import type { PrivateKey, PublicKey } from "@lophus/core/protocol"; class Env { diff --git a/std/events.ts b/std/events.ts deleted file mode 100644 index 51b06b8..0000000 --- a/std/events.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Stringified } from "@lophus/lib/types"; -import type { - ClientToRelayMessage, - EventContent, - EventKind, - NostrEvent, -} from "@lophus/core/protocol"; -import "../nips/01/protocol.ts"; - -export interface EventInit { - kind: NostrEvent["kind"]; - tags?: NostrEvent["tags"]; - content: EventContent | Stringified>; -} - -import type { Signer } from "./signs.ts"; - -export class EventPublisher - extends TransformStream, ClientToRelayMessage<"EVENT", K>> { - constructor(readonly signer: Signer) { - super({ - transform: (init, controller) => { - controller.enqueue(["EVENT", this.signer.sign(init)]); - }, - }); - } -} diff --git a/std/events_test.ts b/std/events_test.ts deleted file mode 100644 index 5139c1f..0000000 --- a/std/events_test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; -import { assertEquals, assertInstanceOf } from "@std/assert"; -import { MockWebSocket } from "@lophus/lib/testing"; -import { Relay } from "@lophus/core/relays"; -import { PrivateKey, Signer } from "@lophus/std/signs"; -import { EventInit, EventPublisher } from "./events.ts"; - -describe("EventPublisher", () => { - let relay: Relay; - let signer: Signer; - let publisher: EventPublisher<1>; - - beforeAll(() => { - globalThis.WebSocket = MockWebSocket; - signer = new Signer(PrivateKey.generate()); - }); - afterAll(() => { - relay?.close(); - }); - - it("should be a TransformStream", () => { - publisher = new EventPublisher(signer); - assertInstanceOf(publisher, TransformStream); - }); - - // FIXME: dangling promise - // @ts-ignore missing property `ignore` - it.ignore( - "should transform EventInit to ClientToRelayMessage", - async () => { - const { readable, writable } = publisher; - const reader = readable.getReader(); - const writer = writable.getWriter(); - const init = { kind: 1, content: "hello" } satisfies EventInit<1>; - await writer.write(init); - const { value } = await reader.read(); - assertEquals(value, ["EVENT", signer.sign(init)]); - await writer.close(); - await reader.cancel(); - }, - ); - - // FIXME: runtime error - // @ts-ignore missing property `ignore` - it.ignore("should be connectable to a relay", () => { - relay = new Relay("wss://example.com"); - // const writable = pipeThroughFrom(relay.writable, publisher); - // assertInstanceOf(writable, WritableStream); - // await writable.close(); - }); -}); diff --git a/std/mod.ts b/std/mod.ts index 8573389..4a528f0 100644 --- a/std/mod.ts +++ b/std/mod.ts @@ -1,6 +1,4 @@ export * from "./env.ts"; -export * from "./events.ts"; -export * from "./notes.ts"; export * from "./times.ts"; export * from "./signs.ts"; export * from "./pools.ts"; diff --git a/std/notes.ts b/std/notes.ts index 0cca312..18f145b 100644 --- a/std/notes.ts +++ b/std/notes.ts @@ -1,10 +1,8 @@ -import type { Optional } from "@lophus/lib/types"; -import type { NostrEvent, RelayUrl } from "@lophus/core/protocol"; -import { type EventInit } from "@lophus/std/events"; +import type { EventInit, NostrEvent, RelayUrl } from "@lophus/core/protocol"; export type TextNote = EventInit<1>; -export type TextNoteInit = Optional; +export type TextNoteInit = Omit; export class TextNoteComposer extends TransformStream { constructor(readonly opts: { recommendedRelay?: RelayUrl } = {}) { diff --git a/std/signs.ts b/std/signs.ts index 1cfc96c..2483a80 100644 --- a/std/signs.ts +++ b/std/signs.ts @@ -1,33 +1,30 @@ import { schnorr } from "@noble/curves/secp256k1"; import { sha256 } from "@noble/hashes/sha256"; import { bytesToHex } from "@noble/hashes/utils"; -import type { Brand, Stringified } from "@lophus/lib/types"; +import type { Stringified } from "@lophus/lib/types"; import type { EventContent, EventId, + EventInit, EventKind, EventSerializePrecursor, NostrEvent, + PrivateKey, + PublicKey, Signature, + UnsignedEvent, } from "@lophus/core/protocol"; -import type { UnsignedEvent } from "../nips/07/protocol.ts"; -import type { EventInit } from "@lophus/std/events"; +import "@lophus/nips/01"; +import "@lophus/nips/07"; import { Timestamp } from "@lophus/std/times"; -export type { UnsignedEvent } from "../nips/07/protocol.ts"; - -export type PrivateKey = Brand; - -export const PrivateKey = { - generate: () => bytesToHex(schnorr.utils.randomPrivateKey()) as PrivateKey, -}; - -export type PublicKey = Brand; +export function generatePrivateKey(): PrivateKey { + return bytesToHex(schnorr.utils.randomPrivateKey()) as PrivateKey; +} -export const PublicKey = { - from: (nsec: PrivateKey) => - bytesToHex(schnorr.getPublicKey(nsec)) as PublicKey, -}; +export function fromPrivateKey(nsec: PrivateKey): PublicKey { + return bytesToHex(schnorr.getPublicKey(nsec)) as PublicKey; +} /** * A transform stream that signs events. @@ -46,16 +43,19 @@ export class Signer extends TransformStream { sign(event: EventInit): NostrEvent { if (isSigned(event)) return event; const unsigned = { - created_at: Timestamp.now, tags: [], ...event, + created_at: Timestamp.now, content: JSON.stringify(event.content) as Stringified>, // TODO: Can we avoid this type assertion? } as UnsignedEvent; - const precursor = { ...unsigned, pubkey: PublicKey.from(this.nsec) }; - const hash = sha256(this.#encoder.encode(serialize(precursor))); + const pubkey = fromPrivateKey(this.nsec); + const hash = sha256( + this.#encoder.encode(serialize({ ...unsigned, pubkey })), + ); return { - ...precursor, + ...unsigned, + pubkey, id: bytesToHex(hash) as EventId, sig: bytesToHex(schnorr.sign(hash, this.nsec)) as Signature, }; @@ -112,3 +112,31 @@ function serialize( } type SerializedEvent = string & { __kind: K }; + +/** + * A transform stream that signs events with a NIP-07 extention. + */ +export class NIP07Signer extends TransformStream { + constructor() { + // deno-lint-ignore no-window + if (!window.nostr) { + throw new Error("NIP-07 extension not installed"); + } + super({ + transform: (init, controller) => { + controller.enqueue(this.sign(init)); + }, + }); + } + sign(init: EventInit): NostrEvent { + const unsigned = { + tags: [], + ...init, + created_at: Timestamp.now, + content: JSON.stringify(init.content) as Stringified>, + // TODO: Can we avoid this type assertion? + } as UnsignedEvent; + // deno-lint-ignore no-window + return window.nostr!.signEvent(unsigned); + } +} diff --git a/std/signs_test.ts b/std/signs_test.ts index 92ffa85..32d7df5 100644 --- a/std/signs_test.ts +++ b/std/signs_test.ts @@ -2,34 +2,33 @@ import { assert, assertEquals } from "@std/assert"; import { describe, it } from "@std/testing/bdd"; import { Stringified } from "@lophus/lib/types"; import { Timestamp } from "@lophus/std/times"; +import { UnsignedEvent } from "@lophus/core/protocol"; import { - PrivateKey, - PublicKey, + fromPrivateKey, + generatePrivateKey, Signer, - UnsignedEvent, Verifier, } from "./signs.ts"; -describe("PrivateKey", () => { +describe("generatePrivateKey", () => { it("generates a private key", () => { - const nsec = PrivateKey.generate(); + const nsec = generatePrivateKey(); assertEquals(nsec.length, 64); }); }); -describe("PublicKey", () => { +describe("fromPrivateKey", () => { it("generates a public key from a private key", () => { - const nsec = PrivateKey.generate(); - const pubkey = PublicKey.from(nsec); + const nsec = generatePrivateKey(); + const pubkey = fromPrivateKey(nsec); assertEquals(pubkey.length, 64); }); }); describe("Signer/Verifier", () => { - const nsec = PrivateKey.generate(); + const nsec = generatePrivateKey(); const signer = new Signer(nsec); const event: UnsignedEvent = { - pubkey: PublicKey.from(nsec), created_at: Timestamp.now, kind: 1, tags: [],