signet-login 0.1.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/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/callback.d.ts +27 -0
- package/dist/callback.js +46 -0
- package/dist/modal.d.ts +13 -0
- package/dist/modal.js +401 -0
- package/dist/redirect.d.ts +85 -0
- package/dist/redirect.js +226 -0
- package/dist/signers.d.ts +81 -0
- package/dist/signers.js +128 -0
- package/dist/signet-login.d.ts +87 -0
- package/dist/signet-login.iife.js +77 -0
- package/dist/signet-login.js +265 -0
- package/dist/storage.d.ts +38 -0
- package/dist/storage.js +159 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +35 -0
- package/dist/verify.d.ts +43 -0
- package/dist/verify.js +117 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 forgesworn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# signet-login
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sponsors/TheCryptoDonkey)
|
|
4
|
+
|
|
5
|
+
**Sign in with Signet** for Nostr-aware websites. One picker, three backends:
|
|
6
|
+
|
|
7
|
+
- **Browser extension** (NIP-07 — bark, Alby, nos2x, Flamingo, …)
|
|
8
|
+
- **Sign in with Signet** (cross-device QR via NIP-17 gift-wrap)
|
|
9
|
+
- **Paste bunker URI** (NIP-46 remote signer — Heartwood, nsecBunker, Amber)
|
|
10
|
+
|
|
11
|
+
Returns a unified `SignetSigner` your code can use to sign Nostr events going forward.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install signet-login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or drop it in via CDN:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script src="https://cdn.signet.forgesworn.dev/signet-login.iife.js"></script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The IIFE bundle additively extends `window.Signet` — it coexists with `signet-verify` on the same page in either load order.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<button id="login">Sign in</button>
|
|
31
|
+
|
|
32
|
+
<script src="https://cdn.signet.forgesworn.dev/signet-login.iife.js"></script>
|
|
33
|
+
<script>
|
|
34
|
+
document.getElementById('login').addEventListener('click', async () => {
|
|
35
|
+
const session = await Signet.login({ appName: 'My Game' });
|
|
36
|
+
if (!session) return; // user cancelled
|
|
37
|
+
|
|
38
|
+
// Sign a Nostr event with the user's chosen signer:
|
|
39
|
+
const signed = await session.signer.signEvent({
|
|
40
|
+
kind: 30762,
|
|
41
|
+
content: '',
|
|
42
|
+
tags: [
|
|
43
|
+
['game', 'my-game'],
|
|
44
|
+
['score', '12350'],
|
|
45
|
+
['p', session.pubkey],
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
console.log('signed:', signed);
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
### `Signet.login(options)`
|
|
56
|
+
|
|
57
|
+
Show the picker, return a `SignetSession` on success or `null` on cancel/timeout.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
interface LoginOptions {
|
|
61
|
+
appName: string; // shown in modal
|
|
62
|
+
challenge?: string; // 64 hex; auto if omitted
|
|
63
|
+
preferredMethod?: 'nip07' | 'redirect' | 'bunker'; // skip the picker
|
|
64
|
+
relayUrl?: string; // default wss://relay.damus.io
|
|
65
|
+
theme?: 'light' | 'dark' | 'auto'; // default 'auto'
|
|
66
|
+
timeout?: number; // default 120_000ms; clamped to [5k, 600k]
|
|
67
|
+
signetAppOrigin?: string; // default https://mysignet.app
|
|
68
|
+
redirectCallback?: string; // for same-device redirect (future)
|
|
69
|
+
persist?: boolean; // default true (localStorage)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface SignetSession {
|
|
73
|
+
pubkey: string; // hex
|
|
74
|
+
method: 'nip07' | 'redirect' | 'bunker';
|
|
75
|
+
signer: SignetSigner;
|
|
76
|
+
authEvent: SignetAuthEvent; // signed kind-21236 challenge proof
|
|
77
|
+
expiresAt?: number;
|
|
78
|
+
displayName?: string;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `Signet.restoreSession(opts?)`
|
|
83
|
+
|
|
84
|
+
Restore a session from localStorage. For bunker sessions this attempts to reconnect to the stored bunker. Returns `null` if no session is stored, the session is malformed, or reconnection fails.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const session = await Signet.restoreSession();
|
|
88
|
+
if (session?.signer.capabilities.canSignEvents) {
|
|
89
|
+
// we have ongoing signing capability
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `Signet.logout(currentSession?)`
|
|
94
|
+
|
|
95
|
+
Clear stored session and close the active signer.
|
|
96
|
+
|
|
97
|
+
### `Signet.handleCallback(opts?)`
|
|
98
|
+
|
|
99
|
+
Run on your callback page when using the same-device redirect flow. Parses URL params and posts them to `window.opener` (if popup-opened), then closes the popup.
|
|
100
|
+
|
|
101
|
+
## The three signers
|
|
102
|
+
|
|
103
|
+
All three implement `SignetSigner`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
interface SignetSigner {
|
|
107
|
+
readonly pubkey: string;
|
|
108
|
+
readonly method: 'nip07' | 'redirect' | 'bunker';
|
|
109
|
+
readonly capabilities: { canSignEvents: boolean; hasNip44: boolean };
|
|
110
|
+
signEvent(template: EventTemplate): Promise<NostrEvent>;
|
|
111
|
+
nip44?: { encrypt, decrypt };
|
|
112
|
+
close(): Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Signer | `canSignEvents` | Source |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `Nip07Signer` | true | `window.nostr` (any NIP-07 extension) |
|
|
119
|
+
| `BunkerSignerImpl` | true | `nostr-tools` BunkerSigner over NIP-46 relay |
|
|
120
|
+
| `EphemeralSigner` | **false** | Auth-only — redirect returned only `authEvent` |
|
|
121
|
+
|
|
122
|
+
`EphemeralSigner` exists because the v0.1 redirect flow returns a single signed challenge but no ongoing-signing channel. Use `signer.capabilities.canSignEvents` to gate UI:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
if (session.signer.capabilities.canSignEvents) {
|
|
126
|
+
enableLeaderboardPublish();
|
|
127
|
+
} else {
|
|
128
|
+
promptUserToInstallExtensionOrPasteBunkerURI();
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A future Option-B upgrade to signet-app will spawn a session-bunker per origin during the redirect approval, at which point redirect sessions will be full signers transparently. The SDK API does not change.
|
|
133
|
+
|
|
134
|
+
## Server-side verification
|
|
135
|
+
|
|
136
|
+
The client sends `session.authEvent` to your server. Verify it before granting any privileges or paying out sats:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { verifyLogin } from 'signet-login/verify';
|
|
140
|
+
|
|
141
|
+
const result = verifyLogin(authEvent, {
|
|
142
|
+
expectedChallenge: theChallengeYouIssued,
|
|
143
|
+
expectedOrigin: 'https://my-game.example',
|
|
144
|
+
expectedAppName: 'My Game', // optional
|
|
145
|
+
maxAgeSeconds: 300, // default 300
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (result.valid) {
|
|
149
|
+
// result.pubkey is the authenticated user
|
|
150
|
+
} else {
|
|
151
|
+
// result.error: 'invalid-signature' | 'challenge-mismatch' | 'too-old' | …
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The verifier checks: schnorr signature, canonical event ID, kind=21236, challenge tag match, origin tag match, optional app tag match, freshness window (5-min default + 60s skew tolerance).
|
|
156
|
+
|
|
157
|
+
## Storage
|
|
158
|
+
|
|
159
|
+
Session data is stored in localStorage under `signet:login.*`:
|
|
160
|
+
|
|
161
|
+
| Key | Purpose |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `signet:login.pubkey` | Authenticated pubkey |
|
|
164
|
+
| `signet:login.method` | `nip07` / `redirect` / `bunker` |
|
|
165
|
+
| `signet:login.authEvent` | Serialised kind-21236 auth event |
|
|
166
|
+
| `signet:login.bunkerUri` | Bunker URI for reconnect (bunker only) |
|
|
167
|
+
| `signet:login.bunkerClientSk` | Client secret key hex (bunker only) |
|
|
168
|
+
| `signet:login.expiresAt` | Optional expiry |
|
|
169
|
+
| `signet:login.displayName` | Optional persona handle |
|
|
170
|
+
|
|
171
|
+
Storage namespace is `signet:login.*` so it doesn't collide with `signet:verify.*`. `Signet.logout()` clears all login keys without touching other Signet SDKs.
|
|
172
|
+
|
|
173
|
+
## Coexistence with signet-verify
|
|
174
|
+
|
|
175
|
+
Both SDKs attach to `window.Signet` additively — load order doesn't matter:
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<script src=".../signet-verify.iife.js"></script>
|
|
179
|
+
<script src=".../signet-login.iife.js"></script>
|
|
180
|
+
|
|
181
|
+
<script>
|
|
182
|
+
// age verification
|
|
183
|
+
const ageResult = await Signet.verifyAge('18+');
|
|
184
|
+
// login
|
|
185
|
+
const session = await Signet.login({ appName: 'My App' });
|
|
186
|
+
</script>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Each SDK manages its own slice of `window.Signet` and `localStorage` namespaces.
|
|
190
|
+
|
|
191
|
+
## Bundle size
|
|
192
|
+
|
|
193
|
+
Approx **48.5 KB gzipped** (135 KB unminified). The bulk is `nostr-tools` `BunkerSigner` for NIP-46 + `signet-verify` for the cross-device QR primitive. A future split-bundle could lazy-load the bunker path to halve the initial size.
|
|
194
|
+
|
|
195
|
+
## Browser support
|
|
196
|
+
|
|
197
|
+
ES2020 baseline. Tested on modern Chrome / Firefox / Safari. Requires `localStorage`, `crypto.subtle`, `WebSocket`, and the native `<dialog>` element.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm install
|
|
203
|
+
npm run build # dist/signet-login.js (ESM) + dist/signet-login.iife.js (browser)
|
|
204
|
+
npm run typecheck
|
|
205
|
+
npm test # vitest in jsdom
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Examples in `examples/`:
|
|
209
|
+
- `basic.html` — full demo with login / sign / logout / restore
|
|
210
|
+
- `callback.html` — redirect-back receiver page
|
|
211
|
+
|
|
212
|
+
Build the IIFE bundle first, then serve the repo root with any static server and open `examples/basic.html`.
|
|
213
|
+
|
|
214
|
+
## Out of scope
|
|
215
|
+
|
|
216
|
+
| Excluded | Where it lives |
|
|
217
|
+
|---|---|
|
|
218
|
+
| Age verification | `signet-verify` |
|
|
219
|
+
| Per-game persona derivation | Heartwood RPC (reserved scope) |
|
|
220
|
+
| Sign-time policy clauses | Reserved (G34 NLnet Jun) |
|
|
221
|
+
| Generating bunker URIs | Heartwood / bark |
|
|
222
|
+
| Lightning, payments | Out of scope |
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
|
227
|
+
|
|
228
|
+
## Related
|
|
229
|
+
|
|
230
|
+
- [signet](https://github.com/forgesworn/signet) — protocol, specs, docs
|
|
231
|
+
- [signet-protocol](https://www.npmjs.com/package/signet-protocol) — npm primitives
|
|
232
|
+
- [signet-verify](https://github.com/forgesworn/signet-verify) — age verification + cross-device auth primitives
|
|
233
|
+
- [bark](https://github.com/forgesworn/bark) — NIP-07 browser extension that signs via NIP-46 to Heartwood
|
|
234
|
+
- [Heartwood](https://github.com/forgesworn/heartwood) — self-hosted signing appliance
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback-page helper.
|
|
3
|
+
*
|
|
4
|
+
* This is for consumers who want to support same-device redirect (signet-app
|
|
5
|
+
* redirects back to their callback page after signing). The cross-device QR
|
|
6
|
+
* path used by `Signet.login()` does NOT need this — it returns directly via
|
|
7
|
+
* a NIP-17 gift-wrapped relay event.
|
|
8
|
+
*
|
|
9
|
+
* v0.1 limitation: full same-device redirect support requires reconstruction
|
|
10
|
+
* of the signed kind-21236 event from URL params. signet-app's exact redirect-
|
|
11
|
+
* back parameter shape will determine this implementation. Until that's pinned
|
|
12
|
+
* down, this helper just relays the raw URL params back to the opener via
|
|
13
|
+
* postMessage and lets the consumer decide what to do.
|
|
14
|
+
*/
|
|
15
|
+
export interface CallbackResult {
|
|
16
|
+
/** Raw URL parameters from the redirect-back. */
|
|
17
|
+
params: Record<string, string>;
|
|
18
|
+
/** True if this page was opened as a popup (window.opener present). */
|
|
19
|
+
isPopup: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse the current page's URL parameters and post them to the opener (if any).
|
|
23
|
+
* Optionally close the popup.
|
|
24
|
+
*/
|
|
25
|
+
export declare function handleCallback(options?: {
|
|
26
|
+
closeAfterPost?: boolean;
|
|
27
|
+
}): CallbackResult;
|
package/dist/callback.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback-page helper.
|
|
3
|
+
*
|
|
4
|
+
* This is for consumers who want to support same-device redirect (signet-app
|
|
5
|
+
* redirects back to their callback page after signing). The cross-device QR
|
|
6
|
+
* path used by `Signet.login()` does NOT need this — it returns directly via
|
|
7
|
+
* a NIP-17 gift-wrapped relay event.
|
|
8
|
+
*
|
|
9
|
+
* v0.1 limitation: full same-device redirect support requires reconstruction
|
|
10
|
+
* of the signed kind-21236 event from URL params. signet-app's exact redirect-
|
|
11
|
+
* back parameter shape will determine this implementation. Until that's pinned
|
|
12
|
+
* down, this helper just relays the raw URL params back to the opener via
|
|
13
|
+
* postMessage and lets the consumer decide what to do.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Parse the current page's URL parameters and post them to the opener (if any).
|
|
17
|
+
* Optionally close the popup.
|
|
18
|
+
*/
|
|
19
|
+
export function handleCallback(options) {
|
|
20
|
+
const params = {};
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
const search = new URLSearchParams(window.location.search);
|
|
23
|
+
search.forEach((value, key) => {
|
|
24
|
+
params[key] = value;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const isPopup = typeof window !== 'undefined' && !!window.opener && window.opener !== window;
|
|
28
|
+
if (isPopup) {
|
|
29
|
+
try {
|
|
30
|
+
window.opener.postMessage({ type: 'signet-login-callback', params },
|
|
31
|
+
// Restrict target origin to opener's origin if known; fall back to '*' so
|
|
32
|
+
// cross-origin popups still deliver. Consumers must validate origin.
|
|
33
|
+
'*');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// postMessage failed — ignore
|
|
37
|
+
}
|
|
38
|
+
if (options?.closeAfterPost ?? true) {
|
|
39
|
+
try {
|
|
40
|
+
window.close();
|
|
41
|
+
}
|
|
42
|
+
catch { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { params, isPopup };
|
|
46
|
+
}
|
package/dist/modal.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The login modal — picker → method-specific UI → resolved session.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors signet-verify's <dialog>-based pattern: native focus trap,
|
|
5
|
+
* top-layer placement, theme-aware colours, no third-party UI deps.
|
|
6
|
+
*/
|
|
7
|
+
import type { LoginOptions, SignetSession } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Entry point — show the modal, route to the chosen method, return a session.
|
|
10
|
+
*
|
|
11
|
+
* Returns null when the user cancels or the flow times out.
|
|
12
|
+
*/
|
|
13
|
+
export declare function showLoginModal(opts: LoginOptions): Promise<SignetSession | null>;
|