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 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
- /** A nostrconnect:// URI */
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 secret = url.searchParams.get("secret") ?? undefined;
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
- if (data.secret)
51
- url.searchParams.set("secret", data.secret);
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 secret = url.searchParams.get("secret");
95
+ const connectSecret = url.searchParams.get("secret");
61
96
  const relays = url.searchParams.getAll("relay");
62
- if (!secret)
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
- params.set("secret", data.secret);
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
- /** A random secret used to authorize clients to connect */
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
- secret?: string;
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, secret, permissionsStr]: ConnectRequestParams[NostrConnectMethod.Connect]): Promise<ConnectResponseResults[NostrConnectMethod.Connect]>;
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
- secret;
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.secret = options.secret;
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, secret, permissionsStr]) {
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.secret && !this.client && this.secret !== secret)
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.secret)
282
- this.secret = secret;
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 secret || "ack";
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
- secret: this.secret,
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
- /** A secret used when initalizing the connection from the client side */
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
- secret: string;
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
- /** Connect to remote signer */
73
- connect(secret?: string | undefined, permissions?: string[]): Promise<string>;
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
- secret;
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.secret = options.secret || nanoid(12);
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 && (response.result === "ack" || (this.secret && response.result === this.secret))) {
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
- /** Connect to remote signer */
200
- async connect(secret, permissions) {
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
- secret || "",
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
- secret: this.secret,
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, secret } = NostrConnectSigner.parseBunkerURI(uri);
353
- const client = new NostrConnectSigner({ relays, remote, ...options });
354
- await client.connect(secret, options?.permissions);
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.1",
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.7.1",
48
- "applesauce-core": "^6.0.0",
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"