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 +16 -4
- package/dist/modal.js +41 -18
- package/dist/signet-login.iife.js +16 -16
- package/dist/types.d.ts +7 -5
- package/package.json +1 -1
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
|
-
- **
|
|
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
|
-
| '
|
|
92
|
-
| '
|
|
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: ['
|
|
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', '
|
|
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
|
-
|
|
258
|
-
|
|
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(
|
|
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
|
-
|
|
815
|
-
|
|
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 =>
|
|
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 (
|
|
923
|
+
if (routeChoice === null || routeChoice === 'cancel')
|
|
901
924
|
return null;
|
|
902
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
1055
|
+
if (routeChoice === 'nsec') {
|
|
1033
1056
|
const signer = await Promise.race([runNsecFlow(refs, resolved), aborted]);
|
|
1034
1057
|
if (userAborted)
|
|
1035
1058
|
return null;
|