divine-signer 0.3.2 → 0.4.1

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.
@@ -1,6 +1,12 @@
1
1
  import { type BunkerSignerParams } from 'nostr-tools/nip46';
2
2
  import type { EventTemplate, VerifiedEvent } from 'nostr-tools/pure';
3
3
  import type { NostrSigner, SignerType } from './types';
4
+ export interface NostrConnectHandle {
5
+ /** Call after showing the QR code. Resolves when the remote signer acks. */
6
+ waitForSigner(): Promise<BunkerNIP44Signer>;
7
+ /** Abort the connection attempt. */
8
+ abort(): void;
9
+ }
4
10
  export declare class BunkerNIP44Signer implements NostrSigner {
5
11
  readonly type: SignerType;
6
12
  private readonly inner;
@@ -14,14 +20,20 @@ export declare class BunkerNIP44Signer implements NostrSigner {
14
20
  * and calls getPublicKey() to verify the channel is alive (and prime the cache).
15
21
  */
16
22
  static reconnect(clientSecretKey: Uint8Array, bunkerUrl: string, params?: BunkerSignerParams, connectTimeout?: number): Promise<BunkerNIP44Signer>;
23
+ /**
24
+ * Two-phase nostrconnect: sets up relay connections and subscription,
25
+ * then returns a handle. Show the QR code, then call handle.waitForSigner().
26
+ *
27
+ * This avoids a race where the user scans before the subscription is live.
28
+ */
29
+ static prepareNostrConnect(connectionURI: string, clientSecretKey: Uint8Array, params?: BunkerSignerParams, timeoutOrAbort?: number | AbortSignal): Promise<NostrConnectHandle>;
17
30
  /**
18
31
  * Connect via a nostrconnect:// URI (QR code flow).
19
32
  *
20
- * Implements the connect handshake manually instead of using
21
- * BunkerSigner.fromURI, which sends a `switch_relays` RPC that
22
- * signers like Primal don't understand (causing a parse error and
23
- * potentially disrupting the session). After the handshake we create
24
- * the signer via BunkerSigner.fromBunker which skips switch_relays.
33
+ * Convenience wrapper around prepareNostrConnect() connects to relays,
34
+ * subscribes, and waits for the ack in one call. If you need to show a
35
+ * QR code only after the subscription is live, use prepareNostrConnect()
36
+ * instead.
25
37
  */
26
38
  static fromNostrConnect(connectionURI: string, clientSecretKey: Uint8Array, params?: BunkerSignerParams, timeoutOrAbort?: number | AbortSignal): Promise<BunkerNIP44Signer>;
27
39
  getPublicKey(): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"bunker-signer.d.ts","sourceRoot":"","sources":["../src/bunker-signer.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IAErC,OAAO;IAKP,wDAAwD;WAC3C,aAAa,CACxB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,kBAAkB,EAC3B,YAAY,CAAC,EAAE,UAAU,EACzB,cAAc,SAAS,GACtB,OAAO,CAAC,iBAAiB,CAAC;IAgB7B;;;;;OAKG;WACU,SAAS,CACpB,eAAe,EAAE,UAAU,EAC3B,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,kBAAkB,EAC3B,cAAc,SAAS,GACtB,OAAO,CAAC,iBAAiB,CAAC;IAoB7B;;;;;;;;OAQG;WACU,gBAAgB,CAC3B,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,UAAU,EAC3B,MAAM,CAAC,EAAE,kBAAkB,EAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,WAAW,GACpC,OAAO,CAAC,iBAAiB,CAAC;IAgFvB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAW/B,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAIvD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvE,YAAY,IAAI,MAAM;IAIhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
1
+ {"version":3,"file":"bunker-signer.d.ts","sourceRoot":"","sources":["../src/bunker-signer.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,aAAa,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC5C,oCAAoC;IACpC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IAErC,OAAO;IAKP,wDAAwD;WAC3C,aAAa,CACxB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,kBAAkB,EAC3B,YAAY,CAAC,EAAE,UAAU,EACzB,cAAc,SAAS,GACtB,OAAO,CAAC,iBAAiB,CAAC;IAgB7B;;;;;OAKG;WACU,SAAS,CACpB,eAAe,EAAE,UAAU,EAC3B,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,kBAAkB,EAC3B,cAAc,SAAS,GACtB,OAAO,CAAC,iBAAiB,CAAC;IAoB7B;;;;;OAKG;WACU,mBAAmB,CAC9B,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,UAAU,EAC3B,MAAM,CAAC,EAAE,kBAAkB,EAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,WAAW,GACpC,OAAO,CAAC,kBAAkB,CAAC;IA0G9B;;;;;;;OAOG;WACU,gBAAgB,CAC3B,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,UAAU,EAC3B,MAAM,CAAC,EAAE,kBAAkB,EAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,WAAW,GACpC,OAAO,CAAC,iBAAiB,CAAC;IAUvB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAW/B,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAIvD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvE,YAAY,IAAI,MAAM;IAIhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export type { TokenRefreshResult } from './oauth-signer';
5
5
  export { NsecSigner } from './nsec-signer';
6
6
  export { ExtensionSigner } from './extension-signer';
7
7
  export { BunkerNIP44Signer } from './bunker-signer';
8
+ export type { NostrConnectHandle } from './bunker-signer';
8
9
  export { OAuthSigner, OAuthError } from './oauth-signer';
9
10
  export { createSessionStore, restoreSession } from './session';
10
11
  export { buildOAuthUrl, exchangeCode } from './oauth';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG/D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG/D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -129,15 +129,12 @@ var BunkerNIP44Signer = class _BunkerNIP44Signer {
129
129
  return new _BunkerNIP44Signer(inner, "nostrconnect");
130
130
  }
131
131
  /**
132
- * Connect via a nostrconnect:// URI (QR code flow).
132
+ * Two-phase nostrconnect: sets up relay connections and subscription,
133
+ * then returns a handle. Show the QR code, then call handle.waitForSigner().
133
134
  *
134
- * Implements the connect handshake manually instead of using
135
- * BunkerSigner.fromURI, which sends a `switch_relays` RPC that
136
- * signers like Primal don't understand (causing a parse error and
137
- * potentially disrupting the session). After the handshake we create
138
- * the signer via BunkerSigner.fromBunker which skips switch_relays.
135
+ * This avoids a race where the user scans before the subscription is live.
139
136
  */
140
- static async fromNostrConnect(connectionURI, clientSecretKey, params, timeoutOrAbort) {
137
+ static async prepareNostrConnect(connectionURI, clientSecretKey, params, timeoutOrAbort) {
141
138
  const uri = new URL(connectionURI);
142
139
  const relays = uri.searchParams.getAll("relay");
143
140
  const secret = uri.searchParams.get("secret");
@@ -146,13 +143,29 @@ var BunkerNIP44Signer = class _BunkerNIP44Signer {
146
143
  throw new Error("No relays specified in nostrconnect URI");
147
144
  }
148
145
  const abort = typeof timeoutOrAbort === "number" ? AbortSignal.timeout(timeoutOrAbort) : timeoutOrAbort;
146
+ const ac = new AbortController();
147
+ const effectiveAbort = abort ?? ac.signal;
149
148
  const pool = new SimplePool();
150
- const signerPubkey = await new Promise((resolve, reject) => {
151
- if (abort?.aborted) {
149
+ const RELAY_CONNECT_TIMEOUT = 5e3;
150
+ const results = await Promise.allSettled(
151
+ relays.map(
152
+ (r) => Promise.race([
153
+ pool.ensureRelay(r),
154
+ new Promise(
155
+ (_, rej) => setTimeout(() => rej(new Error(`Relay ${r} connect timeout`)), RELAY_CONNECT_TIMEOUT)
156
+ )
157
+ ])
158
+ )
159
+ );
160
+ if (results.every((r) => r.status === "rejected")) {
161
+ throw new Error("Failed to connect to any relay");
162
+ }
163
+ let settled = false;
164
+ const signerPromise = new Promise((resolve, reject) => {
165
+ if (effectiveAbort.aborted) {
152
166
  reject(new Error("Aborted"));
153
167
  return;
154
168
  }
155
- let settled = false;
156
169
  const sub = pool.subscribe(
157
170
  relays,
158
171
  { kinds: [24133], "#p": [clientPubkey], limit: 0 },
@@ -179,10 +192,10 @@ var BunkerNIP44Signer = class _BunkerNIP44Signer {
179
192
  );
180
193
  }
181
194
  },
182
- abort
195
+ abort: effectiveAbort
183
196
  }
184
197
  );
185
- abort?.addEventListener(
198
+ effectiveAbort.addEventListener(
186
199
  "abort",
187
200
  () => {
188
201
  if (!settled) {
@@ -194,12 +207,37 @@ var BunkerNIP44Signer = class _BunkerNIP44Signer {
194
207
  { once: true }
195
208
  );
196
209
  });
197
- const bp = { pubkey: signerPubkey, relays, secret: secret || "" };
198
- const inner = BunkerSigner.fromBunker(clientSecretKey, bp, {
199
- ...params,
200
- pool
201
- });
202
- return new _BunkerNIP44Signer(inner, "nostrconnect");
210
+ return {
211
+ async waitForSigner() {
212
+ const signerPubkey = await signerPromise;
213
+ const bp = { pubkey: signerPubkey, relays, secret: secret || "" };
214
+ const inner = BunkerSigner.fromBunker(clientSecretKey, bp, {
215
+ ...params,
216
+ pool
217
+ });
218
+ return new _BunkerNIP44Signer(inner, "nostrconnect");
219
+ },
220
+ abort() {
221
+ ac.abort();
222
+ }
223
+ };
224
+ }
225
+ /**
226
+ * Connect via a nostrconnect:// URI (QR code flow).
227
+ *
228
+ * Convenience wrapper around prepareNostrConnect() — connects to relays,
229
+ * subscribes, and waits for the ack in one call. If you need to show a
230
+ * QR code only after the subscription is live, use prepareNostrConnect()
231
+ * instead.
232
+ */
233
+ static async fromNostrConnect(connectionURI, clientSecretKey, params, timeoutOrAbort) {
234
+ const handle = await _BunkerNIP44Signer.prepareNostrConnect(
235
+ connectionURI,
236
+ clientSecretKey,
237
+ params,
238
+ timeoutOrAbort
239
+ );
240
+ return handle.waitForSigner();
203
241
  }
204
242
  async getPublicKey() {
205
243
  return Promise.race([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "divine-signer",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",