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 +98 -2
- package/dist/__tests__/exports.test.d.ts +1 -0
- package/dist/__tests__/exports.test.js +22 -0
- package/dist/helpers/__tests__/exports.test.d.ts +1 -0
- package/dist/helpers/__tests__/exports.test.js +11 -0
- package/dist/signers/__tests__/exports.test.d.ts +1 -0
- package/dist/signers/__tests__/exports.test.js +21 -0
- package/dist/signers/__tests__/nostr-connect-signer.test.d.ts +1 -0
- package/dist/signers/__tests__/nostr-connect-signer.test.js +66 -0
- package/dist/signers/nostr-connect-signer.d.ts +5 -4
- package/dist/signers/nostr-connect-signer.js +24 -9
- package/dist/signers/nostr-connect-signer.test.js +1 -1
- package/dist/signers/serial-port-signer.d.ts +1 -1
- package/dist/signers/serial-port-signer.js +4 -4
- package/package.json +3 -3
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
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 = (
|
|
86
|
-
export type NostrPublishMethod = (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
96
|
+
if (this.listening)
|
|
96
97
|
return;
|
|
97
|
-
this.
|
|
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
|
-
]
|
|
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.
|
|
113
|
+
this.listening = false;
|
|
113
114
|
this.isConnected = false;
|
|
114
|
-
|
|
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?.(
|
|
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] }]
|
|
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 {
|
|
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 {
|
|
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-
|
|
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-
|
|
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.
|
|
48
|
+
"vitest": "^3.1.3"
|
|
49
49
|
},
|
|
50
50
|
"funding": {
|
|
51
51
|
"type": "lightning",
|