applesauce-signers 6.0.1 → 6.2.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.
- package/README.md +6 -0
- package/dist/helpers/nostr-connect.d.ts +30 -5
- package/dist/helpers/nostr-connect.js +47 -9
- package/dist/signers/nostr-connect-provider.d.ts +18 -4
- package/dist/signers/nostr-connect-provider.js +55 -12
- package/dist/signers/nostr-connect-signer.d.ts +37 -6
- package/dist/signers/nostr-connect-signer.js +82 -13
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -93,8 +93,14 @@ const uri = signer.getNostrConnectURI({
|
|
|
93
93
|
const bunkerSigner = await NostrConnectSigner.fromBunkerURI("bunker://...your-uri-here...", {
|
|
94
94
|
permissions: NostrConnectSigner.buildSigningPermissions([0, 1, 3]),
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
// Import or export a complete NIP-46 client session
|
|
98
|
+
const sessionSigner = await NostrConnectSigner.fromNbunksec("nbunksec1...");
|
|
99
|
+
const encodedSession = sessionSigner.getNbunksec();
|
|
96
100
|
```
|
|
97
101
|
|
|
102
|
+
`nbunksec` values include the local client private key and should be handled like secrets.
|
|
103
|
+
|
|
98
104
|
### Other Signers
|
|
99
105
|
|
|
100
106
|
- **Serial Port Signer**: For hardware signing devices (Chrome browsers only)
|
|
@@ -19,7 +19,8 @@ export declare enum NostrConnectMethod {
|
|
|
19
19
|
Nip04Decrypt = "nip04_decrypt",
|
|
20
20
|
Nip44Encrypt = "nip44_encrypt",
|
|
21
21
|
Nip44Decrypt = "nip44_decrypt",
|
|
22
|
-
SwitchRelays = "switch_relays"
|
|
22
|
+
SwitchRelays = "switch_relays",
|
|
23
|
+
Logout = "logout"
|
|
23
24
|
}
|
|
24
25
|
export type ConnectRequestParams = {
|
|
25
26
|
[NostrConnectMethod.Connect]: [string] | [string, string] | [string, string, string];
|
|
@@ -32,6 +33,7 @@ export type ConnectRequestParams = {
|
|
|
32
33
|
[NostrConnectMethod.Nip44Encrypt]: [string, string];
|
|
33
34
|
[NostrConnectMethod.Nip44Decrypt]: [string, string];
|
|
34
35
|
[NostrConnectMethod.SwitchRelays]: [];
|
|
36
|
+
[NostrConnectMethod.Logout]: [];
|
|
35
37
|
};
|
|
36
38
|
export type ConnectResponseResults = {
|
|
37
39
|
[NostrConnectMethod.Connect]: "ack" | string;
|
|
@@ -44,6 +46,7 @@ export type ConnectResponseResults = {
|
|
|
44
46
|
[NostrConnectMethod.Nip44Encrypt]: string;
|
|
45
47
|
[NostrConnectMethod.Nip44Decrypt]: string;
|
|
46
48
|
[NostrConnectMethod.SwitchRelays]: string[] | null;
|
|
49
|
+
[NostrConnectMethod.Logout]: "ack";
|
|
47
50
|
};
|
|
48
51
|
export type NostrConnectRequest<N extends NostrConnectMethod> = {
|
|
49
52
|
id: string;
|
|
@@ -64,12 +67,24 @@ export type NostrConnectErrorResponse = {
|
|
|
64
67
|
export type BunkerURI = {
|
|
65
68
|
remote: string;
|
|
66
69
|
relays: string[];
|
|
70
|
+
/** @deprecated Use bunkerSecret instead */
|
|
67
71
|
secret?: string;
|
|
72
|
+
/** The secret used to authorize the connection to the bunker (the `secret` in a bunker:// URI) */
|
|
73
|
+
bunkerSecret?: string;
|
|
74
|
+
};
|
|
75
|
+
/** An nbunksec-encoded NIP-46 signer session */
|
|
76
|
+
export type Nbunksec = BunkerURI & {
|
|
77
|
+
/** The local client private key as hex */
|
|
78
|
+
clientKey: string;
|
|
68
79
|
};
|
|
69
80
|
/** Parse a bunker:// URI */
|
|
70
81
|
export declare function parseBunkerURI(uri: string): BunkerURI;
|
|
71
82
|
/** Creates a bunker:// URI from a {@link BunkerURI} object */
|
|
72
83
|
export declare function createBunkerURI(data: BunkerURI): string;
|
|
84
|
+
/** Parse an nbunksec encoded NIP-46 signer session */
|
|
85
|
+
export declare function parseNbunksec(encoded: string): Nbunksec;
|
|
86
|
+
/** Create an nbunksec encoded NIP-46 signer session */
|
|
87
|
+
export declare function createNbunksec(data: Nbunksec): string;
|
|
73
88
|
/** App metadata for a nostrconnect:// URI */
|
|
74
89
|
export type NostrConnectAppMetadata = {
|
|
75
90
|
/** The name of the client */
|
|
@@ -81,20 +96,30 @@ export type NostrConnectAppMetadata = {
|
|
|
81
96
|
/** The permissions the client has */
|
|
82
97
|
permissions?: string[];
|
|
83
98
|
};
|
|
84
|
-
|
|
85
|
-
export type NostrConnectURI = {
|
|
99
|
+
type NostrConnectURIBase = {
|
|
86
100
|
/** The pubkey of the client */
|
|
87
101
|
client: string;
|
|
88
|
-
/** The secret used by the signer to connect to the client */
|
|
89
|
-
secret: string;
|
|
90
102
|
/** The relays used to communicate with the remote signer */
|
|
91
103
|
relays: string[];
|
|
92
104
|
/** The metadata of the client */
|
|
93
105
|
metadata?: NostrConnectAppMetadata;
|
|
94
106
|
};
|
|
107
|
+
/** A nostrconnect:// URI */
|
|
108
|
+
export type NostrConnectURI = NostrConnectURIBase & ({
|
|
109
|
+
/** The secret used by the signer to connect to the client (the `secret` in a nostrconnect:// URI) */
|
|
110
|
+
connectSecret: string;
|
|
111
|
+
/** @deprecated Use connectSecret instead */
|
|
112
|
+
secret?: string;
|
|
113
|
+
} | {
|
|
114
|
+
/** @deprecated Use connectSecret instead */
|
|
115
|
+
secret: string;
|
|
116
|
+
/** The secret used by the signer to connect to the client (the `secret` in a nostrconnect:// URI) */
|
|
117
|
+
connectSecret?: string;
|
|
118
|
+
});
|
|
95
119
|
/** Parse a nostrconnect:// URI */
|
|
96
120
|
export declare function parseNostrConnectURI(uri: string): NostrConnectURI;
|
|
97
121
|
/** Create a nostrconnect:// URI from a {@link NostrConnectURI} object */
|
|
98
122
|
export declare function createNostrConnectURI(data: NostrConnectURI): string;
|
|
99
123
|
/** Build an array of signing permissions for event kinds */
|
|
100
124
|
export declare function buildSigningPermissions(kinds: number[]): string[];
|
|
125
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeNbunksec, decodeNbunksec } from "@sandwichfarm/encoded-entities";
|
|
1
2
|
import { setHiddenContentEncryptionMethod } from "applesauce-core/helpers/hidden-content";
|
|
2
3
|
import { isHexKey } from "applesauce-core/helpers/string";
|
|
3
4
|
import { kinds } from "applesauce-core/helpers/event";
|
|
@@ -29,6 +30,7 @@ export var NostrConnectMethod;
|
|
|
29
30
|
NostrConnectMethod["Nip44Encrypt"] = "nip44_encrypt";
|
|
30
31
|
NostrConnectMethod["Nip44Decrypt"] = "nip44_decrypt";
|
|
31
32
|
NostrConnectMethod["SwitchRelays"] = "switch_relays";
|
|
33
|
+
NostrConnectMethod["Logout"] = "logout";
|
|
32
34
|
})(NostrConnectMethod || (NostrConnectMethod = {}));
|
|
33
35
|
/** Parse a bunker:// URI */
|
|
34
36
|
export function parseBunkerURI(uri) {
|
|
@@ -40,26 +42,59 @@ export function parseBunkerURI(uri) {
|
|
|
40
42
|
const relays = url.searchParams.getAll("relay");
|
|
41
43
|
if (relays.length === 0)
|
|
42
44
|
throw new Error("Invalid bunker URI: missing relays");
|
|
43
|
-
const
|
|
44
|
-
return { remote, relays, secret };
|
|
45
|
+
const bunkerSecret = url.searchParams.get("secret") ?? undefined;
|
|
46
|
+
return { remote, relays, secret: bunkerSecret, bunkerSecret };
|
|
45
47
|
}
|
|
46
48
|
/** Creates a bunker:// URI from a {@link BunkerURI} object */
|
|
47
49
|
export function createBunkerURI(data) {
|
|
48
50
|
const url = new URL(`bunker://${data.remote}`);
|
|
49
51
|
data.relays.forEach((relay) => url.searchParams.append("relay", relay));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const bunkerSecret = data.bunkerSecret ?? data.secret;
|
|
53
|
+
if (bunkerSecret)
|
|
54
|
+
url.searchParams.set("secret", bunkerSecret);
|
|
52
55
|
return url.toString();
|
|
53
56
|
}
|
|
57
|
+
/** Parse an nbunksec encoded NIP-46 signer session */
|
|
58
|
+
export function parseNbunksec(encoded) {
|
|
59
|
+
const info = decodeNbunksec(encoded);
|
|
60
|
+
if (!isHexKey(info.pubkey))
|
|
61
|
+
throw new Error("Invalid nbunksec: remote is not a valid hex key");
|
|
62
|
+
if (!isHexKey(info.local_key))
|
|
63
|
+
throw new Error("Invalid nbunksec: clientKey is not a valid hex key");
|
|
64
|
+
if (info.relays.length === 0)
|
|
65
|
+
throw new Error("Invalid nbunksec: missing relays");
|
|
66
|
+
return {
|
|
67
|
+
remote: info.pubkey,
|
|
68
|
+
clientKey: info.local_key,
|
|
69
|
+
relays: info.relays,
|
|
70
|
+
secret: info.secret,
|
|
71
|
+
bunkerSecret: info.secret,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Create an nbunksec encoded NIP-46 signer session */
|
|
75
|
+
export function createNbunksec(data) {
|
|
76
|
+
if (!isHexKey(data.remote))
|
|
77
|
+
throw new Error("Invalid nbunksec: remote is not a valid hex key");
|
|
78
|
+
if (!isHexKey(data.clientKey))
|
|
79
|
+
throw new Error("Invalid nbunksec: clientKey is not a valid hex key");
|
|
80
|
+
if (data.relays.length === 0)
|
|
81
|
+
throw new Error("Invalid nbunksec: missing relays");
|
|
82
|
+
return encodeNbunksec({
|
|
83
|
+
pubkey: data.remote,
|
|
84
|
+
local_key: data.clientKey,
|
|
85
|
+
relays: data.relays,
|
|
86
|
+
secret: data.bunkerSecret ?? data.secret,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
54
89
|
/** Parse a nostrconnect:// URI */
|
|
55
90
|
export function parseNostrConnectURI(uri) {
|
|
56
91
|
const url = new URL(uri);
|
|
57
92
|
const client = url.host || url.pathname.replace("//", "");
|
|
58
93
|
if (!isHexKey(client))
|
|
59
94
|
throw new Error("Invalid nostrconnect URI: client is not a valid hex key");
|
|
60
|
-
const
|
|
95
|
+
const connectSecret = url.searchParams.get("secret");
|
|
61
96
|
const relays = url.searchParams.getAll("relay");
|
|
62
|
-
if (!
|
|
97
|
+
if (!connectSecret)
|
|
63
98
|
throw new Error("Invalid nostrconnect URI: missing secret");
|
|
64
99
|
if (relays.length === 0)
|
|
65
100
|
throw new Error("Invalid nostrconnect URI: missing relays");
|
|
@@ -71,14 +106,17 @@ export function parseNostrConnectURI(uri) {
|
|
|
71
106
|
};
|
|
72
107
|
/** Omit metadata if all values are undefined */
|
|
73
108
|
if (Object.values(metadata).every((v) => v === undefined))
|
|
74
|
-
return { client, secret, relays };
|
|
109
|
+
return { client, secret: connectSecret, connectSecret, relays };
|
|
75
110
|
else
|
|
76
|
-
return { client, secret, relays, metadata };
|
|
111
|
+
return { client, secret: connectSecret, connectSecret, relays, metadata };
|
|
77
112
|
}
|
|
78
113
|
/** Create a nostrconnect:// URI from a {@link NostrConnectURI} object */
|
|
79
114
|
export function createNostrConnectURI(data) {
|
|
80
115
|
const params = new URLSearchParams();
|
|
81
|
-
|
|
116
|
+
const connectSecret = data.connectSecret ?? data.secret;
|
|
117
|
+
if (!connectSecret)
|
|
118
|
+
throw new Error("Invalid nostrconnect URI: missing secret");
|
|
119
|
+
params.set("secret", connectSecret);
|
|
82
120
|
if (data.metadata?.name)
|
|
83
121
|
params.set("name", data.metadata.name);
|
|
84
122
|
if (data.metadata?.url)
|
|
@@ -2,6 +2,7 @@ import { EventTemplate, NostrEvent } from "applesauce-core/helpers/event";
|
|
|
2
2
|
import { Subscription } from "rxjs";
|
|
3
3
|
import { ConnectRequestParams, ConnectResponseResults, NostrConnectMethod, NostrConnectResponse, NostrConnectURI } from "../helpers/nostr-connect.js";
|
|
4
4
|
import { ISigner, NostrConnectionMethodsOptions, NostrPool, NostrPublishMethod, NostrSubscriptionMethod } from "../interop.js";
|
|
5
|
+
import { PrivateKeySigner } from "./private-key-signer.js";
|
|
5
6
|
export interface ProviderAuthorization {
|
|
6
7
|
/** A method used to accept or reject `connect` requests */
|
|
7
8
|
onConnect?: (client: string, permissions: string[]) => boolean | Promise<boolean>;
|
|
@@ -17,6 +18,8 @@ export interface ProviderAuthorization {
|
|
|
17
18
|
onNip44Decrypt?: (pubkey: string, ciphertext: string, client: string) => boolean | Promise<boolean>;
|
|
18
19
|
/** A method called when a client requests relay switching. Return new relay list or null if no change */
|
|
19
20
|
onSwitchRelays?: (client: string) => string[] | null | Promise<string[] | null>;
|
|
21
|
+
/** A method called when a client requests to logout (end the session) */
|
|
22
|
+
onLogout?: (client: string) => void | Promise<void>;
|
|
20
23
|
}
|
|
21
24
|
export type NostrConnectProviderOptions = ProviderAuthorization & NostrConnectionMethodsOptions & {
|
|
22
25
|
/** The relays to communicate over */
|
|
@@ -25,8 +28,10 @@ export type NostrConnectProviderOptions = ProviderAuthorization & NostrConnectio
|
|
|
25
28
|
upstream: ISigner;
|
|
26
29
|
/** Optional signer for provider identity */
|
|
27
30
|
signer?: ISigner;
|
|
28
|
-
/**
|
|
31
|
+
/** @deprecated Use bunkerSecret instead */
|
|
29
32
|
secret?: string;
|
|
33
|
+
/** A random secret used to authorize clients to connect (the `secret` in a bunker:// URI) */
|
|
34
|
+
bunkerSecret?: string;
|
|
30
35
|
/** Callback for when a client connects (receives a `connect` request) */
|
|
31
36
|
onClientConnect?: (client: string) => any;
|
|
32
37
|
/** Callback for when a client disconnects (previously connected and the provider stops) */
|
|
@@ -55,8 +60,11 @@ export declare class NostrConnectProvider implements ProviderAuthorization {
|
|
|
55
60
|
listening: boolean;
|
|
56
61
|
/** The connected client's public key */
|
|
57
62
|
client?: string;
|
|
58
|
-
/** The secret used to authorize clients to connect */
|
|
59
|
-
|
|
63
|
+
/** The secret used to authorize clients to connect (the `secret` in a bunker:// URI) */
|
|
64
|
+
bunkerSecret?: string;
|
|
65
|
+
/** @deprecated Use bunkerSecret instead */
|
|
66
|
+
get secret(): string | undefined;
|
|
67
|
+
set secret(value: string | undefined);
|
|
60
68
|
/** Relays to communicate over */
|
|
61
69
|
readonly relays: string[];
|
|
62
70
|
/** Whether a client is connected (received a `connect` request) */
|
|
@@ -78,6 +86,8 @@ export declare class NostrConnectProvider implements ProviderAuthorization {
|
|
|
78
86
|
onNip44Decrypt?: (pubkey: string, ciphertext: string, client: string) => boolean | Promise<boolean>;
|
|
79
87
|
/** A method called when a client requests relay switching. Return new relay list or null if no change */
|
|
80
88
|
onSwitchRelays?: (client: string) => string[] | null | Promise<string[] | null>;
|
|
89
|
+
/** A method called when a client requests to logout (end the session) */
|
|
90
|
+
onLogout?: (client: string) => void | Promise<void>;
|
|
81
91
|
constructor(options: NostrConnectProviderOptions);
|
|
82
92
|
/** The currently active REQ subscription */
|
|
83
93
|
protected req?: Subscription;
|
|
@@ -98,7 +108,7 @@ export declare class NostrConnectProvider implements ProviderAuthorization {
|
|
|
98
108
|
/** Handle an initial NostrConnectURI */
|
|
99
109
|
handleNostrConnectURI(uri: NostrConnectURI | string): Promise<void>;
|
|
100
110
|
/** Handle connect request */
|
|
101
|
-
protected handleConnect(client: string, [target,
|
|
111
|
+
protected handleConnect(client: string, [target, bunkerSecret, permissionsStr]: ConnectRequestParams[NostrConnectMethod.Connect]): Promise<ConnectResponseResults[NostrConnectMethod.Connect]>;
|
|
102
112
|
/** Handle sign event request */
|
|
103
113
|
protected handleSignEvent([eventJson]: ConnectRequestParams[NostrConnectMethod.SignEvent]): Promise<ConnectResponseResults[NostrConnectMethod.SignEvent]>;
|
|
104
114
|
/** Handle NIP-04 encryption */
|
|
@@ -111,6 +121,8 @@ export declare class NostrConnectProvider implements ProviderAuthorization {
|
|
|
111
121
|
protected handleNip44Decrypt([pubkey, ciphertext,]: ConnectRequestParams[NostrConnectMethod.Nip44Decrypt]): Promise<ConnectResponseResults[NostrConnectMethod.Nip44Decrypt]>;
|
|
112
122
|
/** Handle switch relays request */
|
|
113
123
|
protected handleSwitchRelays(): Promise<ConnectResponseResults[NostrConnectMethod.SwitchRelays]>;
|
|
124
|
+
/** Handle a logout request by ending the session associated with the client */
|
|
125
|
+
protected handleLogout(client: string): Promise<void>;
|
|
114
126
|
/**
|
|
115
127
|
* Send a response to the client
|
|
116
128
|
* @param clientOrRequest - The client pubkey or request event
|
|
@@ -124,4 +136,6 @@ export declare class NostrConnectProvider implements ProviderAuthorization {
|
|
|
124
136
|
protected sendMessage(clientOrRequest: string | NostrEvent, message: NostrConnectResponse<any>): Promise<void>;
|
|
125
137
|
/** Get the connection string that clients can use to connect */
|
|
126
138
|
getBunkerURI(): Promise<string>;
|
|
139
|
+
/** Get an nbunksec encoded session that clients can import */
|
|
140
|
+
getNbunksec(clientSigner?: PrivateKeySigner): Promise<string>;
|
|
127
141
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
2
|
import { getEncryptedContentEncryptionMethods, getHiddenContent, isEvent, unixNow, unlockHiddenContent, } from "applesauce-core/helpers";
|
|
3
|
-
import { kinds, verifyEvent } from "applesauce-core/helpers/event";
|
|
3
|
+
import { bytesToHex, kinds, verifyEvent } from "applesauce-core/helpers/event";
|
|
4
4
|
import { createDefer } from "applesauce-core/promise";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
6
|
import { filter, from, repeat, retry } from "rxjs";
|
|
7
7
|
import { isNIP04 } from "../helpers/encryption.js";
|
|
8
|
-
import { createBunkerURI, NostrConnectMethod, parseNostrConnectURI, } from "../helpers/nostr-connect.js";
|
|
8
|
+
import { createBunkerURI, createNbunksec, NostrConnectMethod, parseNostrConnectURI, } from "../helpers/nostr-connect.js";
|
|
9
9
|
import { getConnectionMethods, } from "../interop.js";
|
|
10
10
|
import { PrivateKeySigner } from "./private-key-signer.js";
|
|
11
11
|
export class NostrConnectProvider {
|
|
@@ -31,8 +31,15 @@ export class NostrConnectProvider {
|
|
|
31
31
|
listening = false;
|
|
32
32
|
/** The connected client's public key */
|
|
33
33
|
client;
|
|
34
|
-
/** The secret used to authorize clients to connect */
|
|
35
|
-
|
|
34
|
+
/** The secret used to authorize clients to connect (the `secret` in a bunker:// URI) */
|
|
35
|
+
bunkerSecret;
|
|
36
|
+
/** @deprecated Use bunkerSecret instead */
|
|
37
|
+
get secret() {
|
|
38
|
+
return this.bunkerSecret;
|
|
39
|
+
}
|
|
40
|
+
set secret(value) {
|
|
41
|
+
this.bunkerSecret = value;
|
|
42
|
+
}
|
|
36
43
|
/** Relays to communicate over */
|
|
37
44
|
relays;
|
|
38
45
|
/** Whether a client is connected (received a `connect` request) */
|
|
@@ -54,11 +61,13 @@ export class NostrConnectProvider {
|
|
|
54
61
|
onNip44Decrypt;
|
|
55
62
|
/** A method called when a client requests relay switching. Return new relay list or null if no change */
|
|
56
63
|
onSwitchRelays;
|
|
64
|
+
/** A method called when a client requests to logout (end the session) */
|
|
65
|
+
onLogout;
|
|
57
66
|
constructor(options) {
|
|
58
67
|
this.relays = options.relays;
|
|
59
68
|
this.upstream = options.upstream;
|
|
60
69
|
this.signer = options.signer ?? new PrivateKeySigner();
|
|
61
|
-
this.
|
|
70
|
+
this.bunkerSecret = options.bunkerSecret ?? options.secret;
|
|
62
71
|
// Get the subscription and publish methods
|
|
63
72
|
const { subscriptionMethod, publishMethod } = getConnectionMethods(options, NostrConnectProvider);
|
|
64
73
|
// Use arrow functions so "this" isn't bound to the signer
|
|
@@ -84,6 +93,8 @@ export class NostrConnectProvider {
|
|
|
84
93
|
this.onNip44Decrypt = options.onNip44Decrypt;
|
|
85
94
|
if (options.onSwitchRelays)
|
|
86
95
|
this.onSwitchRelays = options.onSwitchRelays;
|
|
96
|
+
if (options.onLogout)
|
|
97
|
+
this.onLogout = options.onLogout;
|
|
87
98
|
}
|
|
88
99
|
/** The currently active REQ subscription */
|
|
89
100
|
req;
|
|
@@ -223,11 +234,17 @@ export class NostrConnectProvider {
|
|
|
223
234
|
case NostrConnectMethod.SwitchRelays:
|
|
224
235
|
result = await this.handleSwitchRelays();
|
|
225
236
|
break;
|
|
237
|
+
case NostrConnectMethod.Logout:
|
|
238
|
+
result = "ack";
|
|
239
|
+
break;
|
|
226
240
|
default:
|
|
227
241
|
throw new Error(`Unsupported method: ${request.method}`);
|
|
228
242
|
}
|
|
229
243
|
// Send success response
|
|
230
244
|
await this.sendResponse(event, request.id, result);
|
|
245
|
+
// Ack is sent before ending the session, so the client receives confirmation before we tear down
|
|
246
|
+
if (request.method === NostrConnectMethod.Logout)
|
|
247
|
+
await this.handleLogout(event.pubkey);
|
|
231
248
|
}
|
|
232
249
|
catch (error) {
|
|
233
250
|
this.log(`Error processing ${request.method}:`, error);
|
|
@@ -248,14 +265,14 @@ export class NostrConnectProvider {
|
|
|
248
265
|
// Get a response to a fake initial `connect` request
|
|
249
266
|
const response = await this.handleConnect(uri.client, [
|
|
250
267
|
await this.signer.getPublicKey(),
|
|
251
|
-
uri.secret,
|
|
268
|
+
uri.connectSecret ?? uri.secret ?? "",
|
|
252
269
|
uri.metadata?.permissions?.join(",") ?? "",
|
|
253
270
|
]);
|
|
254
271
|
// Send `connect` response with random id
|
|
255
272
|
await this.sendResponse(uri.client, nanoid(8), response);
|
|
256
273
|
}
|
|
257
274
|
/** Handle connect request */
|
|
258
|
-
async handleConnect(client, [target,
|
|
275
|
+
async handleConnect(client, [target, bunkerSecret, permissionsStr]) {
|
|
259
276
|
const permissions = permissionsStr ? permissionsStr.split(",") : [];
|
|
260
277
|
// Check if this is a connection to our provider
|
|
261
278
|
const providerPubkey = await this.signer.getPublicKey();
|
|
@@ -265,7 +282,7 @@ export class NostrConnectProvider {
|
|
|
265
282
|
if (this.client && this.client !== client)
|
|
266
283
|
throw new Error("Only one client can connect at a time");
|
|
267
284
|
// If this is the first `connect` request, check that the secret matches
|
|
268
|
-
if (this.
|
|
285
|
+
if (this.bunkerSecret && !this.client && this.bunkerSecret !== bunkerSecret)
|
|
269
286
|
throw new Error("Invalid connection secret");
|
|
270
287
|
// Handle authorization if callback is provided
|
|
271
288
|
if (this.onConnect) {
|
|
@@ -278,8 +295,8 @@ export class NostrConnectProvider {
|
|
|
278
295
|
// Establish connection
|
|
279
296
|
this.client = client;
|
|
280
297
|
this.connected = true;
|
|
281
|
-
if (!this.
|
|
282
|
-
this.
|
|
298
|
+
if (!this.bunkerSecret)
|
|
299
|
+
this.bunkerSecret = bunkerSecret;
|
|
283
300
|
// Update the subscription since we now know the client pubkey
|
|
284
301
|
if (isFirstRequest)
|
|
285
302
|
await this.updateSubscription();
|
|
@@ -292,7 +309,7 @@ export class NostrConnectProvider {
|
|
|
292
309
|
this.waitingPromise = null;
|
|
293
310
|
}
|
|
294
311
|
// Return ack or the secret if provided
|
|
295
|
-
return
|
|
312
|
+
return bunkerSecret || "ack";
|
|
296
313
|
}
|
|
297
314
|
/** Handle sign event request */
|
|
298
315
|
async handleSignEvent([eventJson]) {
|
|
@@ -363,6 +380,23 @@ export class NostrConnectProvider {
|
|
|
363
380
|
// Default: return current relays (no change)
|
|
364
381
|
return null;
|
|
365
382
|
}
|
|
383
|
+
/** Handle a logout request by ending the session associated with the client */
|
|
384
|
+
async handleLogout(client) {
|
|
385
|
+
this.log(`Client ${client} logged out`);
|
|
386
|
+
// Notify the logout callback
|
|
387
|
+
if (this.onLogout)
|
|
388
|
+
await this.onLogout(client);
|
|
389
|
+
// Notify client disconnect
|
|
390
|
+
if (this.connected && this.onClientDisconnect)
|
|
391
|
+
this.onClientDisconnect(client);
|
|
392
|
+
// Forget the client so a new `connect` is required to re-establish a session.
|
|
393
|
+
// NOTE: do not clear `seen` here, otherwise the relay will replay the old `connect`/`logout`
|
|
394
|
+
// events on the new subscription and the provider would reconnect then logout in a loop.
|
|
395
|
+
this.client = undefined;
|
|
396
|
+
this.connected = false;
|
|
397
|
+
// Reset the subscription to listen for new connections
|
|
398
|
+
await this.updateSubscription();
|
|
399
|
+
}
|
|
366
400
|
/**
|
|
367
401
|
* Send a response to the client
|
|
368
402
|
* @param clientOrRequest - The client pubkey or request event
|
|
@@ -419,7 +453,16 @@ export class NostrConnectProvider {
|
|
|
419
453
|
return createBunkerURI({
|
|
420
454
|
remote: await this.signer.getPublicKey(),
|
|
421
455
|
relays: this.relays,
|
|
422
|
-
|
|
456
|
+
bunkerSecret: this.bunkerSecret,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/** Get an nbunksec encoded session that clients can import */
|
|
460
|
+
async getNbunksec(clientSigner = new PrivateKeySigner()) {
|
|
461
|
+
return createNbunksec({
|
|
462
|
+
remote: await this.signer.getPublicKey(),
|
|
463
|
+
clientKey: bytesToHex(clientSigner.key),
|
|
464
|
+
relays: this.relays,
|
|
465
|
+
bunkerSecret: this.bunkerSecret,
|
|
423
466
|
});
|
|
424
467
|
}
|
|
425
468
|
}
|
|
@@ -2,7 +2,7 @@ import { EventTemplate, NostrEvent, verifyEvent } from "applesauce-core/helpers/
|
|
|
2
2
|
import { Deferred } from "applesauce-core/promise";
|
|
3
3
|
import { ISigner, NostrConnectionMethodsOptions, NostrPool, NostrPublishMethod, NostrSubscriptionMethod, PrivateKeySigner } from "applesauce-signers";
|
|
4
4
|
import { Subscription } from "rxjs";
|
|
5
|
-
import { BunkerURI, NostrConnectAppMetadata } from "../helpers/nostr-connect.js";
|
|
5
|
+
import { BunkerURI, NostrConnectAppMetadata, Nbunksec } from "../helpers/nostr-connect.js";
|
|
6
6
|
export type NostrConnectSignerOptions = NostrConnectionMethodsOptions & {
|
|
7
7
|
/** The relays to communicate over */
|
|
8
8
|
relays: string[];
|
|
@@ -12,8 +12,12 @@ export type NostrConnectSignerOptions = NostrConnectionMethodsOptions & {
|
|
|
12
12
|
remote?: string;
|
|
13
13
|
/** Users pubkey */
|
|
14
14
|
pubkey?: string;
|
|
15
|
-
/**
|
|
15
|
+
/** @deprecated Use connectSecret instead */
|
|
16
16
|
secret?: string;
|
|
17
|
+
/** A secret used when initiating the connection from the client side (the `secret` in a nostrconnect:// URI) */
|
|
18
|
+
connectSecret?: string;
|
|
19
|
+
/** A secret used when connecting to a remote bunker (the `secret` in a bunker:// URI) */
|
|
20
|
+
bunkerSecret?: string;
|
|
17
21
|
/** A method for handling "auth" requests */
|
|
18
22
|
onAuth?: (url: string) => Promise<void>;
|
|
19
23
|
};
|
|
@@ -46,8 +50,13 @@ export declare class NostrConnectSigner implements ISigner {
|
|
|
46
50
|
/** A method for handling "auth" requests */
|
|
47
51
|
onAuth: (url: string) => Promise<void>;
|
|
48
52
|
verifyEvent: typeof verifyEvent;
|
|
49
|
-
/** A secret used when initiating a connection from the client side */
|
|
50
|
-
|
|
53
|
+
/** A secret used when initiating a connection from the client side (the `secret` in a nostrconnect:// URI) */
|
|
54
|
+
connectSecret: string;
|
|
55
|
+
/** A secret used when connecting to a remote bunker (the `secret` in a bunker:// URI) */
|
|
56
|
+
bunkerSecret?: string;
|
|
57
|
+
/** @deprecated Use connectSecret instead */
|
|
58
|
+
get secret(): string;
|
|
59
|
+
set secret(value: string);
|
|
51
60
|
nip04?: {
|
|
52
61
|
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
|
|
53
62
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
|
|
@@ -69,8 +78,12 @@ export declare class NostrConnectSigner implements ISigner {
|
|
|
69
78
|
handleEvent(event: NostrEvent): Promise<void>;
|
|
70
79
|
protected createRequestEvent(content: string, target?: string | undefined, kind?: number): Promise<import("nostr-tools/core").VerifiedEvent>;
|
|
71
80
|
private makeRequest;
|
|
72
|
-
/**
|
|
73
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Connect to a remote signer
|
|
83
|
+
* @param bunkerSecret - The `secret` from a `bunker://` URI used to authorize this client with the remote signer
|
|
84
|
+
* @param permissions - The signing permissions to request (see {@link NostrConnectSigner.buildSigningPermissions})
|
|
85
|
+
*/
|
|
86
|
+
connect(bunkerSecret?: string | undefined, permissions?: string[]): Promise<string>;
|
|
74
87
|
private waitingPromise;
|
|
75
88
|
/** Wait for a remote signer to connect */
|
|
76
89
|
waitForSigner(abort?: AbortSignal): Promise<void>;
|
|
@@ -95,10 +108,24 @@ export declare class NostrConnectSigner implements ISigner {
|
|
|
95
108
|
* @returns An array of relay URLs if the signer wants to switch relays, or null if no change
|
|
96
109
|
*/
|
|
97
110
|
switchRelays(): Promise<string[] | null>;
|
|
111
|
+
/**
|
|
112
|
+
* Notify the remote signer that the session is ending and close the connection
|
|
113
|
+
*
|
|
114
|
+
* The `logout` request is a courtesy hint, so the local connection is always closed even if the
|
|
115
|
+
* remote signer does not acknowledge it. The consumer is responsible for deleting any persisted
|
|
116
|
+
* client keypair.
|
|
117
|
+
*/
|
|
118
|
+
logout(): Promise<void>;
|
|
98
119
|
/** Returns the nostrconnect:// URI for this signer */
|
|
99
120
|
getNostrConnectURI(metadata?: NostrConnectAppMetadata): string;
|
|
121
|
+
/** Returns the nbunksec encoded session for this signer */
|
|
122
|
+
getNbunksec(): string;
|
|
100
123
|
/** Parses a bunker:// URI */
|
|
101
124
|
static parseBunkerURI(uri: string): BunkerURI;
|
|
125
|
+
/** Parses an nbunksec encoded session */
|
|
126
|
+
static parseNbunksec(encoded: string): Nbunksec;
|
|
127
|
+
/** Creates an nbunksec encoded session */
|
|
128
|
+
static createNbunksec(data: Nbunksec): string;
|
|
102
129
|
/** Builds an array of signing permissions for event kinds */
|
|
103
130
|
static buildSigningPermissions(kinds: number[]): string[];
|
|
104
131
|
/** Create a {@link NostrConnectSigner} from a bunker:// URI */
|
|
@@ -106,4 +133,8 @@ export declare class NostrConnectSigner implements ISigner {
|
|
|
106
133
|
permissions?: string[];
|
|
107
134
|
signer?: PrivateKeySigner;
|
|
108
135
|
}): Promise<NostrConnectSigner>;
|
|
136
|
+
/** Create a {@link NostrConnectSigner} from an nbunksec encoded session */
|
|
137
|
+
static fromNbunksec(encoded: string, options?: Omit<NostrConnectSignerOptions, "relays" | "remote" | "signer" | "bunkerSecret"> & {
|
|
138
|
+
permissions?: string[];
|
|
139
|
+
}): Promise<NostrConnectSigner>;
|
|
109
140
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
2
|
import { getHiddenContent, isHexKey, unixNow } from "applesauce-core/helpers";
|
|
3
|
-
import { kinds, verifyEvent } from "applesauce-core/helpers/event";
|
|
3
|
+
import { bytesToHex, hexToBytes, kinds, verifyEvent } from "applesauce-core/helpers/event";
|
|
4
4
|
import { getPublicKey } from "applesauce-core/helpers/keys";
|
|
5
5
|
import { createDefer } from "applesauce-core/promise";
|
|
6
6
|
import { PrivateKeySigner, getConnectionMethods, } from "applesauce-signers";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { filter, from, repeat, retry } from "rxjs";
|
|
9
9
|
import { isNIP04 } from "../helpers/encryption.js";
|
|
10
|
-
import { NostrConnectMethod, buildSigningPermissions, createNostrConnectURI, parseBunkerURI, } from "../helpers/nostr-connect.js";
|
|
10
|
+
import { createNbunksec, NostrConnectMethod, buildSigningPermissions, createNostrConnectURI, parseBunkerURI, parseNbunksec, } from "../helpers/nostr-connect.js";
|
|
11
11
|
async function defaultHandleAuth(url) {
|
|
12
12
|
window.open(url, "auth", "width=400,height=600,resizable=no,status=no,location=no,toolbar=no,menubar=no");
|
|
13
13
|
}
|
|
@@ -42,15 +42,25 @@ export class NostrConnectSigner {
|
|
|
42
42
|
/** A method for handling "auth" requests */
|
|
43
43
|
onAuth = defaultHandleAuth;
|
|
44
44
|
verifyEvent = verifyEvent;
|
|
45
|
-
/** A secret used when initiating a connection from the client side */
|
|
46
|
-
|
|
45
|
+
/** A secret used when initiating a connection from the client side (the `secret` in a nostrconnect:// URI) */
|
|
46
|
+
connectSecret;
|
|
47
|
+
/** A secret used when connecting to a remote bunker (the `secret` in a bunker:// URI) */
|
|
48
|
+
bunkerSecret;
|
|
49
|
+
/** @deprecated Use connectSecret instead */
|
|
50
|
+
get secret() {
|
|
51
|
+
return this.connectSecret;
|
|
52
|
+
}
|
|
53
|
+
set secret(value) {
|
|
54
|
+
this.connectSecret = value;
|
|
55
|
+
}
|
|
47
56
|
nip04;
|
|
48
57
|
nip44;
|
|
49
58
|
constructor(options) {
|
|
50
59
|
this.relays = options.relays;
|
|
51
60
|
this.pubkey = options.pubkey;
|
|
52
61
|
this.remote = options.remote;
|
|
53
|
-
this.
|
|
62
|
+
this.connectSecret = options.connectSecret || options.secret || nanoid(12);
|
|
63
|
+
this.bunkerSecret = options.bunkerSecret;
|
|
54
64
|
// Get the subscription and publish methods
|
|
55
65
|
const { subscriptionMethod, publishMethod } = getConnectionMethods(options, NostrConnectSigner);
|
|
56
66
|
// Use arrow functions so "this" isn't bound to the signer
|
|
@@ -128,7 +138,8 @@ export class NostrConnectSigner {
|
|
|
128
138
|
return;
|
|
129
139
|
const response = JSON.parse(responseStr);
|
|
130
140
|
// handle remote signer connection
|
|
131
|
-
if (!this.remote &&
|
|
141
|
+
if (!this.remote &&
|
|
142
|
+
(response.result === "ack" || (this.connectSecret && response.result === this.connectSecret))) {
|
|
132
143
|
this.log("Got ack response from", event.pubkey, response.result);
|
|
133
144
|
this.isConnected = true;
|
|
134
145
|
this.remote = event.pubkey;
|
|
@@ -196,8 +207,12 @@ export class NostrConnectSigner {
|
|
|
196
207
|
this.log(`Sent ${id} (${method})`);
|
|
197
208
|
return p;
|
|
198
209
|
}
|
|
199
|
-
/**
|
|
200
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Connect to a remote signer
|
|
212
|
+
* @param bunkerSecret - The `secret` from a `bunker://` URI used to authorize this client with the remote signer
|
|
213
|
+
* @param permissions - The signing permissions to request (see {@link NostrConnectSigner.buildSigningPermissions})
|
|
214
|
+
*/
|
|
215
|
+
async connect(bunkerSecret, permissions) {
|
|
201
216
|
// Attempt to connect to the users pubkey if remote note set
|
|
202
217
|
if (!this.remote && this.pubkey)
|
|
203
218
|
this.remote = this.pubkey;
|
|
@@ -205,9 +220,11 @@ export class NostrConnectSigner {
|
|
|
205
220
|
throw new Error("Missing remote signer pubkey");
|
|
206
221
|
await this.open();
|
|
207
222
|
try {
|
|
223
|
+
if (bunkerSecret !== undefined)
|
|
224
|
+
this.bunkerSecret = bunkerSecret;
|
|
208
225
|
const result = await this.makeRequest(NostrConnectMethod.Connect, [
|
|
209
226
|
this.remote,
|
|
210
|
-
|
|
227
|
+
this.bunkerSecret || "",
|
|
211
228
|
permissions?.join(",") ?? "",
|
|
212
229
|
]);
|
|
213
230
|
this.isConnected = true;
|
|
@@ -330,28 +347,80 @@ export class NostrConnectSigner {
|
|
|
330
347
|
}
|
|
331
348
|
return result;
|
|
332
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Notify the remote signer that the session is ending and close the connection
|
|
352
|
+
*
|
|
353
|
+
* The `logout` request is a courtesy hint, so the local connection is always closed even if the
|
|
354
|
+
* remote signer does not acknowledge it. The consumer is responsible for deleting any persisted
|
|
355
|
+
* client keypair.
|
|
356
|
+
*/
|
|
357
|
+
async logout() {
|
|
358
|
+
try {
|
|
359
|
+
if (this.isConnected && this.remote)
|
|
360
|
+
await this.makeRequest(NostrConnectMethod.Logout, []);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
this.log("Failed to send logout request", error);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
this.isConnected = false;
|
|
367
|
+
await this.close();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
333
370
|
/** Returns the nostrconnect:// URI for this signer */
|
|
334
371
|
getNostrConnectURI(metadata) {
|
|
335
372
|
return createNostrConnectURI({
|
|
336
373
|
client: getPublicKey(this.signer.key),
|
|
337
|
-
|
|
374
|
+
connectSecret: this.connectSecret,
|
|
338
375
|
relays: this.relays,
|
|
339
376
|
metadata,
|
|
340
377
|
});
|
|
341
378
|
}
|
|
379
|
+
/** Returns the nbunksec encoded session for this signer */
|
|
380
|
+
getNbunksec() {
|
|
381
|
+
if (!this.remote)
|
|
382
|
+
throw new Error("Cant create nbunksec when remote signer is not set");
|
|
383
|
+
return createNbunksec({
|
|
384
|
+
remote: this.remote,
|
|
385
|
+
clientKey: bytesToHex(this.signer.key),
|
|
386
|
+
relays: this.relays,
|
|
387
|
+
bunkerSecret: this.bunkerSecret,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
342
390
|
/** Parses a bunker:// URI */
|
|
343
391
|
static parseBunkerURI(uri) {
|
|
344
392
|
return parseBunkerURI(uri);
|
|
345
393
|
}
|
|
394
|
+
/** Parses an nbunksec encoded session */
|
|
395
|
+
static parseNbunksec(encoded) {
|
|
396
|
+
return parseNbunksec(encoded);
|
|
397
|
+
}
|
|
398
|
+
/** Creates an nbunksec encoded session */
|
|
399
|
+
static createNbunksec(data) {
|
|
400
|
+
return createNbunksec(data);
|
|
401
|
+
}
|
|
346
402
|
/** Builds an array of signing permissions for event kinds */
|
|
347
403
|
static buildSigningPermissions(kinds) {
|
|
348
404
|
return buildSigningPermissions(kinds);
|
|
349
405
|
}
|
|
350
406
|
/** Create a {@link NostrConnectSigner} from a bunker:// URI */
|
|
351
407
|
static async fromBunkerURI(uri, options) {
|
|
352
|
-
const { remote, relays,
|
|
353
|
-
const client = new NostrConnectSigner({ relays, remote, ...options });
|
|
354
|
-
await client.connect(
|
|
408
|
+
const { remote, relays, bunkerSecret } = NostrConnectSigner.parseBunkerURI(uri);
|
|
409
|
+
const client = new NostrConnectSigner({ relays, remote, bunkerSecret, ...options });
|
|
410
|
+
await client.connect(bunkerSecret, options?.permissions);
|
|
411
|
+
return client;
|
|
412
|
+
}
|
|
413
|
+
/** Create a {@link NostrConnectSigner} from an nbunksec encoded session */
|
|
414
|
+
static async fromNbunksec(encoded, options) {
|
|
415
|
+
const { remote, clientKey, relays, bunkerSecret } = NostrConnectSigner.parseNbunksec(encoded);
|
|
416
|
+
const client = new NostrConnectSigner({
|
|
417
|
+
relays,
|
|
418
|
+
remote,
|
|
419
|
+
signer: new PrivateKeySigner(hexToBytes(clientKey)),
|
|
420
|
+
bunkerSecret,
|
|
421
|
+
...options,
|
|
422
|
+
});
|
|
423
|
+
await client.connect(bunkerSecret, options?.permissions);
|
|
355
424
|
return client;
|
|
356
425
|
}
|
|
357
426
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-signers",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Signer classes for applesauce",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@noble/secp256k1": "^1.
|
|
48
|
-
"
|
|
47
|
+
"@noble/secp256k1": "^3.1.0",
|
|
48
|
+
"@sandwichfarm/encoded-entities": "^0.1.1",
|
|
49
|
+
"applesauce-core": "^6.2.0",
|
|
49
50
|
"debug": "^4.4.0",
|
|
50
51
|
"nanoid": "^5.0.9",
|
|
51
52
|
"rxjs": "^7.8.2"
|