applesauce-signers 0.0.0-next-20250428223834 → 0.0.0-next-20250511152752

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # applesauce-signer
2
2
 
3
- A collection of signer classes for applesauce
3
+ A collection of signer classes for applesauce that are compatible with the [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) API.
4
4
 
5
- See [documentation](https://hzrd149.github.io/applesauce/signers/signers.html)
5
+ ## Documentation
6
+
7
+ For detailed documentation and API reference, see:
8
+
9
+ - [Signers Documentation](https://hzrd149.github.io/applesauce/signers/signers.html)
10
+ - [Nostr Connect Documentation](https://hzrd149.github.io/applesauce/signers/nostr-connect.html)
11
+ - [API Reference](https://hzrd149.github.io/applesauce/typedoc/modules/applesauce-signers.html)
12
+
13
+ ## Available Signers
14
+
15
+ ### Password Signer (NIP-49)
16
+
17
+ A secure signer that encrypts private keys using [NIP-49](https://github.com/nostr-protocol/nips/blob/master/49.md).
18
+
19
+ ```ts
20
+ // Create a new password signer
21
+ const signer = new PasswordSigner();
22
+
23
+ // Set up with a new key and password
24
+ const randomBytes = new Uint8Array(64);
25
+ window.crypto.getRandomValues(randomBytes);
26
+
27
+ signer.key = randomBytes;
28
+ signer.setPassword("your-password");
29
+
30
+ // Unlock the signer when needed
31
+ await signer.unlock("your-password");
32
+ ```
33
+
34
+ ### Simple Signer
35
+
36
+ A basic signer that holds the secret key in memory with NIP-04 and NIP-44 encryption support.
37
+
38
+ ```ts
39
+ // Create new signer with random key
40
+ const signer = new SimpleSigner();
41
+
42
+ // Or import existing key
43
+ const key = new Uint8Array(32);
44
+ window.crypto.getRandomValues(key);
45
+ const signer = new SimpleSigner(key);
46
+ ```
47
+
48
+ ### Nostr Connect Signer (NIP-46)
49
+
50
+ A client-side implementation for remote signing using [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md).
51
+
52
+ ```ts
53
+ // First, set up the required relay communication methods
54
+ import { Observable } from "rxjs";
55
+
56
+ // Define subscription method for receiving events
57
+ const subscriptionMethod = (filters, relays) => {
58
+ return new Observable((observer) => {
59
+ // Create subscription to relays
60
+ const cleanup = subscribeToRelays(relays, filters, (event) => {
61
+ observer.next(event);
62
+ });
63
+ return () => cleanup();
64
+ });
65
+ };
66
+
67
+ // Define publish method for sending events
68
+ const publishMethod = async (event, relays) => {
69
+ for (const relay of relays) await publishToRelay(relay, event);
70
+ };
71
+
72
+ // You can set these methods globally at app initialization
73
+ NostrConnectSigner.subscriptionMethod = subscriptionMethod;
74
+ NostrConnectSigner.publishMethod = publishMethod;
75
+
76
+ // Now create and use the signer
77
+ const signer = new NostrConnectSigner({
78
+ remote: "<remote signer pubkey>",
79
+ relays: ["wss://relay.example.com"],
80
+ // Or pass methods directly to the constructor
81
+ subscriptionMethod,
82
+ publishMethod,
83
+ });
84
+
85
+ // Create a connection URI for your app
86
+ const uri = signer.getNostrConnectURI({
87
+ name: "My App",
88
+ url: "https://example.com",
89
+ permissions: NostrConnectSigner.buildSigningPermissions([0, 1, 3]),
90
+ });
91
+
92
+ // Connect using bunker URI
93
+ const bunkerSigner = await NostrConnectSigner.fromBunkerURI("bunker://...your-uri-here...", {
94
+ permissions: NostrConnectSigner.buildSigningPermissions([0, 1, 3]),
95
+ });
96
+ ```
97
+
98
+ ### Other Signers
99
+
100
+ - **Serial Port Signer**: For hardware signing devices (Chrome browsers only)
101
+ - **Amber Clipboard Signer**: Integration with Amber wallet's web API
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as exports from "../index.js";
3
+ describe("exports", () => {
4
+ it("should export the expected functions", () => {
5
+ expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
6
+ [
7
+ "AmberClipboardSigner",
8
+ "ExtensionMissingError",
9
+ "ExtensionSigner",
10
+ "Helpers",
11
+ "NostrConnectMethod",
12
+ "NostrConnectSigner",
13
+ "PasswordSigner",
14
+ "Permission",
15
+ "ReadonlySigner",
16
+ "SerialPortSigner",
17
+ "SimpleSigner",
18
+ "isErrorResponse",
19
+ ]
20
+ `);
21
+ });
22
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as exports from "../index.js";
3
+ describe("exports", () => {
4
+ it("should export the expected functions", () => {
5
+ expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
6
+ [
7
+ "isNIP04",
8
+ ]
9
+ `);
10
+ });
11
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as exports from "../index.js";
3
+ describe("exports", () => {
4
+ it("should export the expected functions", () => {
5
+ expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
6
+ [
7
+ "AmberClipboardSigner",
8
+ "ExtensionMissingError",
9
+ "ExtensionSigner",
10
+ "NostrConnectMethod",
11
+ "NostrConnectSigner",
12
+ "PasswordSigner",
13
+ "Permission",
14
+ "ReadonlySigner",
15
+ "SerialPortSigner",
16
+ "SimpleSigner",
17
+ "isErrorResponse",
18
+ ]
19
+ `);
20
+ });
21
+ });
@@ -0,0 +1,66 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { NostrConnectSigner } from "../nostr-connect-signer.js";
3
+ import { SimpleSigner } from "../simple-signer.js";
4
+ const relays = ["wss://relay.signer.com"];
5
+ const client = new SimpleSigner();
6
+ const remote = new SimpleSigner();
7
+ const observable = { unsubscribe: vi.fn() };
8
+ const req = { subscribe: vi.fn().mockReturnValue(observable) };
9
+ const subscriptionMethod = vi.fn().mockReturnValue(req);
10
+ const publishMethod = vi.fn(async () => { });
11
+ let signer;
12
+ beforeEach(async () => {
13
+ observable.unsubscribe.mockClear();
14
+ req.subscribe.mockClear();
15
+ subscriptionMethod.mockClear();
16
+ publishMethod.mockClear();
17
+ signer = new NostrConnectSigner({
18
+ relays,
19
+ remote: await remote.getPublicKey(),
20
+ signer: client,
21
+ subscriptionMethod,
22
+ publishMethod,
23
+ });
24
+ });
25
+ describe("connection", () => {
26
+ it("should call subscription method with filters", async () => {
27
+ signer.connect();
28
+ expect(subscriptionMethod).toHaveBeenCalledWith(relays, [{ "#p": [await client.getPublicKey()], kinds: [24133] }]);
29
+ });
30
+ });
31
+ describe("open", () => {
32
+ it("should call subscription method with filters", async () => {
33
+ signer.open();
34
+ expect(subscriptionMethod).toHaveBeenCalledWith(relays, [{ "#p": [await client.getPublicKey()], kinds: [24133] }]);
35
+ });
36
+ });
37
+ describe("waitForSigner", () => {
38
+ it("should accept an abort signal", async () => {
39
+ const signer = new NostrConnectSigner({
40
+ relays: ["wss://relay.signer.com"],
41
+ signer: client,
42
+ subscriptionMethod,
43
+ publishMethod,
44
+ });
45
+ const controller = new AbortController();
46
+ const p = signer.waitForSigner(controller.signal);
47
+ setTimeout(() => {
48
+ controller.abort();
49
+ }, 10);
50
+ await expect(p).rejects.toThrow("Aborted");
51
+ expect(signer.listening).toBe(false);
52
+ });
53
+ });
54
+ describe("close", () => {
55
+ it("should close the connection", async () => {
56
+ await signer.open();
57
+ expect(req.subscribe).toHaveBeenCalled();
58
+ await signer.close();
59
+ expect(observable.unsubscribe).toHaveBeenCalled();
60
+ });
61
+ it("it should cancel waiting for signer promie", async () => {
62
+ const p = signer.waitForSigner();
63
+ await signer.close();
64
+ await expect(p).rejects.toThrow("Closed");
65
+ });
66
+ });
@@ -82,8 +82,8 @@ interface Observer<T> {
82
82
  type Subscribable<T extends unknown> = {
83
83
  subscribe: (observer: Partial<Observer<T>>) => Unsubscribable;
84
84
  };
85
- export type NostrSubscriptionMethod = (filters: Filter[], relays: string[]) => Subscribable<NostrEvent>;
86
- export type NostrPublishMethod = (event: NostrEvent, relays: string[]) => void | Promise<void>;
85
+ export type NostrSubscriptionMethod = (relays: string[], filters: Filter[]) => Subscribable<NostrEvent>;
86
+ export type NostrPublishMethod = (relays: string[], event: NostrEvent) => any | Promise<any>;
87
87
  export type NostrConnectAppMetadata = {
88
88
  name?: string;
89
89
  image?: string;
@@ -98,7 +98,8 @@ export declare class NostrConnectSigner implements Nip07Interface {
98
98
  protected log: import("debug").Debugger;
99
99
  /** The local client signer */
100
100
  signer: SimpleSigner;
101
- protected subscriptionOpen: boolean;
101
+ /** Whether the signer is listening for events */
102
+ listening: boolean;
102
103
  /** Whether the signer is connected to the remote signer */
103
104
  isConnected: boolean;
104
105
  /** The users pubkey */
@@ -143,7 +144,7 @@ export declare class NostrConnectSigner implements Nip07Interface {
143
144
  connect(secret?: string | undefined, permissions?: string[]): Promise<"ack">;
144
145
  private waitingPromise;
145
146
  /** Wait for a remote signer to connect */
146
- waitForSigner(): Promise<void>;
147
+ waitForSigner(abort?: AbortSignal): Promise<void>;
147
148
  /** Request to create an account on the remote signer */
148
149
  createAccount(username: string, domain: string, email?: string, permissions?: string[]): Promise<string>;
149
150
  /** Ensure the signer is connected to the remote signer */
@@ -40,7 +40,8 @@ export class NostrConnectSigner {
40
40
  log = logger.extend("NostrConnectSigner");
41
41
  /** The local client signer */
42
42
  signer;
43
- subscriptionOpen = false;
43
+ /** Whether the signer is listening for events */
44
+ listening = false;
44
45
  /** Whether the signer is connected to the remote signer */
45
46
  isConnected = false;
46
47
  /** The users pubkey */
@@ -92,26 +93,35 @@ export class NostrConnectSigner {
92
93
  req;
93
94
  /** Open the connection */
94
95
  async open() {
95
- if (this.subscriptionOpen)
96
+ if (this.listening)
96
97
  return;
97
- this.subscriptionOpen = true;
98
+ this.listening = true;
98
99
  const pubkey = await this.signer.getPublicKey();
99
100
  // Setup subscription
100
- this.req = this.subscriptionMethod([
101
+ this.req = this.subscriptionMethod(this.relays, [
101
102
  {
102
103
  kinds: [kinds.NostrConnect],
103
104
  "#p": [pubkey],
104
105
  },
105
- ], this.relays).subscribe({
106
+ ]).subscribe({
106
107
  next: (event) => this.handleEvent(event),
107
108
  });
108
109
  this.log("Opened", this.relays);
109
110
  }
110
111
  /** Close the connection */
111
112
  async close() {
112
- this.subscriptionOpen = false;
113
+ this.listening = false;
113
114
  this.isConnected = false;
114
- this.req?.unsubscribe();
115
+ // Close the current subscription
116
+ if (this.req) {
117
+ this.req.unsubscribe();
118
+ this.req = undefined;
119
+ }
120
+ // Cancel waiting promise
121
+ if (this.waitingPromise) {
122
+ this.waitingPromise.reject(new Error("Closed"));
123
+ this.waitingPromise = null;
124
+ }
115
125
  this.log("Closed");
116
126
  }
117
127
  requests = new Map();
@@ -188,7 +198,7 @@ export class NostrConnectSigner {
188
198
  this.log(`Sending request ${id} (${method}) ${JSON.stringify(params)}`);
189
199
  const p = createDefer();
190
200
  this.requests.set(id, p);
191
- await this.publishMethod?.(event, this.relays);
201
+ await this.publishMethod?.(this.relays, event);
192
202
  return p;
193
203
  }
194
204
  /** Connect to remote signer */
@@ -216,11 +226,16 @@ export class NostrConnectSigner {
216
226
  }
217
227
  waitingPromise = null;
218
228
  /** Wait for a remote signer to connect */
219
- waitForSigner() {
229
+ waitForSigner(abort) {
220
230
  if (this.isConnected)
221
231
  return Promise.resolve();
222
232
  this.open();
223
233
  this.waitingPromise = createDefer();
234
+ abort?.addEventListener("abort", () => {
235
+ this.waitingPromise?.reject(new Error("Aborted"));
236
+ this.waitingPromise = null;
237
+ this.close();
238
+ }, true);
224
239
  return this.waitingPromise;
225
240
  }
226
241
  /** Request to create an account on the remote signer */
@@ -17,7 +17,7 @@ describe("NostrConnectSigner", () => {
17
17
  publishMethod: publish,
18
18
  });
19
19
  signer.connect();
20
- expect(subscription).toHaveBeenCalledWith([{ "#p": [await client.getPublicKey()], kinds: [24133] }], relays);
20
+ expect(subscription).toHaveBeenCalledWith(relays, [{ "#p": [await client.getPublicKey()], kinds: [24133] }]);
21
21
  });
22
22
  });
23
23
  });
@@ -1,5 +1,5 @@
1
- import { EventTemplate, verifyEvent } from "nostr-tools";
2
1
  import { Deferred } from "applesauce-core/promise";
2
+ import { EventTemplate, verifyEvent } from "nostr-tools";
3
3
  import { Nip07Interface } from "../nip-07.js";
4
4
  type Callback = () => void;
5
5
  type DeviceOpts = {
@@ -1,10 +1,10 @@
1
1
  /// <reference types="@types/dom-serial" />
2
- import { getEventHash, verifyEvent } from "nostr-tools";
3
- import { base64 } from "@scure/base";
4
- import { randomBytes, hexToBytes, bytesToHex } from "@noble/hashes/utils";
2
+ import { bytesToHex, hexToBytes, randomBytes } from "@noble/hashes/utils";
5
3
  import { Point } from "@noble/secp256k1";
4
+ import { base64 } from "@scure/base";
5
+ import { logger } from "applesauce-core";
6
6
  import { createDefer } from "applesauce-core/promise";
7
- import { logger } from "../logger.js";
7
+ import { getEventHash, verifyEvent } from "nostr-tools";
8
8
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
9
  function xOnlyToXY(p) {
10
10
  return Point.fromHex(p).toHex().substring(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-signers",
3
- "version": "0.0.0-next-20250428223834",
3
+ "version": "0.0.0-next-20250511152752",
4
4
  "description": "Signer classes for applesauce",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,7 +36,7 @@
36
36
  "@noble/hashes": "^1.7.1",
37
37
  "@noble/secp256k1": "^1.7.1",
38
38
  "@scure/base": "^1.2.4",
39
- "applesauce-core": "0.0.0-next-20250428223834",
39
+ "applesauce-core": "0.0.0-next-20250511152752",
40
40
  "debug": "^4.4.0",
41
41
  "nanoid": "^5.0.9",
42
42
  "nostr-tools": "^2.10.4"
@@ -45,7 +45,7 @@
45
45
  "@types/debug": "^4.1.12",
46
46
  "@types/dom-serial": "^1.0.6",
47
47
  "typescript": "^5.8.3",
48
- "vitest": "^3.1.1"
48
+ "vitest": "^3.1.3"
49
49
  },
50
50
  "funding": {
51
51
  "type": "lightning",