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.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Signet Login SDK — Sign in with Signet for Nostr-aware websites.
3
+ *
4
+ * ESM / bundler usage:
5
+ * import { login, restoreSession, logout } from 'signet-login';
6
+ *
7
+ * Script-tag / IIFE usage (additively extends `window.Signet`):
8
+ * <script src="https://cdn.signet.forgesworn.dev/signet-login.iife.js"></script>
9
+ * <script>
10
+ * const session = await Signet.login({ appName: 'Asteroid Sats' });
11
+ * </script>
12
+ *
13
+ * The IIFE bundle does NOT overwrite `window.Signet` — it augments whatever is
14
+ * already there (so `signet-verify.iife.js` and `signet-login.iife.js` coexist
15
+ * in either load order on the same page).
16
+ */
17
+ import { DEFAULTS } from './types.js';
18
+ import { showLoginModal } from './modal.js';
19
+ import { saveSession, loadSession, clearSession, bytesToHexLocal, hexToBytesLocal } from './storage.js';
20
+ import { hasNip07, createNip07Signer, createBunkerSigner, EphemeralSigner, } from './signers.js';
21
+ import { handleCallback as handlePopupCallback } from './callback.js';
22
+ import { consumeCallback, startRedirect } from './redirect.js';
23
+ // ── Public API ────────────────────────────────────────────────────────────────
24
+ /**
25
+ * Show the login picker and resolve to a SignetSession on success, or null on
26
+ * cancel / timeout.
27
+ *
28
+ * When `mode: 'redirect'` is set, the picker is skipped entirely — the current
29
+ * tab navigates to signet-app and this promise NEVER resolves in this tab.
30
+ * Callers should treat the returned promise as "fire and forget" in that case
31
+ * and call `Signet.handleCallback()` on the next page load to receive the
32
+ * session. The other login methods (NIP-07, bunker) don't use redirect at all
33
+ * and are unaffected by this option.
34
+ */
35
+ export async function login(opts) {
36
+ // Redirect mode short-circuits the picker — the user is going to signet-app.
37
+ // We don't gate on preferredMethod here: redirect mode implies the consumer
38
+ // wants the Sign in with Signet method, which is the only method that uses
39
+ // navigation. (NIP-07 and bunker resolve in-tab regardless of mode.)
40
+ if (opts.mode === 'redirect') {
41
+ if (typeof window === 'undefined') {
42
+ throw new Error('signet-login: redirect mode requires a browser environment');
43
+ }
44
+ const challenge = opts.challenge ?? generateChallenge();
45
+ if (!/^[0-9a-f]{64}$/i.test(challenge))
46
+ throw new Error('challenge-must-be-64-hex');
47
+ if (!opts.appName || opts.appName.length === 0)
48
+ throw new Error('appName-required');
49
+ if (opts.appName.length > 64)
50
+ throw new Error('appName-too-long');
51
+ return startRedirect({
52
+ appName: opts.appName,
53
+ challenge: challenge.toLowerCase(),
54
+ origin: window.location.origin,
55
+ signetAppOrigin: opts.signetAppOrigin ?? DEFAULTS.signetAppOrigin,
56
+ ...(opts.redirectCallback !== undefined ? { redirectCallback: opts.redirectCallback } : {}),
57
+ });
58
+ }
59
+ const session = await showLoginModal(opts);
60
+ if (!session)
61
+ return null;
62
+ if (opts.persist !== false) {
63
+ persistSession(session);
64
+ }
65
+ return session;
66
+ }
67
+ /**
68
+ * Generate a 64-hex random challenge. Mirrors the modal's helper but lives at
69
+ * the module level so the redirect path can call it without pulling the modal
70
+ * into the bundle when only `mode: 'redirect'` is used.
71
+ */
72
+ function generateChallenge() {
73
+ const bytes = new Uint8Array(32);
74
+ crypto.getRandomValues(bytes);
75
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
76
+ }
77
+ /**
78
+ * Try to restore a session from localStorage. Returns null if no session is
79
+ * stored or it's malformed/expired.
80
+ *
81
+ * For bunker sessions, attempts to reconnect using the stored URI + client SK.
82
+ * If the bunker is unreachable, returns null and clears the stored session.
83
+ */
84
+ export async function restoreSession(opts) {
85
+ const stored = loadSession();
86
+ if (!stored)
87
+ return null;
88
+ let authEvent;
89
+ try {
90
+ authEvent = JSON.parse(stored.authEventJson);
91
+ }
92
+ catch {
93
+ clearSession();
94
+ return null;
95
+ }
96
+ if (stored.method === 'nip07') {
97
+ if (!hasNip07()) {
98
+ // Extension was uninstalled — return ephemeral identity-only session
99
+ const ephemeral = new EphemeralSigner(stored.pubkey, authEvent);
100
+ return {
101
+ pubkey: stored.pubkey,
102
+ method: 'redirect', // downgrade — caller sees "no signing"
103
+ signer: ephemeral,
104
+ authEvent,
105
+ };
106
+ }
107
+ try {
108
+ const signer = await createNip07Signer();
109
+ // Verify the same pubkey is selected — extension may have switched accounts
110
+ if (signer.pubkey !== stored.pubkey) {
111
+ clearSession();
112
+ return null;
113
+ }
114
+ return {
115
+ pubkey: stored.pubkey,
116
+ method: 'nip07',
117
+ signer,
118
+ authEvent,
119
+ };
120
+ }
121
+ catch {
122
+ clearSession();
123
+ return null;
124
+ }
125
+ }
126
+ if (stored.method === 'bunker') {
127
+ if (opts?.reconnectBunker === false) {
128
+ const ephemeral = new EphemeralSigner(stored.pubkey, authEvent);
129
+ return {
130
+ pubkey: stored.pubkey,
131
+ method: 'redirect',
132
+ signer: ephemeral,
133
+ authEvent,
134
+ };
135
+ }
136
+ if (!stored.bunkerUri || !stored.bunkerClientSkHex) {
137
+ clearSession();
138
+ return null;
139
+ }
140
+ try {
141
+ const sk = hexToBytesLocal(stored.bunkerClientSkHex);
142
+ const signer = await createBunkerSigner({ uri: stored.bunkerUri, clientSecretKey: sk });
143
+ if (signer.pubkey !== stored.pubkey) {
144
+ await signer.close();
145
+ clearSession();
146
+ return null;
147
+ }
148
+ return {
149
+ pubkey: stored.pubkey,
150
+ method: 'bunker',
151
+ signer,
152
+ authEvent,
153
+ };
154
+ }
155
+ catch {
156
+ clearSession();
157
+ return null;
158
+ }
159
+ }
160
+ // method === 'redirect' — restore as ephemeral (auth-only)
161
+ const ephemeral = new EphemeralSigner(stored.pubkey, authEvent);
162
+ const session = {
163
+ pubkey: stored.pubkey,
164
+ method: 'redirect',
165
+ signer: ephemeral,
166
+ authEvent,
167
+ };
168
+ if (stored.displayName)
169
+ session.displayName = stored.displayName;
170
+ return session;
171
+ }
172
+ /**
173
+ * Popup-style callback receiver. Use on the page that signet-app redirects
174
+ * a popup to. Parses URL params and posts them to `window.opener`, then
175
+ * closes the popup. Returns the raw params for non-popup contexts.
176
+ *
177
+ * For the same-tab redirect flow (`mode: 'redirect'` on `login()`), use
178
+ * `Signet.handleRedirectCallback()` instead — that one validates against the
179
+ * persisted pending state and returns a fully-formed `SignetSession`.
180
+ */
181
+ export const handleCallback = handlePopupCallback;
182
+ /**
183
+ * Same-tab redirect callback receiver. Call once on app boot, before
184
+ * `restoreSession()`, to consume an incoming `?pubkey&signature&eventId`
185
+ * payload from signet-app.
186
+ *
187
+ * Behaviour:
188
+ *
189
+ * - `'session'`: validates the round-trip against the pending state saved
190
+ * by `login({ mode: 'redirect' })`, builds and persists a SignetSession
191
+ * (so `restoreSession()` finds it next time), and strips the auth params
192
+ * from the URL via `history.replaceState`. The returned session uses an
193
+ * `EphemeralSigner` — `signer.capabilities.canSignEvents` is false. Pair
194
+ * with NIP-07 / bunker if you need ongoing signing.
195
+ *
196
+ * - `'denied'`: signet-app reported the user rejected the request.
197
+ *
198
+ * - `'no-callback'`: no auth params on the URL — the typical case on most
199
+ * page loads. Idempotent: a second call after success also returns this.
200
+ *
201
+ * - `'invalid'`: params present but failed validation. `reason` is a
202
+ * machine-readable token (`origin-mismatch`, `pending-stale`,
203
+ * `pubkey-malformed`, …). Pending state is cleared either way so a stale
204
+ * URL can't poison the next attempt.
205
+ *
206
+ * The returned shape is intentionally tagged so consumers can distinguish
207
+ * "user denied" from "no callback" without inspecting null. Persistence on
208
+ * success uses the same storage layer as relay-mode sessions, so downstream
209
+ * code that consumes `restoreSession()` doesn't need to care which path
210
+ * authenticated the user.
211
+ */
212
+ export async function handleRedirectCallback() {
213
+ const result = consumeCallback();
214
+ if (result.kind === 'session') {
215
+ persistSession(result.session);
216
+ }
217
+ return result;
218
+ }
219
+ /**
220
+ * Clear the stored session and close the active signer.
221
+ */
222
+ export async function logout(currentSession) {
223
+ if (currentSession) {
224
+ try {
225
+ await currentSession.signer.close();
226
+ }
227
+ catch { /* ignore */ }
228
+ }
229
+ clearSession();
230
+ }
231
+ // ── Persistence helpers (internal) ────────────────────────────────────────────
232
+ function persistSession(session) {
233
+ const payload = {
234
+ pubkey: session.pubkey,
235
+ method: session.method,
236
+ authEventJson: JSON.stringify(session.authEvent),
237
+ };
238
+ if (session.method === 'bunker') {
239
+ // Cast: in bunker mode, the signer is a BunkerSignerImpl with bunkerUri + clientSecretKey
240
+ const bunkerSigner = session.signer;
241
+ if (bunkerSigner.bunkerUri && bunkerSigner.clientSecretKey instanceof Uint8Array) {
242
+ payload.bunkerUri = bunkerSigner.bunkerUri;
243
+ payload.bunkerClientSkHex = bytesToHexLocal(bunkerSigner.clientSecretKey);
244
+ }
245
+ }
246
+ if (session.expiresAt !== undefined)
247
+ payload.expiresAt = session.expiresAt;
248
+ if (session.displayName !== undefined)
249
+ payload.displayName = session.displayName;
250
+ saveSession(payload);
251
+ }
252
+ // ── Auto-attach to window.Signet (additive) ───────────────────────────────────
253
+ if (typeof window !== 'undefined') {
254
+ // Never overwrite — additive only. Coexists with signet-verify on the same page.
255
+ const existing = window.Signet;
256
+ const SignetGlobal = existing ?? {};
257
+ Object.assign(SignetGlobal, {
258
+ login,
259
+ restoreSession,
260
+ logout,
261
+ handleCallback,
262
+ handleRedirectCallback,
263
+ });
264
+ window.Signet = SignetGlobal;
265
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * localStorage persistence for signet-login sessions.
3
+ *
4
+ * Storage keys are namespaced under `signet:login.*` so they don't collide
5
+ * with `signet:verify.*` or any future Signet SDK.
6
+ */
7
+ import type { LoginMethod, PendingRedirect } from './types.js';
8
+ /** Raw shape of a persisted session — flat string fields, JSON for the auth event. */
9
+ export interface PersistedSession {
10
+ pubkey: string;
11
+ method: LoginMethod;
12
+ authEventJson: string;
13
+ bunkerUri?: string;
14
+ bunkerClientSkHex?: string;
15
+ expiresAt?: number;
16
+ displayName?: string;
17
+ }
18
+ /** Save a session. Caller must serialise authEvent to JSON. */
19
+ export declare function saveSession(s: PersistedSession): void;
20
+ /** Load a session if one is present. Returns null if no session or it's malformed. */
21
+ export declare function loadSession(): PersistedSession | null;
22
+ /** Clear all signet-login keys. Does not touch other Signet SDK storage. */
23
+ export declare function clearSession(): void;
24
+ /**
25
+ * Persist the in-flight redirect state. Called immediately before navigating
26
+ * to signet-app so the callback consumer can validate the round-trip.
27
+ *
28
+ * Stored as a single JSON blob under `signet:login.pendingRedirect`. We keep
29
+ * it in localStorage rather than sessionStorage because some browsers (older
30
+ * iOS Safari especially) wipe sessionStorage on cross-origin navigation.
31
+ */
32
+ export declare function savePendingRedirect(p: PendingRedirect): void;
33
+ /** Load and shape-validate the pending redirect. Returns null if absent or malformed. */
34
+ export declare function loadPendingRedirect(): PendingRedirect | null;
35
+ /** Clear the pending-redirect record. Safe to call when none exists. */
36
+ export declare function clearPendingRedirect(): void;
37
+ export declare function bytesToHexLocal(bytes: Uint8Array): string;
38
+ export declare function hexToBytesLocal(hex: string): Uint8Array;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * localStorage persistence for signet-login sessions.
3
+ *
4
+ * Storage keys are namespaced under `signet:login.*` so they don't collide
5
+ * with `signet:verify.*` or any future Signet SDK.
6
+ */
7
+ import { STORAGE_KEYS } from './types.js';
8
+ function safeGet(key) {
9
+ try {
10
+ return typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function safeSet(key, value) {
17
+ try {
18
+ if (typeof localStorage !== 'undefined')
19
+ localStorage.setItem(key, value);
20
+ }
21
+ catch {
22
+ // localStorage unavailable (private mode, quota, etc.) — silently skip
23
+ }
24
+ }
25
+ function safeRemove(key) {
26
+ try {
27
+ if (typeof localStorage !== 'undefined')
28
+ localStorage.removeItem(key);
29
+ }
30
+ catch {
31
+ // ignore
32
+ }
33
+ }
34
+ /** Save a session. Caller must serialise authEvent to JSON. */
35
+ export function saveSession(s) {
36
+ safeSet(STORAGE_KEYS.pubkey, s.pubkey);
37
+ safeSet(STORAGE_KEYS.method, s.method);
38
+ safeSet(STORAGE_KEYS.authEvent, s.authEventJson);
39
+ if (s.bunkerUri !== undefined)
40
+ safeSet(STORAGE_KEYS.bunkerUri, s.bunkerUri);
41
+ if (s.bunkerClientSkHex !== undefined)
42
+ safeSet(STORAGE_KEYS.bunkerClientSk, s.bunkerClientSkHex);
43
+ if (s.expiresAt !== undefined)
44
+ safeSet(STORAGE_KEYS.expiresAt, String(s.expiresAt));
45
+ if (s.displayName !== undefined)
46
+ safeSet(STORAGE_KEYS.displayName, s.displayName);
47
+ }
48
+ /** Load a session if one is present. Returns null if no session or it's malformed. */
49
+ export function loadSession() {
50
+ const pubkey = safeGet(STORAGE_KEYS.pubkey);
51
+ const method = safeGet(STORAGE_KEYS.method);
52
+ const authEventJson = safeGet(STORAGE_KEYS.authEvent);
53
+ if (!pubkey || !method || !authEventJson)
54
+ return null;
55
+ if (!/^[0-9a-f]{64}$/i.test(pubkey))
56
+ return null;
57
+ if (method !== 'nip07' && method !== 'redirect' && method !== 'bunker')
58
+ return null;
59
+ // Sanity-parse the auth event before returning
60
+ let authEvent;
61
+ try {
62
+ authEvent = JSON.parse(authEventJson);
63
+ if (typeof authEvent !== 'object' || authEvent === null)
64
+ return null;
65
+ if (authEvent.pubkey !== pubkey)
66
+ return null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ const expiresAtRaw = safeGet(STORAGE_KEYS.expiresAt);
72
+ const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined;
73
+ if (expiresAt !== undefined && Number.isFinite(expiresAt) && Date.now() > expiresAt) {
74
+ // Session expired — drop it
75
+ clearSession();
76
+ return null;
77
+ }
78
+ const result = { pubkey, method, authEventJson };
79
+ const bunkerUri = safeGet(STORAGE_KEYS.bunkerUri);
80
+ const bunkerClientSkHex = safeGet(STORAGE_KEYS.bunkerClientSk);
81
+ const displayName = safeGet(STORAGE_KEYS.displayName);
82
+ if (bunkerUri)
83
+ result.bunkerUri = bunkerUri;
84
+ if (bunkerClientSkHex)
85
+ result.bunkerClientSkHex = bunkerClientSkHex;
86
+ if (expiresAt !== undefined && Number.isFinite(expiresAt))
87
+ result.expiresAt = expiresAt;
88
+ if (displayName)
89
+ result.displayName = displayName;
90
+ return result;
91
+ }
92
+ /** Clear all signet-login keys. Does not touch other Signet SDK storage. */
93
+ export function clearSession() {
94
+ safeRemove(STORAGE_KEYS.pubkey);
95
+ safeRemove(STORAGE_KEYS.method);
96
+ safeRemove(STORAGE_KEYS.authEvent);
97
+ safeRemove(STORAGE_KEYS.bunkerUri);
98
+ safeRemove(STORAGE_KEYS.bunkerClientSk);
99
+ safeRemove(STORAGE_KEYS.expiresAt);
100
+ safeRemove(STORAGE_KEYS.displayName);
101
+ }
102
+ // ── Pending-redirect persistence ──────────────────────────────────────────────
103
+ /**
104
+ * Persist the in-flight redirect state. Called immediately before navigating
105
+ * to signet-app so the callback consumer can validate the round-trip.
106
+ *
107
+ * Stored as a single JSON blob under `signet:login.pendingRedirect`. We keep
108
+ * it in localStorage rather than sessionStorage because some browsers (older
109
+ * iOS Safari especially) wipe sessionStorage on cross-origin navigation.
110
+ */
111
+ export function savePendingRedirect(p) {
112
+ safeSet(STORAGE_KEYS.pendingRedirect, JSON.stringify(p));
113
+ }
114
+ /** Load and shape-validate the pending redirect. Returns null if absent or malformed. */
115
+ export function loadPendingRedirect() {
116
+ const raw = safeGet(STORAGE_KEYS.pendingRedirect);
117
+ if (!raw)
118
+ return null;
119
+ try {
120
+ const parsed = JSON.parse(raw);
121
+ const challenge = parsed.challenge;
122
+ const origin = parsed.origin;
123
+ const appName = parsed.appName;
124
+ const createdAt = parsed.createdAt;
125
+ if (typeof challenge !== 'string' || !/^[0-9a-f]{64}$/i.test(challenge))
126
+ return null;
127
+ if (typeof origin !== 'string' || origin.length === 0)
128
+ return null;
129
+ if (typeof appName !== 'string' || appName.length === 0)
130
+ return null;
131
+ if (typeof createdAt !== 'number' || !Number.isFinite(createdAt))
132
+ return null;
133
+ return { challenge, origin, appName, createdAt };
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
139
+ /** Clear the pending-redirect record. Safe to call when none exists. */
140
+ export function clearPendingRedirect() {
141
+ safeRemove(STORAGE_KEYS.pendingRedirect);
142
+ }
143
+ // ── Hex helpers (avoid pulling in @noble for two functions) ───────────────────
144
+ export function bytesToHexLocal(bytes) {
145
+ let out = '';
146
+ for (let i = 0; i < bytes.length; i++) {
147
+ out += bytes[i].toString(16).padStart(2, '0');
148
+ }
149
+ return out;
150
+ }
151
+ export function hexToBytesLocal(hex) {
152
+ if (hex.length % 2 !== 0)
153
+ throw new Error('odd-hex-length');
154
+ const out = new Uint8Array(hex.length / 2);
155
+ for (let i = 0; i < out.length; i++) {
156
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
157
+ }
158
+ return out;
159
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Public types for signet-login.
3
+ *
4
+ * The SDK exposes a single SignetSigner interface that wraps three backends
5
+ * (NIP-07 extension, NIP-46 bunker, ephemeral redirect-only). Consumers code
6
+ * against the interface; the SDK picks the implementation based on user choice.
7
+ */
8
+ /** A signed Nostr event. */
9
+ export interface NostrEvent {
10
+ id: string;
11
+ pubkey: string;
12
+ kind: number;
13
+ created_at: number;
14
+ tags: string[][];
15
+ content: string;
16
+ sig: string;
17
+ }
18
+ /** An unsigned event template ready for signing. */
19
+ export interface EventTemplate {
20
+ kind: number;
21
+ created_at?: number;
22
+ tags?: string[][];
23
+ content: string;
24
+ }
25
+ /** Login method actually used to authenticate. */
26
+ export type LoginMethod = 'nip07' | 'redirect' | 'bunker';
27
+ /** Capability flags exposed by a signer. */
28
+ export interface SignerCapabilities {
29
+ /** True if the signer can sign arbitrary events going forward. False for redirect-auth-only sessions. */
30
+ canSignEvents: boolean;
31
+ /** True if NIP-44 encrypt/decrypt is available. */
32
+ hasNip44: boolean;
33
+ }
34
+ /** Unified signer interface — three backends, one shape. */
35
+ export interface SignetSigner {
36
+ readonly pubkey: string;
37
+ readonly method: LoginMethod;
38
+ readonly capabilities: SignerCapabilities;
39
+ signEvent(template: EventTemplate): Promise<NostrEvent>;
40
+ nip44?: {
41
+ encrypt(peerPubkey: string, plaintext: string): Promise<string>;
42
+ decrypt(peerPubkey: string, ciphertext: string): Promise<string>;
43
+ };
44
+ close(): Promise<void>;
45
+ }
46
+ /** A signed kind-21236 auth event proving pubkey ownership. */
47
+ export interface SignetAuthEvent extends NostrEvent {
48
+ kind: 21236;
49
+ }
50
+ /** An authenticated session — pubkey + signer + the signed challenge proof. */
51
+ export interface SignetSession {
52
+ pubkey: string;
53
+ method: LoginMethod;
54
+ signer: SignetSigner;
55
+ /** The signed challenge event — proves identity, useful for server-side verification. */
56
+ authEvent: SignetAuthEvent;
57
+ /** Unix-ms expiry, if the session can expire (bunker tokens). Absent = session does not expire. */
58
+ expiresAt?: number;
59
+ /** Optional display name the user shared at approval (sanitised). */
60
+ displayName?: string;
61
+ }
62
+ /**
63
+ * Delivery mode for the "Sign in with Signet" method.
64
+ *
65
+ * - 'relay' (default): the modal shows a QR / link, signet-app gift-wraps the
66
+ * signed auth event back via a Nostr relay. The current tab stays put. Best
67
+ * for desktop where users have a phone alongside.
68
+ *
69
+ * - 'redirect': the current tab navigates to signet-app, the user signs in
70
+ * there, signet-app redirects the same tab back to `redirectCallback` with
71
+ * auth params in the query string. The consumer must call
72
+ * `Signet.handleCallback()` on boot to consume the params and resolve a
73
+ * session. Best for mobile / single-device flows.
74
+ *
75
+ * Only affects the 'redirect' login method. NIP-07 and bunker are unchanged.
76
+ */
77
+ export type SignetDeliveryMode = 'relay' | 'redirect';
78
+ /** Options for Signet.login(). */
79
+ export interface LoginOptions {
80
+ /** Required. Shown in the consent UI (e.g. "Asteroid Sats"). */
81
+ appName: string;
82
+ /** Optional 64-hex challenge. Auto-generated if omitted. */
83
+ challenge?: string;
84
+ /** Skip the picker and force a specific method. */
85
+ preferredMethod?: LoginMethod;
86
+ /** Relay URL for cross-device communication. Default: wss://relay.damus.io */
87
+ relayUrl?: string;
88
+ /** Modal colour scheme. Default: 'auto'. */
89
+ theme?: 'light' | 'dark' | 'auto';
90
+ /** Timeout in milliseconds. Default: 120_000. Clamped to [5_000, 600_000]. */
91
+ timeout?: number;
92
+ /**
93
+ * Origin of the Signet app. Default: https://mysignet.app
94
+ * Override for local development against your own signet-app instance.
95
+ */
96
+ signetAppOrigin?: string;
97
+ /**
98
+ * Callback URL used by the same-device redirect path. Must be same-origin
99
+ * as the calling page. Defaults to `${origin}/`. Only used when `mode` is
100
+ * 'redirect'.
101
+ */
102
+ redirectCallback?: string;
103
+ /**
104
+ * Delivery mode for the Sign in with Signet method. See `SignetDeliveryMode`.
105
+ * Default: 'relay'.
106
+ *
107
+ * In 'redirect' mode `Signet.login()` navigates the current tab away and
108
+ * never resolves in this tab — the returned promise is abandoned. Wire up
109
+ * `Signet.handleCallback()` on boot to receive the session on return.
110
+ */
111
+ mode?: SignetDeliveryMode;
112
+ /** Persist the session to localStorage. Default: true. */
113
+ persist?: boolean;
114
+ }
115
+ /**
116
+ * State persisted to localStorage between starting a redirect and consuming
117
+ * the callback. Used by `consumeCallback()` to validate the round-trip and
118
+ * reconstruct the kind-21236 auth event.
119
+ */
120
+ export interface PendingRedirect {
121
+ /** 64-hex challenge issued at login start — must match the auth event tag. */
122
+ challenge: string;
123
+ /** Origin that initiated the login — must match `window.location.origin` on return. */
124
+ origin: string;
125
+ /** App name — used to reconstruct the `app` tag on the auth event. */
126
+ appName: string;
127
+ /** Unix-ms when the redirect started. Used for the freshness window. */
128
+ createdAt: number;
129
+ }
130
+ /** Options for Signet.restoreSession(). */
131
+ export interface RestoreOptions {
132
+ /** Reconnect a stored bunker session if present. Default: true. */
133
+ reconnectBunker?: boolean;
134
+ /** Default relay for bunker reconnection if URI omits it. */
135
+ defaultRelay?: string;
136
+ }
137
+ /** Default values applied when the consumer omits an option. */
138
+ export declare const DEFAULTS: {
139
+ relayUrl: string;
140
+ signetAppOrigin: string;
141
+ timeout: number;
142
+ theme: "auto";
143
+ persist: boolean;
144
+ mode: SignetDeliveryMode;
145
+ };
146
+ /**
147
+ * Pending redirect must be consumed within this window of starting it,
148
+ * otherwise the callback is treated as stale (likely a stray bookmark or
149
+ * tab restored after a long pause). Mirrors signet-app's URL freshness
150
+ * window (5 min) so callback consumers behave consistently with the issuer.
151
+ */
152
+ export declare const PENDING_REDIRECT_TTL_MS: number;
153
+ /** Storage keys, namespaced under signet:login.* */
154
+ export declare const STORAGE_KEYS: {
155
+ pubkey: string;
156
+ method: string;
157
+ authEvent: string;
158
+ bunkerUri: string;
159
+ bunkerClientSk: string;
160
+ expiresAt: string;
161
+ displayName: string;
162
+ /** Session-storage key for in-flight redirect state. */
163
+ pendingRedirect: string;
164
+ };
package/dist/types.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Public types for signet-login.
3
+ *
4
+ * The SDK exposes a single SignetSigner interface that wraps three backends
5
+ * (NIP-07 extension, NIP-46 bunker, ephemeral redirect-only). Consumers code
6
+ * against the interface; the SDK picks the implementation based on user choice.
7
+ */
8
+ /** Default values applied when the consumer omits an option. */
9
+ export const DEFAULTS = {
10
+ relayUrl: 'wss://relay.damus.io',
11
+ signetAppOrigin: 'https://mysignet.app',
12
+ timeout: 120000,
13
+ theme: 'auto',
14
+ persist: true,
15
+ mode: 'relay',
16
+ };
17
+ /**
18
+ * Pending redirect must be consumed within this window of starting it,
19
+ * otherwise the callback is treated as stale (likely a stray bookmark or
20
+ * tab restored after a long pause). Mirrors signet-app's URL freshness
21
+ * window (5 min) so callback consumers behave consistently with the issuer.
22
+ */
23
+ export const PENDING_REDIRECT_TTL_MS = 5 * 60 * 1000;
24
+ /** Storage keys, namespaced under signet:login.* */
25
+ export const STORAGE_KEYS = {
26
+ pubkey: 'signet:login.pubkey',
27
+ method: 'signet:login.method',
28
+ authEvent: 'signet:login.authEvent',
29
+ bunkerUri: 'signet:login.bunkerUri',
30
+ bunkerClientSk: 'signet:login.bunkerClientSk',
31
+ expiresAt: 'signet:login.expiresAt',
32
+ displayName: 'signet:login.displayName',
33
+ /** Session-storage key for in-flight redirect state. */
34
+ pendingRedirect: 'signet:login.pendingRedirect',
35
+ };