signet-login 0.10.1 → 0.10.2

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
@@ -6,7 +6,8 @@ Published as `signet-login`.
6
6
 
7
7
  **Signet Access** is a drop-in auth and signer-access SDK for Nostr-aware websites. One picker, one session shape, multiple ways to prove identity and, when available, keep a live signer:
8
8
 
9
- - **Sign in with Signet** on this device or by cross-device QR
9
+ - **Local Signet** on this device, against hosted or local-dev Signet
10
+ - **Remote Signet** by cross-device QR, so a phone or second machine can approve
10
11
  - **Browser extension** via NIP-07 (bark, Alby, nos2x, Flamingo, ...)
11
12
  - **Connect a Nostr signer** via app-initiated NIP-46 / NostrConnect
12
13
  - **Paste or scan bunker URI** for Heartwood, nsecBunker, Amber, or compatible signers
@@ -88,8 +89,10 @@ interface SignetStorage {
88
89
 
89
90
  type LoginPickerMethod =
90
91
  | 'nip07'
91
- | 'redirect' // same-device Signet, relay delivery
92
- | 'qr' // cross-device Signet QR
92
+ | 'local-signet' // same-device Signet, relay delivery
93
+ | 'remote-signet' // cross-device Signet QR
94
+ | 'redirect' // legacy alias for local-signet
95
+ | 'qr' // legacy alias for remote-signet
93
96
  | 'bunker' // paste bunker://
94
97
  | 'nostrconnect' // show nostrconnect:// QR
95
98
  | 'amber' // Android NIP-55
@@ -110,7 +113,14 @@ By default, the picker shows ordinary user-facing methods first and groups `bunk
110
113
  ```js
111
114
  await Signet.login({
112
115
  appName: 'My Game',
113
- methods: ['redirect', 'qr', 'nip07'],
116
+ methods: ['local-signet', 'remote-signet', 'nip07'],
117
+ });
118
+
119
+ await Signet.login({
120
+ appName: 'My Local Dev Game',
121
+ preferredMethod: 'local-signet',
122
+ signetAppOrigin: 'http://localhost:5174',
123
+ relayUrl: 'ws://localhost:7777',
114
124
  });
115
125
 
116
126
  await Signet.login({
@@ -121,6 +131,8 @@ await Signet.login({
121
131
  });
122
132
  ```
123
133
 
134
+ `redirect` and `qr` remain supported picker aliases for existing apps, but new integrations should use `local-signet` and `remote-signet`.
135
+
124
136
  When camera APIs are available, the bunker URI screen can scan `bunker://` QR codes directly. Paste remains the fallback.
125
137
 
126
138
  ### Headless/custom UI
package/dist/modal.js CHANGED
@@ -14,7 +14,8 @@ import { bytesToHex } from '@noble/hashes/utils';
14
14
  import QRCode from 'qrcode';
15
15
  import jsQR from 'jsqr';
16
16
  const QR_BUNKER_CONNECT_TIMEOUT_MS = 8000;
17
- const DEFAULT_PICKER_METHODS = ['nip07', 'amber', 'redirect', 'qr', 'bunker', 'nostrconnect', 'nsec'];
17
+ const DEFAULT_PICKER_METHODS = ['nip07', 'amber', 'local-signet', 'remote-signet', 'bunker', 'nostrconnect', 'nsec'];
18
+ const ALL_PICKER_METHODS = [...DEFAULT_PICKER_METHODS, 'redirect', 'qr'];
18
19
  const DEFAULT_ADVANCED_METHODS = ['bunker', 'nostrconnect', 'nsec'];
19
20
  const DEFAULT_NOSTR_CONNECT_PERMS = ['sign_event', 'nip44_encrypt', 'nip44_decrypt'];
20
21
  function escapeHtml(str) {
@@ -254,12 +255,28 @@ async function startCameraQrScanner(input) {
254
255
  const METHOD_META = {
255
256
  nip07: { icon: '🌐', title: 'Browser extension', hint: 'bark, Alby, nos2x' },
256
257
  amber: { icon: '🤖', title: 'Sign in with Amber', hint: 'Android signer (NIP-55)' },
257
- redirect: { icon: '🪪', title: 'Sign in with Signet', hint: 'Open Signet on this device' },
258
- qr: { icon: '📱', title: 'Signet on another device', hint: 'Scan QR with your phone' },
258
+ 'local-signet': { icon: '🪪', title: 'Local Signet', hint: 'Open Signet on this device' },
259
+ 'remote-signet': { icon: '📱', title: 'Remote Signet', hint: 'Scan with Signet on another device' },
260
+ redirect: { icon: '🪪', title: 'Local Signet', hint: 'Open Signet on this device' },
261
+ qr: { icon: '📱', title: 'Remote Signet', hint: 'Scan with Signet on another device' },
259
262
  bunker: { icon: '🔑', title: 'Paste bunker URI', hint: 'For NIP-46 power users' },
260
263
  nostrconnect: { icon: '📡', title: 'Connect a Nostr signer', hint: 'Scan with nsec.app, Amber, Keychat...' },
261
264
  nsec: { icon: '⚠️', title: 'Paste private key', hint: 'In-memory only - risky, last resort' },
262
265
  };
266
+ function pickerMethodKey(method) {
267
+ if (method === 'local-signet' || method === 'redirect')
268
+ return 'local-signet';
269
+ if (method === 'remote-signet' || method === 'qr')
270
+ return 'remote-signet';
271
+ return method;
272
+ }
273
+ function routePickerChoice(choice) {
274
+ if (choice === 'local-signet')
275
+ return 'redirect';
276
+ if (choice === 'remote-signet')
277
+ return 'qr';
278
+ return choice;
279
+ }
263
280
  function isMethodAvailable(method) {
264
281
  if (method === 'nip07')
265
282
  return hasNip07();
@@ -277,9 +294,9 @@ function renderPicker(refs, opts) {
277
294
  return new Promise(resolve => {
278
295
  let advancedOpen = false;
279
296
  const availableMethods = opts.methods.filter(isMethodAvailable);
280
- const advancedSet = new Set(opts.advancedMethods);
281
- const primaryMethods = availableMethods.filter(method => !advancedSet.has(method));
282
- const advancedMethods = availableMethods.filter(method => advancedSet.has(method));
297
+ const advancedSet = new Set(opts.advancedMethods.map(pickerMethodKey));
298
+ const primaryMethods = availableMethods.filter(method => !advancedSet.has(pickerMethodKey(method)));
299
+ const advancedMethods = availableMethods.filter(method => advancedSet.has(pickerMethodKey(method)));
283
300
  const attachChoiceHandlers = () => {
284
301
  refs.dialog.querySelectorAll('button[data-choice]').forEach(btn => {
285
302
  btn.addEventListener('click', () => {
@@ -806,20 +823,25 @@ async function runNsecFlow(refs, opts) {
806
823
  }
807
824
  function uniquePickerMethods(input, fallback) {
808
825
  const source = input ?? fallback;
809
- const allowed = new Set(DEFAULT_PICKER_METHODS);
826
+ const allowed = new Set(ALL_PICKER_METHODS);
827
+ const seen = new Set();
810
828
  const out = [];
811
829
  for (const method of source) {
812
830
  if (!allowed.has(method))
813
831
  continue;
814
- if (!out.includes(method))
815
- out.push(method);
832
+ const key = pickerMethodKey(method);
833
+ if (seen.has(key))
834
+ continue;
835
+ seen.add(key);
836
+ out.push(method);
816
837
  }
817
838
  return input === undefined && out.length === 0 ? [...fallback] : out;
818
839
  }
819
840
  function resolveMethodConfig(opts) {
820
841
  const methods = uniquePickerMethods(opts.methods, DEFAULT_PICKER_METHODS);
842
+ const methodKeys = new Set(methods.map(pickerMethodKey));
821
843
  const advancedMethods = uniquePickerMethods(opts.advancedMethods, DEFAULT_ADVANCED_METHODS)
822
- .filter(method => methods.includes(method));
844
+ .filter(method => methodKeys.has(pickerMethodKey(method)));
823
845
  return { methods, advancedMethods };
824
846
  }
825
847
  function resolveRelayUrls(opts) {
@@ -895,11 +917,12 @@ async function runLoginModal(opts) {
895
917
  const choice = resolved.preferredMethod
896
918
  ? resolved.preferredMethod
897
919
  : await Promise.race([renderPicker(refs, resolved), aborted]);
920
+ const routeChoice = choice === null ? null : routePickerChoice(choice);
898
921
  if (userAborted)
899
922
  return null;
900
- if (choice === null || choice === 'cancel')
923
+ if (routeChoice === null || routeChoice === 'cancel')
901
924
  return null;
902
- if (choice === 'nip07') {
925
+ if (routeChoice === 'nip07') {
903
926
  const result = await Promise.race([runNip07Flow(refs, resolved), aborted]);
904
927
  if (userAborted)
905
928
  return null;
@@ -919,7 +942,7 @@ async function runLoginModal(opts) {
919
942
  authEvent: result.authEvent,
920
943
  };
921
944
  }
922
- if (choice === 'redirect') {
945
+ if (routeChoice === 'redirect') {
923
946
  // Same-device Signet in the modal must keep this app tab alive and keep
924
947
  // the My Signet tab alive as the ongoing bunker. Use the relay-backed
925
948
  // auth response path here; explicit `login({ mode: 'redirect' })`
@@ -942,7 +965,7 @@ async function runLoginModal(opts) {
942
965
  }
943
966
  return session;
944
967
  }
945
- if (choice === 'amber') {
968
+ if (routeChoice === 'amber') {
946
969
  // Same-tab navigation to a `nostrsigner:` URL. Android dispatches
947
970
  // it to Amber; the page comes back via callbackUrl with the signed
948
971
  // event in `?event=`. Picked up on next boot by handleRedirectCallback.
@@ -955,7 +978,7 @@ async function runLoginModal(opts) {
955
978
  });
956
979
  return null; // unreachable
957
980
  }
958
- if (choice === 'qr') {
981
+ if (routeChoice === 'qr') {
959
982
  const result = await Promise.race([runRedirectFlow(refs, resolved), aborted]);
960
983
  if (userAborted)
961
984
  return null;
@@ -974,7 +997,7 @@ async function runLoginModal(opts) {
974
997
  }
975
998
  return session;
976
999
  }
977
- if (choice === 'bunker') {
1000
+ if (routeChoice === 'bunker') {
978
1001
  const signer = await Promise.race([runBunkerFlow(refs, resolved), aborted]);
979
1002
  if (userAborted)
980
1003
  return null;
@@ -1000,7 +1023,7 @@ async function runLoginModal(opts) {
1000
1023
  authEvent,
1001
1024
  };
1002
1025
  }
1003
- if (choice === 'nostrconnect') {
1026
+ if (routeChoice === 'nostrconnect') {
1004
1027
  const signer = await Promise.race([runNostrConnectFlow(refs, resolved), aborted]);
1005
1028
  if (userAborted)
1006
1029
  return null;
@@ -1029,7 +1052,7 @@ async function runLoginModal(opts) {
1029
1052
  authEvent,
1030
1053
  };
1031
1054
  }
1032
- if (choice === 'nsec') {
1055
+ if (routeChoice === 'nsec') {
1033
1056
  const signer = await Promise.race([runNsecFlow(refs, resolved), aborted]);
1034
1057
  if (userAborted)
1035
1058
  return null;