applesauce-signers 1.2.0 → 2.0.0

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.
@@ -2,4 +2,4 @@
2
2
  * Checks if a string is encrypted with NIP-04 or NIP-44
3
3
  * @see https://github.com/nostr-protocol/nips/pull/1248#issuecomment-2437731316
4
4
  */
5
- export declare function isNIP04(ciphertext: string): boolean;
5
+ export { isNIP04Encrypted as isNIP04 } from "applesauce-core/helpers/encryption";
@@ -2,9 +2,4 @@
2
2
  * Checks if a string is encrypted with NIP-04 or NIP-44
3
3
  * @see https://github.com/nostr-protocol/nips/pull/1248#issuecomment-2437731316
4
4
  */
5
- export function isNIP04(ciphertext) {
6
- const l = ciphertext.length;
7
- if (l < 28)
8
- return false;
9
- return (ciphertext[l - 28] == "?" && ciphertext[l - 27] == "i" && ciphertext[l - 26] == "v" && ciphertext[l - 25] == "=");
10
- }
5
+ export { isNIP04Encrypted as isNIP04 } from "applesauce-core/helpers/encryption";
@@ -82,8 +82,10 @@ interface Observer<T> {
82
82
  type Subscribable<T extends unknown> = {
83
83
  subscribe: (observer: Partial<Observer<T>>) => Unsubscribable;
84
84
  };
85
- export type NostrSubscriptionMethod = (relays: string[], filters: Filter[]) => Subscribable<NostrEvent>;
86
- export type NostrPublishMethod = (relays: string[], event: NostrEvent) => any | Promise<any>;
85
+ /** A method used to subscribe to events on a set of relays */
86
+ export type NostrSubscriptionMethod = (relays: string[], filters: Filter[]) => Subscribable<NostrEvent | string>;
87
+ /** A method used for publishing an event, can return a Promise that completes when published or an Observable that completes when published*/
88
+ export type NostrPublishMethod = (relays: string[], event: NostrEvent) => Promise<any> | Subscribable<any>;
87
89
  export type NostrConnectAppMetadata = {
88
90
  name?: string;
89
91
  image?: string;
@@ -98,7 +100,8 @@ export declare class NostrConnectSigner implements Nip07Interface {
98
100
  protected log: import("debug").Debugger;
99
101
  /** The local client signer */
100
102
  signer: SimpleSigner;
101
- protected subscriptionOpen: boolean;
103
+ /** Whether the signer is listening for events */
104
+ listening: boolean;
102
105
  /** Whether the signer is connected to the remote signer */
103
106
  isConnected: boolean;
104
107
  /** The users pubkey */
@@ -143,7 +146,7 @@ export declare class NostrConnectSigner implements Nip07Interface {
143
146
  connect(secret?: string | undefined, permissions?: string[]): Promise<"ack">;
144
147
  private waitingPromise;
145
148
  /** Wait for a remote signer to connect */
146
- waitForSigner(): Promise<void>;
149
+ waitForSigner(abort?: AbortSignal): Promise<void>;
147
150
  /** Request to create an account on the remote signer */
148
151
  createAccount(username: string, domain: string, email?: string, permissions?: string[]): Promise<string>;
149
152
  /** 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,9 +93,9 @@ 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
101
  this.req = this.subscriptionMethod(this.relays, [
@@ -103,15 +104,24 @@ export class NostrConnectSigner {
103
104
  "#p": [pubkey],
104
105
  },
105
106
  ]).subscribe({
106
- next: (event) => this.handleEvent(event),
107
+ next: (event) => typeof event !== "string" && 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();
@@ -185,10 +195,16 @@ export class NostrConnectSigner {
185
195
  const request = { id, method, params };
186
196
  const encrypted = await this.signer.nip44.encrypt(this.remote, JSON.stringify(request));
187
197
  const event = await this.createRequestEvent(encrypted, this.remote, kind);
188
- this.log(`Sending request ${id} (${method}) ${JSON.stringify(params)}`);
198
+ this.log(`Sending ${id} (${method}) ${JSON.stringify(params)}`);
189
199
  const p = createDefer();
190
200
  this.requests.set(id, p);
191
- await this.publishMethod?.(this.relays, event);
201
+ const result = this.publishMethod?.(this.relays, event);
202
+ // Handle returned Promise or Observable
203
+ if (result instanceof Promise)
204
+ await result;
205
+ else if ("subscribe" in result)
206
+ await new Promise((res) => result.subscribe({ complete: res }));
207
+ this.log(`Sent ${id} (${method})`);
192
208
  return p;
193
209
  }
194
210
  /** Connect to remote signer */
@@ -216,11 +232,16 @@ export class NostrConnectSigner {
216
232
  }
217
233
  waitingPromise = null;
218
234
  /** Wait for a remote signer to connect */
219
- waitForSigner() {
235
+ waitForSigner(abort) {
220
236
  if (this.isConnected)
221
237
  return Promise.resolve();
222
238
  this.open();
223
239
  this.waitingPromise = createDefer();
240
+ abort?.addEventListener("abort", () => {
241
+ this.waitingPromise?.reject(new Error("Aborted"));
242
+ this.waitingPromise = null;
243
+ this.close();
244
+ }, true);
224
245
  return this.waitingPromise;
225
246
  }
226
247
  /** Request to create an account on the remote signer */
@@ -17,13 +17,22 @@ export declare class PasswordSigner implements Nip07Interface {
17
17
  constructor();
18
18
  unlockPromise?: Deferred<void>;
19
19
  protected requestUnlock(): Deferred<void> | undefined;
20
+ /** Sets the ncryptsec from the key and password */
20
21
  setPassword(password: string): Promise<void>;
22
+ /** Tests if the provided password is correct by decrypting the ncryptsec */
21
23
  testPassword(password: string): Promise<void>;
24
+ /** Unlocks the signer by decrypting the ncryptsec using the provided password */
22
25
  unlock(password: string): Promise<void>;
26
+ /** Locks the signer by removing the unencrypted key from memory */
27
+ lock(): void;
23
28
  getPublicKey(): Promise<string>;
24
29
  signEvent(event: EventTemplate): Promise<import("nostr-tools").VerifiedEvent>;
25
30
  nip04Encrypt(pubkey: string, plaintext: string): Promise<string>;
26
31
  nip04Decrypt(pubkey: string, ciphertext: string): Promise<string>;
27
32
  nip44Encrypt(pubkey: string, plaintext: string): Promise<string>;
28
33
  nip44Decrypt(pubkey: string, ciphertext: string): Promise<string>;
34
+ /** Creates a PasswordSigner from a hex private key or NIP-19 nsec and password */
35
+ static fromPrivateKey(privateKey: Uint8Array | string, password: string): Promise<PasswordSigner>;
36
+ /** Creates a PasswordSigner from a ncryptsec and unlocks it with the provided password */
37
+ static fromNcryptsec(ncryptsec: string, password?: string): Promise<PasswordSigner>;
29
38
  }
@@ -1,6 +1,7 @@
1
1
  import { finalizeEvent, getPublicKey, nip04, nip44 } from "nostr-tools";
2
2
  import { encrypt, decrypt } from "nostr-tools/nip49";
3
3
  import { createDefer } from "applesauce-core/promise";
4
+ import { normalizeToSecretKey } from "applesauce-core/helpers";
4
5
  /** A NIP-49 (Private Key Encryption) signer */
5
6
  export class PasswordSigner {
6
7
  key = null;
@@ -30,11 +31,13 @@ export class PasswordSigner {
30
31
  this.unlockPromise = p;
31
32
  return p;
32
33
  }
34
+ /** Sets the ncryptsec from the key and password */
33
35
  async setPassword(password) {
34
36
  if (!this.key)
35
37
  throw new Error("Cant set password until unlocked");
36
38
  this.ncryptsec = encrypt(this.key, password);
37
39
  }
40
+ /** Tests if the provided password is correct by decrypting the ncryptsec */
38
41
  async testPassword(password) {
39
42
  if (this.ncryptsec) {
40
43
  const key = decrypt(this.ncryptsec, password);
@@ -44,6 +47,7 @@ export class PasswordSigner {
44
47
  else
45
48
  throw new Error("Missing ncryptsec");
46
49
  }
50
+ /** Unlocks the signer by decrypting the ncryptsec using the provided password */
47
51
  async unlock(password) {
48
52
  if (this.key)
49
53
  return;
@@ -55,6 +59,10 @@ export class PasswordSigner {
55
59
  else
56
60
  throw new Error("Missing ncryptsec");
57
61
  }
62
+ /** Locks the signer by removing the unencrypted key from memory */
63
+ lock() {
64
+ this.key = null;
65
+ }
58
66
  // public methods
59
67
  async getPublicKey() {
60
68
  await this.requestUnlock();
@@ -82,4 +90,19 @@ export class PasswordSigner {
82
90
  await this.requestUnlock();
83
91
  return nip44.v2.decrypt(ciphertext, nip44.v2.utils.getConversationKey(this.key, pubkey));
84
92
  }
93
+ /** Creates a PasswordSigner from a hex private key or NIP-19 nsec and password */
94
+ static async fromPrivateKey(privateKey, password) {
95
+ const signer = new PasswordSigner();
96
+ signer.key = normalizeToSecretKey(privateKey);
97
+ await signer.setPassword(password);
98
+ return signer;
99
+ }
100
+ /** Creates a PasswordSigner from a ncryptsec and unlocks it with the provided password */
101
+ static async fromNcryptsec(ncryptsec, password) {
102
+ const signer = new PasswordSigner();
103
+ signer.ncryptsec = ncryptsec;
104
+ if (password)
105
+ await signer.unlock(password);
106
+ return signer;
107
+ }
85
108
  }
@@ -19,4 +19,6 @@ export declare class ReadonlySigner implements Nip07Interface {
19
19
  nip04Decrypt(): string;
20
20
  nip44Encrypt(): string;
21
21
  nip44Decrypt(): string;
22
+ /** Creates a ReadonlySigner from a hex public key or NIP-19 npub */
23
+ static fromPubkey(pubkey: string): ReadonlySigner;
22
24
  }
@@ -1,3 +1,4 @@
1
+ import { isHexKey, normalizeToPubkey } from "applesauce-core/helpers";
1
2
  /** A signer that only implements getPublicKey and throws on ever other method */
2
3
  export class ReadonlySigner {
3
4
  pubkey;
@@ -5,6 +6,8 @@ export class ReadonlySigner {
5
6
  nip44;
6
7
  constructor(pubkey) {
7
8
  this.pubkey = pubkey;
9
+ if (!isHexKey(pubkey))
10
+ throw new Error("Invalid public key");
8
11
  this.nip04 = {
9
12
  encrypt: this.nip04Encrypt.bind(this),
10
13
  decrypt: this.nip04Decrypt.bind(this),
@@ -35,4 +38,8 @@ export class ReadonlySigner {
35
38
  nip44Decrypt() {
36
39
  throw new Error("Cant decrypt with readonly");
37
40
  }
41
+ /** Creates a ReadonlySigner from a hex public key or NIP-19 npub */
42
+ static fromPubkey(pubkey) {
43
+ return new ReadonlySigner(normalizeToPubkey(pubkey));
44
+ }
38
45
  }
@@ -13,4 +13,6 @@ export declare class SimpleSigner {
13
13
  encrypt: (pubkey: string, plaintext: string) => Promise<string>;
14
14
  decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
15
15
  };
16
+ /** Creates a SimpleSigner from a hex private key or NIP-19 nsec */
17
+ static fromKey(privateKey: Uint8Array | string): SimpleSigner;
16
18
  }
@@ -1,3 +1,4 @@
1
+ import { normalizeToSecretKey } from "applesauce-core/helpers";
1
2
  import { finalizeEvent, generateSecretKey, getPublicKey, nip04, nip44 } from "nostr-tools";
2
3
  /** A Simple NIP-07 signer class */
3
4
  export class SimpleSigner {
@@ -19,4 +20,8 @@ export class SimpleSigner {
19
20
  encrypt: async (pubkey, plaintext) => nip44.v2.encrypt(plaintext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
20
21
  decrypt: async (pubkey, ciphertext) => nip44.v2.decrypt(ciphertext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
21
22
  };
23
+ /** Creates a SimpleSigner from a hex private key or NIP-19 nsec */
24
+ static fromKey(privateKey) {
25
+ return new SimpleSigner(normalizeToSecretKey(privateKey));
26
+ }
22
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-signers",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Signer classes for applesauce",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,16 +36,16 @@
36
36
  "@noble/hashes": "^1.7.1",
37
37
  "@noble/secp256k1": "^1.7.1",
38
38
  "@scure/base": "^1.2.4",
39
- "applesauce-core": "^1.2.0",
39
+ "applesauce-core": "^2.0.0",
40
40
  "debug": "^4.4.0",
41
41
  "nanoid": "^5.0.9",
42
- "nostr-tools": "^2.10.4"
42
+ "nostr-tools": "^2.13"
43
43
  },
44
44
  "devDependencies": {
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",
@@ -1 +0,0 @@
1
- export {};
@@ -1,22 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,11 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,21 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,23 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { NostrConnectSigner } from "../nostr-connect-signer.js";
3
- import { SimpleSigner } from "../simple-signer.js";
4
- describe("NostrConnectSigner", () => {
5
- describe("connection", () => {
6
- it("should call subscription method with filters", async () => {
7
- const relays = ["wss://relay.signer.com"];
8
- const subscription = vi.fn().mockReturnValue({ subscribe: vi.fn() });
9
- const publish = vi.fn(async () => { });
10
- const client = new SimpleSigner();
11
- const remote = new SimpleSigner();
12
- const signer = new NostrConnectSigner({
13
- relays,
14
- remote: await remote.getPublicKey(),
15
- signer: client,
16
- subscriptionMethod: subscription,
17
- publishMethod: publish,
18
- });
19
- signer.connect();
20
- expect(subscription).toHaveBeenCalledWith(relays, [{ "#p": [await client.getPublicKey()], kinds: [24133] }]);
21
- });
22
- });
23
- });