signet-login 0.7.0 → 0.7.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.
package/dist/modal.js CHANGED
@@ -75,7 +75,7 @@ function renderPicker(refs, appName, theme) {
75
75
  <button data-choice="nostrconnect" style="${buttonStyle(dark)}"><span style="font-size:1.2rem;">📡</span><span><strong>Connect a Nostr signer</strong><br><span style="font-size:0.8rem;color:${muted};">Scan with nsec.app, Amber, Keychat…</span></span></button>
76
76
  <button data-choice="nsec" style="${buttonStyle(dark)}"><span style="font-size:1.2rem;">⚠️</span><span><strong>Paste private key</strong><br><span style="font-size:0.8rem;color:${muted};">In-memory only — risky, last resort</span></span></button>
77
77
  </div>
78
- <button data-choice="cancel" style="background:none;border:0;color:${muted};padding:12px;cursor:pointer;font-size:0.85rem;margin-top:8px;">Cancel</button>
78
+ <button data-choice="cancel" style="background:transparent;color:${dark ? '#e0e0e0' : '#1a1a2e'};border:1px solid ${dark ? '#3a3a4e' : '#d0d0d0'};border-radius:8px;padding:12px;cursor:pointer;font-size:0.95rem;width:100%;margin-top:12px;text-align:center;">Cancel</button>
79
79
  `;
80
80
  return new Promise(resolve => {
81
81
  refs.dialog.querySelectorAll('button[data-choice]').forEach(btn => {
@@ -482,15 +482,28 @@ export async function showLoginModal(opts) {
482
482
  throw new Error('appName-too-long');
483
483
  const resolved = resolveOptions(opts);
484
484
  const refs = buildModalShell(resolved.theme);
485
+ // Escape and the Android / OS back button fire the dialog's native
486
+ // `cancel` event. Unhandled, the dialog closes visually but the
487
+ // in-flight flow promise never resolves — login() hangs forever and
488
+ // the caller's UI is left stuck behind a dead modal. Racing every
489
+ // flow await against this lets the modal exit cleanly.
490
+ let userAborted = false;
491
+ const aborted = new Promise((resolve) => {
492
+ refs.dialog.addEventListener('cancel', () => { userAborted = true; resolve(null); });
493
+ });
485
494
  try {
486
495
  while (true) {
487
496
  const choice = resolved.preferredMethod
488
497
  ? resolved.preferredMethod
489
- : await renderPicker(refs, resolved.appName, resolved.theme);
490
- if (choice === 'cancel')
498
+ : await Promise.race([renderPicker(refs, resolved.appName, resolved.theme), aborted]);
499
+ if (userAborted)
500
+ return null;
501
+ if (choice === null || choice === 'cancel')
491
502
  return null;
492
503
  if (choice === 'nip07') {
493
- const result = await runNip07Flow(refs, resolved);
504
+ const result = await Promise.race([runNip07Flow(refs, resolved), aborted]);
505
+ if (userAborted)
506
+ return null;
494
507
  if (!result) {
495
508
  if (resolved.preferredMethod)
496
509
  return null;
@@ -537,7 +550,9 @@ export async function showLoginModal(opts) {
537
550
  return null; // unreachable
538
551
  }
539
552
  if (choice === 'qr') {
540
- const result = await runRedirectFlow(refs, resolved);
553
+ const result = await Promise.race([runRedirectFlow(refs, resolved), aborted]);
554
+ if (userAborted)
555
+ return null;
541
556
  if (!result) {
542
557
  if (resolved.preferredMethod)
543
558
  return null;
@@ -555,7 +570,9 @@ export async function showLoginModal(opts) {
555
570
  return session;
556
571
  }
557
572
  if (choice === 'bunker') {
558
- const signer = await runBunkerFlow(refs, resolved);
573
+ const signer = await Promise.race([runBunkerFlow(refs, resolved), aborted]);
574
+ if (userAborted)
575
+ return null;
559
576
  if (!signer) {
560
577
  if (resolved.preferredMethod)
561
578
  return null;
@@ -579,7 +596,9 @@ export async function showLoginModal(opts) {
579
596
  };
580
597
  }
581
598
  if (choice === 'nostrconnect') {
582
- const signer = await runNostrConnectFlow(refs, resolved);
599
+ const signer = await Promise.race([runNostrConnectFlow(refs, resolved), aborted]);
600
+ if (userAborted)
601
+ return null;
583
602
  if (!signer) {
584
603
  if (resolved.preferredMethod)
585
604
  return null;
@@ -606,7 +625,9 @@ export async function showLoginModal(opts) {
606
625
  };
607
626
  }
608
627
  if (choice === 'nsec') {
609
- const signer = await runNsecFlow(refs, resolved);
628
+ const signer = await Promise.race([runNsecFlow(refs, resolved), aborted]);
629
+ if (userAborted)
630
+ return null;
610
631
  if (!signer) {
611
632
  if (resolved.preferredMethod)
612
633
  return null;
@@ -23,7 +23,7 @@ Minimum version required to store current data is: `+i+`.
23
23
  <button data-choice="nostrconnect" style="${ae(r)}"><span style="font-size:1.2rem;">\u{1F4E1}</span><span><strong>Connect a Nostr signer</strong><br><span style="font-size:0.8rem;color:${o};">Scan with nsec.app, Amber, Keychat\u2026</span></span></button>
24
24
  <button data-choice="nsec" style="${ae(r)}"><span style="font-size:1.2rem;">\u26A0\uFE0F</span><span><strong>Paste private key</strong><br><span style="font-size:0.8rem;color:${o};">In-memory only \u2014 risky, last resort</span></span></button>
25
25
  </div>
26
- <button data-choice="cancel" style="background:none;border:0;color:${o};padding:12px;cursor:pointer;font-size:0.85rem;margin-top:8px;">Cancel</button>
26
+ <button data-choice="cancel" style="background:transparent;color:${r?"#e0e0e0":"#1a1a2e"};border:1px solid ${r?"#3a3a4e":"#d0d0d0"};border-radius:8px;padding:12px;cursor:pointer;font-size:0.95rem;width:100%;margin-top:12px;text-align:center;">Cancel</button>
27
27
  `,new Promise(a=>{t.dialog.querySelectorAll("button[data-choice]").forEach(c=>{c.addEventListener("click",()=>{let l=c.dataset.choice;a(l)})})})}async function pd(t,e){let n=Bt(e.theme),r=n?"#888":"#666",o=n?"#e0e0e0":"#1a1a2e";t.dialog.innerHTML=`
28
28
  <h2 style="margin:0 0 8px;font-size:1.2rem;">Waiting for your extension</h2>
29
29
  <p style="margin:0 0 20px;color:${r};font-size:0.85rem;">Approve the sign-in prompt in bark, Alby, nos2x, or whichever NIP-07 extension you use. Cold-start can take a few seconds.</p>
@@ -79,7 +79,7 @@ Minimum version required to store current data is: `+i+`.
79
79
  <button data-action="back" style="${ae(n)}width:auto;flex:0 0 auto;padding:8px 16px;">\u2190 Back</button>
80
80
  <button data-action="connect" style="${ae(n,!0)}width:auto;flex:1;padding:8px 16px;text-align:center;">Sign in</button>
81
81
  </div>
82
- `,new Promise(s=>{let a=!1,c=f=>{a||(a=!0,s(f))},l=t.dialog.querySelector("#signet-login-nsec-input"),d=t.dialog.querySelector("#signet-login-nsec-status"),h=t.dialog.querySelector('[data-action="connect"]');t.dialog.querySelector('[data-action="back"]')?.addEventListener("click",()=>{l&&(l.value=""),c(null)}),h?.addEventListener("click",()=>{let f=l?.value??"";if(!f.trim()){d&&(d.textContent="Please paste an nsec.");return}try{let u=Fs(f);l&&(l.value=""),c(u)}catch(u){d&&(d.textContent=`\u2717 ${u instanceof Error?u.message:String(u)}`,d.style.color="#d04848")}})})}function xd(t){let e=t.challenge??ud();if(!/^[0-9a-f]{64}$/i.test(e))throw new Error("challenge-must-be-64-hex");let n=typeof window<"u"?window.location.origin:"http://localhost",r=Math.max(5e3,Math.min(t.timeout??ze.timeout,6e5)),o={appName:t.appName,challenge:e.toLowerCase(),origin:n,relayUrl:t.relayUrl??ze.relayUrl,theme:t.theme??ze.theme,timeout:r,signetAppOrigin:t.signetAppOrigin??ze.signetAppOrigin};return t.preferredMethod!==void 0&&(o.preferredMethod=t.preferredMethod),t.redirectCallback!==void 0&&(o.redirectCallback=t.redirectCallback),o}async function Wc(t){if(!t.appName||t.appName.length===0)throw new Error("appName-required");if(t.appName.length>64)throw new Error("appName-too-long");let e=xd(t),n=dd(e.theme);try{for(;;){let r=e.preferredMethod?e.preferredMethod:await gd(n,e.appName,e.theme);if(r==="cancel")return null;if(r==="nip07"){let o=await pd(n,e);if(!o){if(e.preferredMethod)return null;continue}let i=await ln();return{pubkey:o.pubkey,method:"nip07",signer:i,authEvent:o.authEvent}}if(r==="redirect")return await xr({appName:e.appName,challenge:e.challenge,origin:e.origin,signetAppOrigin:e.signetAppOrigin,...e.redirectCallback!==void 0?{redirectCallback:e.redirectCallback}:{}}),null;if(r==="amber")return await oa({appName:e.appName,challenge:e.challenge,origin:e.origin,...e.redirectCallback!==void 0?{redirectCallback:e.redirectCallback}:{}}),null;if(r==="qr"){let o=await yd(n,e);if(!o){if(e.preferredMethod)return null;continue}let i=new Ee(o.pubkey,o.authEvent),s={pubkey:o.pubkey,method:"redirect",signer:i,authEvent:o.authEvent};return o.displayName&&(s.displayName=o.displayName),s}if(r==="bunker"){let o=await bd(n,e);if(!o){if(e.preferredMethod)return null;continue}let i=await o.signEvent({kind:21236,content:"",tags:[["challenge",e.challenge],["origin",e.origin],["app",e.appName]]});return{pubkey:o.pubkey,method:"bunker",signer:o,authEvent:i}}if(r==="nostrconnect"){let o=await md(n,e);if(!o){if(e.preferredMethod)return null;continue}let i=await o.signEvent({kind:21236,content:"",tags:[["challenge",e.challenge],["origin",e.origin],["app",e.appName]]});return{pubkey:o.pubkey,method:"bunker",signer:o,authEvent:i}}if(r==="nsec"){let o=await wd(n,e);if(!o){if(e.preferredMethod)return null;continue}let i=await o.signEvent({kind:21236,content:"",tags:[["challenge",e.challenge],["origin",e.origin],["app",e.appName]]});return{pubkey:o.pubkey,method:"nsec",signer:o,authEvent:i}}}}finally{hd(n)}}function Yc(t){let e={};typeof window<"u"&&new URLSearchParams(window.location.search).forEach((o,i)=>{e[i]=o});let n=typeof window<"u"&&!!window.opener&&window.opener!==window;if(n){try{window.opener.postMessage({type:"signet-login-callback",params:e},"*")}catch{}if(t?.closeAfterPost??!0)try{window.close()}catch{}}return{params:e,isPopup:n}}async function Jc(t){if(t.mode==="redirect"){if(typeof window>"u")throw new Error("signet-login: redirect mode requires a browser environment");let n=t.challenge??Ed();if(!/^[0-9a-f]{64}$/i.test(n))throw new Error("challenge-must-be-64-hex");if(!t.appName||t.appName.length===0)throw new Error("appName-required");if(t.appName.length>64)throw new Error("appName-too-long");return xr({appName:t.appName,challenge:n.toLowerCase(),origin:window.location.origin,signetAppOrigin:t.signetAppOrigin??ze.signetAppOrigin,...t.redirectCallback!==void 0?{redirectCallback:t.redirectCallback}:{}})}let e=await Wc(t);return e?(t.persist!==!1&&Nr(e),e):null}function Ed(){let t=new Uint8Array(32);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,"0")).join("")}async function Xc(t){let e=ea();if(!e)return null;let n;try{n=JSON.parse(e.authEventJson)}catch{return He(),null}if(e.method==="nip07"){if(!cn()){let i=new Ee(e.pubkey,n);return{pubkey:e.pubkey,method:"redirect",signer:i,authEvent:n}}try{let i=await ln();return i.pubkey!==e.pubkey?(He(),null):{pubkey:e.pubkey,method:"nip07",signer:i,authEvent:n}}catch{return He(),null}}if(e.method==="bunker"){if(t?.reconnectBunker===!1){let i=new Ee(e.pubkey,n);return{pubkey:e.pubkey,method:"redirect",signer:i,authEvent:n}}if(!e.bunkerUri||!e.bunkerClientSkHex)return He(),null;try{let i=na(e.bunkerClientSkHex),s=await fn({uri:e.bunkerUri,clientSecretKey:i});return s.pubkey!==e.pubkey?(await s.close(),He(),null):{pubkey:e.pubkey,method:"bunker",signer:s,authEvent:n}}catch{return He(),null}}let r=new Ee(e.pubkey,n),o={pubkey:e.pubkey,method:e.method,signer:r,authEvent:n};return e.displayName&&(o.displayName=e.displayName),o}var Fc=Yc;async function Qc(){let t=ia();if(t.kind==="session")return Nr(t.session),t;if(t.kind!=="no-callback")return t;let e=Ga();if(e.kind!=="session")return e;if(e.bunkerUri)try{let n=await fn({uri:e.bunkerUri});if(n.pubkey.toLowerCase()===e.session.pubkey.toLowerCase()){let r={pubkey:e.session.pubkey,method:"bunker",signer:n,authEvent:e.session.authEvent};return e.session.displayName&&(r.displayName=e.session.displayName),Nr(r),{kind:"session",session:r}}try{await n.close()}catch{}}catch{}return Nr(e.session),e}async function el(t){if(t)try{await t.signer.close()}catch{}He()}function Nr(t){if(t.method==="nsec")return;let e={pubkey:t.pubkey,method:t.method,authEventJson:JSON.stringify(t.authEvent)};if(t.method==="bunker"){let n=t.signer;n.bunkerUri&&n.clientSecretKey instanceof Uint8Array&&(e.bunkerUri=n.bunkerUri,e.bunkerClientSkHex=ta(n.clientSecretKey))}t.expiresAt!==void 0&&(e.expiresAt=t.expiresAt),t.displayName!==void 0&&(e.displayName=t.displayName),Qs(e)}if(typeof window<"u"){let e=window.Signet??{};Object.assign(e,{login:Jc,restoreSession:Xc,logout:el,handleCallback:Fc,handleRedirectCallback:Qc}),window.Signet=e}return ll(vd);})();
82
+ `,new Promise(s=>{let a=!1,c=f=>{a||(a=!0,s(f))},l=t.dialog.querySelector("#signet-login-nsec-input"),d=t.dialog.querySelector("#signet-login-nsec-status"),h=t.dialog.querySelector('[data-action="connect"]');t.dialog.querySelector('[data-action="back"]')?.addEventListener("click",()=>{l&&(l.value=""),c(null)}),h?.addEventListener("click",()=>{let f=l?.value??"";if(!f.trim()){d&&(d.textContent="Please paste an nsec.");return}try{let u=Fs(f);l&&(l.value=""),c(u)}catch(u){d&&(d.textContent=`\u2717 ${u instanceof Error?u.message:String(u)}`,d.style.color="#d04848")}})})}function xd(t){let e=t.challenge??ud();if(!/^[0-9a-f]{64}$/i.test(e))throw new Error("challenge-must-be-64-hex");let n=typeof window<"u"?window.location.origin:"http://localhost",r=Math.max(5e3,Math.min(t.timeout??ze.timeout,6e5)),o={appName:t.appName,challenge:e.toLowerCase(),origin:n,relayUrl:t.relayUrl??ze.relayUrl,theme:t.theme??ze.theme,timeout:r,signetAppOrigin:t.signetAppOrigin??ze.signetAppOrigin};return t.preferredMethod!==void 0&&(o.preferredMethod=t.preferredMethod),t.redirectCallback!==void 0&&(o.redirectCallback=t.redirectCallback),o}async function Wc(t){if(!t.appName||t.appName.length===0)throw new Error("appName-required");if(t.appName.length>64)throw new Error("appName-too-long");let e=xd(t),n=dd(e.theme),r=!1,o=new Promise(i=>{n.dialog.addEventListener("cancel",()=>{r=!0,i(null)})});try{for(;;){let i=e.preferredMethod?e.preferredMethod:await Promise.race([gd(n,e.appName,e.theme),o]);if(r||i===null||i==="cancel")return null;if(i==="nip07"){let s=await Promise.race([pd(n,e),o]);if(r)return null;if(!s){if(e.preferredMethod)return null;continue}let a=await ln();return{pubkey:s.pubkey,method:"nip07",signer:a,authEvent:s.authEvent}}if(i==="redirect")return await xr({appName:e.appName,challenge:e.challenge,origin:e.origin,signetAppOrigin:e.signetAppOrigin,...e.redirectCallback!==void 0?{redirectCallback:e.redirectCallback}:{}}),null;if(i==="amber")return await oa({appName:e.appName,challenge:e.challenge,origin:e.origin,...e.redirectCallback!==void 0?{redirectCallback:e.redirectCallback}:{}}),null;if(i==="qr"){let s=await Promise.race([yd(n,e),o]);if(r)return null;if(!s){if(e.preferredMethod)return null;continue}let a=new Ee(s.pubkey,s.authEvent),c={pubkey:s.pubkey,method:"redirect",signer:a,authEvent:s.authEvent};return s.displayName&&(c.displayName=s.displayName),c}if(i==="bunker"){let s=await Promise.race([bd(n,e),o]);if(r)return null;if(!s){if(e.preferredMethod)return null;continue}let a=await s.signEvent({kind:21236,content:"",tags:[["challenge",e.challenge],["origin",e.origin],["app",e.appName]]});return{pubkey:s.pubkey,method:"bunker",signer:s,authEvent:a}}if(i==="nostrconnect"){let s=await Promise.race([md(n,e),o]);if(r)return null;if(!s){if(e.preferredMethod)return null;continue}let a=await s.signEvent({kind:21236,content:"",tags:[["challenge",e.challenge],["origin",e.origin],["app",e.appName]]});return{pubkey:s.pubkey,method:"bunker",signer:s,authEvent:a}}if(i==="nsec"){let s=await Promise.race([wd(n,e),o]);if(r)return null;if(!s){if(e.preferredMethod)return null;continue}let a=await s.signEvent({kind:21236,content:"",tags:[["challenge",e.challenge],["origin",e.origin],["app",e.appName]]});return{pubkey:s.pubkey,method:"nsec",signer:s,authEvent:a}}}}finally{hd(n)}}function Yc(t){let e={};typeof window<"u"&&new URLSearchParams(window.location.search).forEach((o,i)=>{e[i]=o});let n=typeof window<"u"&&!!window.opener&&window.opener!==window;if(n){try{window.opener.postMessage({type:"signet-login-callback",params:e},"*")}catch{}if(t?.closeAfterPost??!0)try{window.close()}catch{}}return{params:e,isPopup:n}}async function Jc(t){if(t.mode==="redirect"){if(typeof window>"u")throw new Error("signet-login: redirect mode requires a browser environment");let n=t.challenge??Ed();if(!/^[0-9a-f]{64}$/i.test(n))throw new Error("challenge-must-be-64-hex");if(!t.appName||t.appName.length===0)throw new Error("appName-required");if(t.appName.length>64)throw new Error("appName-too-long");return xr({appName:t.appName,challenge:n.toLowerCase(),origin:window.location.origin,signetAppOrigin:t.signetAppOrigin??ze.signetAppOrigin,...t.redirectCallback!==void 0?{redirectCallback:t.redirectCallback}:{}})}let e=await Wc(t);return e?(t.persist!==!1&&Nr(e),e):null}function Ed(){let t=new Uint8Array(32);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,"0")).join("")}async function Xc(t){let e=ea();if(!e)return null;let n;try{n=JSON.parse(e.authEventJson)}catch{return He(),null}if(e.method==="nip07"){if(!cn()){let i=new Ee(e.pubkey,n);return{pubkey:e.pubkey,method:"redirect",signer:i,authEvent:n}}try{let i=await ln();return i.pubkey!==e.pubkey?(He(),null):{pubkey:e.pubkey,method:"nip07",signer:i,authEvent:n}}catch{return He(),null}}if(e.method==="bunker"){if(t?.reconnectBunker===!1){let i=new Ee(e.pubkey,n);return{pubkey:e.pubkey,method:"redirect",signer:i,authEvent:n}}if(!e.bunkerUri||!e.bunkerClientSkHex)return He(),null;try{let i=na(e.bunkerClientSkHex),s=await fn({uri:e.bunkerUri,clientSecretKey:i});return s.pubkey!==e.pubkey?(await s.close(),He(),null):{pubkey:e.pubkey,method:"bunker",signer:s,authEvent:n}}catch{return He(),null}}let r=new Ee(e.pubkey,n),o={pubkey:e.pubkey,method:e.method,signer:r,authEvent:n};return e.displayName&&(o.displayName=e.displayName),o}var Fc=Yc;async function Qc(){let t=ia();if(t.kind==="session")return Nr(t.session),t;if(t.kind!=="no-callback")return t;let e=Ga();if(e.kind!=="session")return e;if(e.bunkerUri)try{let n=await fn({uri:e.bunkerUri});if(n.pubkey.toLowerCase()===e.session.pubkey.toLowerCase()){let r={pubkey:e.session.pubkey,method:"bunker",signer:n,authEvent:e.session.authEvent};return e.session.displayName&&(r.displayName=e.session.displayName),Nr(r),{kind:"session",session:r}}try{await n.close()}catch{}}catch{}return Nr(e.session),e}async function el(t){if(t)try{await t.signer.close()}catch{}He()}function Nr(t){if(t.method==="nsec")return;let e={pubkey:t.pubkey,method:t.method,authEventJson:JSON.stringify(t.authEvent)};if(t.method==="bunker"){let n=t.signer;n.bunkerUri&&n.clientSecretKey instanceof Uint8Array&&(e.bunkerUri=n.bunkerUri,e.bunkerClientSkHex=ta(n.clientSecretKey))}t.expiresAt!==void 0&&(e.expiresAt=t.expiresAt),t.displayName!==void 0&&(e.displayName=t.displayName),Qs(e)}if(typeof window<"u"){let e=window.Signet??{};Object.assign(e,{login:Jc,restoreSession:Xc,logout:el,handleCallback:Fc,handleRedirectCallback:Qc}),window.Signet=e}return ll(vd);})();
83
83
  /*! Bundled license information:
84
84
 
85
85
  @noble/hashes/utils.js:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signet-login",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Sign in with Signet — drop-in login SDK for Nostr-aware websites. NIP-07, bunker URI, and Signet redirect/QR in one unified API.",
5
5
  "type": "module",
6
6
  "main": "./dist/signet-login.js",