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,226 @@
1
+ /**
2
+ * Same-tab redirect flow for "Sign in with Signet".
3
+ *
4
+ * Two halves:
5
+ *
6
+ * 1. `startRedirect()` — called from `Signet.login({ mode: 'redirect' })`.
7
+ * Persists pending state to localStorage, builds the signet-app auth URL
8
+ * WITHOUT relay/sessionPubkey (so signet-app falls into its
9
+ * `window.location.href = callbackUrl` path), and navigates the current
10
+ * tab. The caller's promise never resolves in this tab — the page is
11
+ * gone.
12
+ *
13
+ * 2. `consumeCallback()` — called from `Signet.handleCallback()` on boot.
14
+ * Detects auth params in `window.location.search`, validates them
15
+ * against the persisted pending state, reconstructs the kind-21236 auth
16
+ * event, persists the session via the existing storage layer, strips
17
+ * the auth params from the URL, and returns a `SignetSession`.
18
+ *
19
+ * Verification note: the reconstructed auth event has a signature that was
20
+ * produced over the original `created_at` chosen by signet-app at sign time.
21
+ * To rebuild the event hash exactly, signet-app must emit `t` (unix seconds)
22
+ * alongside pubkey/signature/eventId in the redirect URL — see the
23
+ * coordinated change in signet-protocol's `buildAuthCallbackUrl`. When `t`
24
+ * is present, the reconstructed event passes signature verification. When
25
+ * absent (older signet-app deployments), the SDK falls back to "now" and
26
+ * logs a warning — server-side strict verification will fail until the
27
+ * issuer is upgraded.
28
+ */
29
+ import { DEFAULTS, PENDING_REDIRECT_TTL_MS } from './types.js';
30
+ import { clearPendingRedirect, loadPendingRedirect, savePendingRedirect, } from './storage.js';
31
+ import { EphemeralSigner } from './signers.js';
32
+ /** Hex regexes — kept local to avoid pulling in @noble for two patterns. */
33
+ const HEX_64 = /^[0-9a-f]{64}$/i;
34
+ const HEX_128 = /^[0-9a-f]{128}$/i;
35
+ /**
36
+ * Build the signet-app auth URL for redirect mode. Deliberately omits `relay`
37
+ * and `sessionPubkey` so signet-app's `isRelayMode` check (App.tsx) returns
38
+ * false and the redirect path runs.
39
+ */
40
+ export function buildRedirectAuthUrl(opts) {
41
+ const callback = opts.redirectCallback ?? `${opts.origin}/`;
42
+ const params = new URLSearchParams({
43
+ auth: '1',
44
+ challenge: opts.challenge,
45
+ origin: opts.origin,
46
+ name: opts.appName,
47
+ callback,
48
+ t: String(Math.floor(Date.now() / 1000)),
49
+ });
50
+ return `${opts.signetAppOrigin}/?${params.toString()}`;
51
+ }
52
+ /**
53
+ * Persist pending state and navigate. Resolves to a never-settling promise on
54
+ * success (the page navigates before it can resolve) so callers using
55
+ * `await Signet.login()` see consistent behaviour with the relay path.
56
+ *
57
+ * Throws synchronously if the environment lacks `window` — calling redirect
58
+ * mode in non-browser code is a programming error, not something to silently
59
+ * swallow.
60
+ */
61
+ export function startRedirect(opts) {
62
+ if (typeof window === 'undefined') {
63
+ throw new Error('signet-login: redirect mode requires a browser environment');
64
+ }
65
+ const pending = {
66
+ challenge: opts.challenge,
67
+ origin: opts.origin,
68
+ appName: opts.appName,
69
+ createdAt: Date.now(),
70
+ };
71
+ savePendingRedirect(pending);
72
+ const url = buildRedirectAuthUrl(opts);
73
+ // Use assignment (not replace) so the user can hit back to abort. The
74
+ // pending record stays put; consumeCallback will GC it via the freshness
75
+ // window if they never come back.
76
+ window.location.href = url;
77
+ // Page is navigating — return a promise that never resolves. Any code
78
+ // running after `await Signet.login()` won't see a value, but the tab is
79
+ // gone before that matters.
80
+ return new Promise(() => { });
81
+ }
82
+ /**
83
+ * Strip auth-callback params from the current URL via `history.replaceState`,
84
+ * preserving anything else the consumer has on the URL. No-op when there's
85
+ * no auth-callback param present.
86
+ */
87
+ function cleanupCallbackUrl() {
88
+ if (typeof window === 'undefined')
89
+ return;
90
+ const url = new URL(window.location.href);
91
+ const removed = ['pubkey', 'npub', 'signature', 'eventId', 'error', 'warnings', 'fromNP', 'display_name', 't'];
92
+ let touched = false;
93
+ for (const key of removed) {
94
+ if (url.searchParams.has(key)) {
95
+ url.searchParams.delete(key);
96
+ touched = true;
97
+ }
98
+ }
99
+ if (!touched)
100
+ return;
101
+ const newHref = url.pathname + (url.search ? url.search : '') + url.hash;
102
+ try {
103
+ window.history.replaceState(window.history.state, document.title, newHref);
104
+ }
105
+ catch {
106
+ // history API blocked (file:// origin, sandboxed iframe, …) — leave URL alone
107
+ }
108
+ }
109
+ /**
110
+ * Detect and consume a redirect-back callback. Returns:
111
+ *
112
+ * - { kind: 'session', session } — round-trip valid; clears pending state
113
+ * and strips auth params from the URL
114
+ * - { kind: 'denied' } — signet-app sent `error=denied`
115
+ * - { kind: 'no-callback' } — no auth params in the URL; do nothing
116
+ * - { kind: 'invalid', reason } — params present but failed validation
117
+ * (pending state mismatch, stale, hex
118
+ * malformed, …). Pending state is cleared
119
+ * in this case too — a stale or attacker-
120
+ * supplied URL shouldn't poison the next
121
+ * login attempt.
122
+ *
123
+ * Idempotent: calling it twice on the same loaded page returns 'no-callback'
124
+ * the second time because the URL params have been stripped.
125
+ */
126
+ export function consumeCallback() {
127
+ if (typeof window === 'undefined')
128
+ return { kind: 'no-callback' };
129
+ const params = new URLSearchParams(window.location.search);
130
+ const error = params.get('error');
131
+ const pubkey = params.get('pubkey');
132
+ const signature = params.get('signature');
133
+ const eventId = params.get('eventId');
134
+ // No callback at all — early return without touching pending state.
135
+ if (!error && !pubkey && !signature && !eventId) {
136
+ return { kind: 'no-callback' };
137
+ }
138
+ const pending = loadPendingRedirect();
139
+ // From here on we're handling a callback — pending state must always be
140
+ // cleared on exit so a stale record can't be reused.
141
+ const finalize = (result) => {
142
+ clearPendingRedirect();
143
+ cleanupCallbackUrl();
144
+ return result;
145
+ };
146
+ if (error === 'denied') {
147
+ return finalize({ kind: 'denied' });
148
+ }
149
+ if (!pending) {
150
+ return finalize({ kind: 'invalid', reason: 'no-pending-state' });
151
+ }
152
+ // Origin sanity — protects against a callback URL fired at a different app
153
+ // (e.g. attacker emails a crafted link). Pending was issued by `origin`
154
+ // matching the calling page; on return the page must still be on that origin.
155
+ if (pending.origin !== window.location.origin) {
156
+ return finalize({ kind: 'invalid', reason: 'origin-mismatch' });
157
+ }
158
+ // Freshness — a callback hours after the user clicked Sign In is almost
159
+ // certainly a stale tab restore. Reject rather than reconstructing an
160
+ // expired auth event.
161
+ if (Date.now() - pending.createdAt > PENDING_REDIRECT_TTL_MS) {
162
+ return finalize({ kind: 'invalid', reason: 'pending-stale' });
163
+ }
164
+ if (!pubkey || !HEX_64.test(pubkey)) {
165
+ return finalize({ kind: 'invalid', reason: 'pubkey-malformed' });
166
+ }
167
+ if (!signature || !HEX_128.test(signature)) {
168
+ return finalize({ kind: 'invalid', reason: 'signature-malformed' });
169
+ }
170
+ if (!eventId || !HEX_64.test(eventId)) {
171
+ return finalize({ kind: 'invalid', reason: 'eventId-malformed' });
172
+ }
173
+ // `t` (created_at unix seconds) — emitted by signet-app for exact event
174
+ // reconstruction. Fall back to "now" with a warning when absent (older
175
+ // signet-app deployments). See module-level note.
176
+ let createdAt;
177
+ const tRaw = params.get('t');
178
+ if (tRaw && /^\d+$/.test(tRaw)) {
179
+ const t = Number(tRaw);
180
+ if (!Number.isFinite(t))
181
+ return finalize({ kind: 'invalid', reason: 't-malformed' });
182
+ createdAt = t;
183
+ }
184
+ else {
185
+ createdAt = Math.floor(Date.now() / 1000);
186
+ // Surface this in dev tools so consumers can spot upstream signet-app
187
+ // versions that don't emit `t`. Doesn't fail the flow because the
188
+ // session is still usable client-side; only strict server-side
189
+ // verification will reject it.
190
+ if (typeof console !== 'undefined') {
191
+ console.warn('signet-login: redirect callback missing `t` param — auth event ' +
192
+ 'created_at approximated. Server-side verification may reject. ' +
193
+ 'Upgrade signet-app to emit `t` in the redirect URL.');
194
+ }
195
+ }
196
+ const lowerPubkey = pubkey.toLowerCase();
197
+ const lowerSig = signature.toLowerCase();
198
+ const lowerEventId = eventId.toLowerCase();
199
+ const authEvent = {
200
+ id: lowerEventId,
201
+ pubkey: lowerPubkey,
202
+ kind: 21236,
203
+ created_at: createdAt,
204
+ tags: [
205
+ ['challenge', pending.challenge],
206
+ ['origin', pending.origin],
207
+ ['app', pending.appName],
208
+ ],
209
+ content: '',
210
+ sig: lowerSig,
211
+ };
212
+ const displayName = params.get('display_name') || undefined;
213
+ const ephemeral = new EphemeralSigner(lowerPubkey, authEvent);
214
+ const session = {
215
+ pubkey: lowerPubkey,
216
+ method: 'redirect',
217
+ signer: ephemeral,
218
+ authEvent,
219
+ };
220
+ if (displayName)
221
+ session.displayName = displayName;
222
+ return finalize({ kind: 'session', session });
223
+ }
224
+ // Re-export DEFAULTS for tree-shaking-friendly callers that want to avoid
225
+ // importing the full types module just for one constant.
226
+ export { DEFAULTS };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Three signer implementations behind one interface.
3
+ *
4
+ * Nip07Signer — wraps window.nostr (bark, Alby, nos2x, Flamingo, …)
5
+ * BunkerSignerImpl — wraps nostr-tools BunkerSigner (NIP-46 over relay)
6
+ * EphemeralSigner — auth-only fallback when only the redirect signature is available
7
+ */
8
+ import type { EventTemplate, NostrEvent, SignetSigner, SignerCapabilities, SignetAuthEvent } from './types.js';
9
+ import { BunkerSigner } from 'nostr-tools/nip46';
10
+ /** The shape of `window.nostr` exposed by NIP-07 extensions. */
11
+ interface Nip07Provider {
12
+ getPublicKey(): Promise<string>;
13
+ signEvent(event: EventTemplate): Promise<NostrEvent>;
14
+ nip44?: {
15
+ encrypt(peerPubkey: string, plaintext: string): Promise<string>;
16
+ decrypt(peerPubkey: string, ciphertext: string): Promise<string>;
17
+ };
18
+ }
19
+ declare global {
20
+ interface Window {
21
+ nostr?: Nip07Provider;
22
+ }
23
+ }
24
+ /** Returns true if a NIP-07 extension is present on the page. */
25
+ export declare function hasNip07(): boolean;
26
+ export declare class Nip07Signer implements SignetSigner {
27
+ readonly pubkey: string;
28
+ private readonly provider;
29
+ readonly method: "nip07";
30
+ readonly capabilities: SignerCapabilities;
31
+ readonly nip44?: SignetSigner['nip44'];
32
+ constructor(pubkey: string, provider: Nip07Provider);
33
+ signEvent(template: EventTemplate): Promise<NostrEvent>;
34
+ close(): Promise<void>;
35
+ }
36
+ /** Connects to the page's NIP-07 provider and returns a Nip07Signer. */
37
+ export declare function createNip07Signer(): Promise<Nip07Signer>;
38
+ /** Wraps nostr-tools' BunkerSigner with our SignetSigner interface. */
39
+ export declare class BunkerSignerImpl implements SignetSigner {
40
+ readonly pubkey: string;
41
+ private readonly bunker;
42
+ /** Original bunker URI — kept for persistence/reconnect. */
43
+ readonly bunkerUri: string;
44
+ /** The 32-byte client secret key used in this session — kept for reconnect. */
45
+ readonly clientSecretKey: Uint8Array;
46
+ readonly method: "bunker";
47
+ readonly capabilities: SignerCapabilities;
48
+ readonly nip44: SignetSigner['nip44'];
49
+ constructor(pubkey: string, bunker: BunkerSigner,
50
+ /** Original bunker URI — kept for persistence/reconnect. */
51
+ bunkerUri: string,
52
+ /** The 32-byte client secret key used in this session — kept for reconnect. */
53
+ clientSecretKey: Uint8Array);
54
+ signEvent(template: EventTemplate): Promise<NostrEvent>;
55
+ close(): Promise<void>;
56
+ }
57
+ /**
58
+ * Connect a bunker session from a `bunker://` or `nostr+connect://` URI (or a
59
+ * NIP-05 identifier). Generates a fresh client secret key for the session.
60
+ */
61
+ export declare function createBunkerSigner(input: {
62
+ uri: string;
63
+ clientSecretKey?: Uint8Array;
64
+ onauth?: (url: string) => void;
65
+ }): Promise<BunkerSignerImpl>;
66
+ /** Generate a 32-byte secret key. */
67
+ export declare function generateSecretKey(): Uint8Array;
68
+ /**
69
+ * Auth-only signer returned by the redirect/QR flow before Option B is built.
70
+ * Holds the signed challenge but cannot sign further events.
71
+ */
72
+ export declare class EphemeralSigner implements SignetSigner {
73
+ readonly pubkey: string;
74
+ readonly authEvent: SignetAuthEvent;
75
+ readonly method: "redirect";
76
+ readonly capabilities: SignerCapabilities;
77
+ constructor(pubkey: string, authEvent: SignetAuthEvent);
78
+ signEvent(_template: EventTemplate): Promise<NostrEvent>;
79
+ close(): Promise<void>;
80
+ }
81
+ export {};
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Three signer implementations behind one interface.
3
+ *
4
+ * Nip07Signer — wraps window.nostr (bark, Alby, nos2x, Flamingo, …)
5
+ * BunkerSignerImpl — wraps nostr-tools BunkerSigner (NIP-46 over relay)
6
+ * EphemeralSigner — auth-only fallback when only the redirect signature is available
7
+ */
8
+ import { BunkerSigner, parseBunkerInput } from 'nostr-tools/nip46';
9
+ /** Returns true if a NIP-07 extension is present on the page. */
10
+ export function hasNip07() {
11
+ return typeof window !== 'undefined' && !!window.nostr && typeof window.nostr.signEvent === 'function';
12
+ }
13
+ export class Nip07Signer {
14
+ constructor(pubkey, provider) {
15
+ this.pubkey = pubkey;
16
+ this.provider = provider;
17
+ this.method = 'nip07';
18
+ this.capabilities = { canSignEvents: true, hasNip44: !!provider.nip44 };
19
+ if (provider.nip44) {
20
+ this.nip44 = {
21
+ encrypt: (peer, pt) => provider.nip44.encrypt(peer, pt),
22
+ decrypt: (peer, ct) => provider.nip44.decrypt(peer, ct),
23
+ };
24
+ }
25
+ }
26
+ async signEvent(template) {
27
+ return this.provider.signEvent(template);
28
+ }
29
+ async close() {
30
+ // NIP-07 extensions have no concept of disconnect — nothing to do.
31
+ }
32
+ }
33
+ /** Connects to the page's NIP-07 provider and returns a Nip07Signer. */
34
+ export async function createNip07Signer() {
35
+ if (!hasNip07())
36
+ throw new Error('no-nip07-provider');
37
+ const provider = window.nostr;
38
+ const pubkey = await provider.getPublicKey();
39
+ if (!/^[0-9a-f]{64}$/i.test(pubkey))
40
+ throw new Error('invalid-pubkey-from-nip07');
41
+ return new Nip07Signer(pubkey.toLowerCase(), provider);
42
+ }
43
+ // ── NIP-46 bunker ─────────────────────────────────────────────────────────────
44
+ /** Wraps nostr-tools' BunkerSigner with our SignetSigner interface. */
45
+ export class BunkerSignerImpl {
46
+ constructor(pubkey, bunker,
47
+ /** Original bunker URI — kept for persistence/reconnect. */
48
+ bunkerUri,
49
+ /** The 32-byte client secret key used in this session — kept for reconnect. */
50
+ clientSecretKey) {
51
+ this.pubkey = pubkey;
52
+ this.bunker = bunker;
53
+ this.bunkerUri = bunkerUri;
54
+ this.clientSecretKey = clientSecretKey;
55
+ this.method = 'bunker';
56
+ this.capabilities = { canSignEvents: true, hasNip44: true };
57
+ this.nip44 = {
58
+ encrypt: (peer, pt) => bunker.nip44Encrypt(peer, pt),
59
+ decrypt: (peer, ct) => bunker.nip44Decrypt(peer, ct),
60
+ };
61
+ }
62
+ async signEvent(template) {
63
+ // BunkerSigner.signEvent expects EventTemplate with required created_at + tags.
64
+ // Strip any pubkey field, fill in defaults if omitted.
65
+ const { pubkey: _omit, ...rest } = template;
66
+ void _omit;
67
+ const filled = {
68
+ kind: rest.kind,
69
+ content: rest.content,
70
+ created_at: rest.created_at ?? Math.floor(Date.now() / 1000),
71
+ tags: rest.tags ?? [],
72
+ };
73
+ const verified = await this.bunker.signEvent(filled);
74
+ return verified;
75
+ }
76
+ async close() {
77
+ await this.bunker.close();
78
+ }
79
+ }
80
+ /**
81
+ * Connect a bunker session from a `bunker://` or `nostr+connect://` URI (or a
82
+ * NIP-05 identifier). Generates a fresh client secret key for the session.
83
+ */
84
+ export async function createBunkerSigner(input) {
85
+ const trimmed = input.uri.trim();
86
+ if (!trimmed)
87
+ throw new Error('empty-bunker-uri');
88
+ const pointer = await parseBunkerInput(trimmed);
89
+ if (!pointer)
90
+ throw new Error('invalid-bunker-uri');
91
+ const sk = input.clientSecretKey ?? generateSecretKey();
92
+ if (sk.length !== 32)
93
+ throw new Error('invalid-client-secret-key');
94
+ const bunker = BunkerSigner.fromBunker(sk, pointer, { onauth: input.onauth });
95
+ await bunker.connect();
96
+ const pubkey = await bunker.getPublicKey();
97
+ if (!/^[0-9a-f]{64}$/i.test(pubkey)) {
98
+ await bunker.close().catch(() => { });
99
+ throw new Error('invalid-pubkey-from-bunker');
100
+ }
101
+ return new BunkerSignerImpl(pubkey.toLowerCase(), bunker, trimmed, sk);
102
+ }
103
+ /** Generate a 32-byte secret key. */
104
+ export function generateSecretKey() {
105
+ const sk = new Uint8Array(32);
106
+ crypto.getRandomValues(sk);
107
+ return sk;
108
+ }
109
+ // ── Ephemeral (redirect-only) ─────────────────────────────────────────────────
110
+ /**
111
+ * Auth-only signer returned by the redirect/QR flow before Option B is built.
112
+ * Holds the signed challenge but cannot sign further events.
113
+ */
114
+ export class EphemeralSigner {
115
+ constructor(pubkey, authEvent) {
116
+ this.pubkey = pubkey;
117
+ this.authEvent = authEvent;
118
+ this.method = 'redirect';
119
+ this.capabilities = { canSignEvents: false, hasNip44: false };
120
+ }
121
+ async signEvent(_template) {
122
+ throw new Error('signer-auth-only: this session was established via redirect and cannot sign new events. ' +
123
+ 'Install a NIP-07 extension (bark, Alby) or paste a bunker URI to upgrade.');
124
+ }
125
+ async close() {
126
+ // nothing to close
127
+ }
128
+ }
@@ -0,0 +1,87 @@
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
+ export type { NostrEvent, EventTemplate, LoginMethod, SignerCapabilities, SignetSigner, SignetAuthEvent, SignetSession, LoginOptions, RestoreOptions, } from './types.js';
18
+ import type { LoginOptions, RestoreOptions, SignetSession } from './types.js';
19
+ import { handleCallback as handlePopupCallback } from './callback.js';
20
+ import type { ConsumeCallbackResult } from './redirect.js';
21
+ export type { CallbackResult } from './callback.js';
22
+ export type { ConsumeCallbackResult } from './redirect.js';
23
+ /**
24
+ * Show the login picker and resolve to a SignetSession on success, or null on
25
+ * cancel / timeout.
26
+ *
27
+ * When `mode: 'redirect'` is set, the picker is skipped entirely — the current
28
+ * tab navigates to signet-app and this promise NEVER resolves in this tab.
29
+ * Callers should treat the returned promise as "fire and forget" in that case
30
+ * and call `Signet.handleCallback()` on the next page load to receive the
31
+ * session. The other login methods (NIP-07, bunker) don't use redirect at all
32
+ * and are unaffected by this option.
33
+ */
34
+ export declare function login(opts: LoginOptions): Promise<SignetSession | null>;
35
+ /**
36
+ * Try to restore a session from localStorage. Returns null if no session is
37
+ * stored or it's malformed/expired.
38
+ *
39
+ * For bunker sessions, attempts to reconnect using the stored URI + client SK.
40
+ * If the bunker is unreachable, returns null and clears the stored session.
41
+ */
42
+ export declare function restoreSession(opts?: RestoreOptions): Promise<SignetSession | null>;
43
+ /**
44
+ * Popup-style callback receiver. Use on the page that signet-app redirects
45
+ * a popup to. Parses URL params and posts them to `window.opener`, then
46
+ * closes the popup. Returns the raw params for non-popup contexts.
47
+ *
48
+ * For the same-tab redirect flow (`mode: 'redirect'` on `login()`), use
49
+ * `Signet.handleRedirectCallback()` instead — that one validates against the
50
+ * persisted pending state and returns a fully-formed `SignetSession`.
51
+ */
52
+ export declare const handleCallback: typeof handlePopupCallback;
53
+ /**
54
+ * Same-tab redirect callback receiver. Call once on app boot, before
55
+ * `restoreSession()`, to consume an incoming `?pubkey&signature&eventId`
56
+ * payload from signet-app.
57
+ *
58
+ * Behaviour:
59
+ *
60
+ * - `'session'`: validates the round-trip against the pending state saved
61
+ * by `login({ mode: 'redirect' })`, builds and persists a SignetSession
62
+ * (so `restoreSession()` finds it next time), and strips the auth params
63
+ * from the URL via `history.replaceState`. The returned session uses an
64
+ * `EphemeralSigner` — `signer.capabilities.canSignEvents` is false. Pair
65
+ * with NIP-07 / bunker if you need ongoing signing.
66
+ *
67
+ * - `'denied'`: signet-app reported the user rejected the request.
68
+ *
69
+ * - `'no-callback'`: no auth params on the URL — the typical case on most
70
+ * page loads. Idempotent: a second call after success also returns this.
71
+ *
72
+ * - `'invalid'`: params present but failed validation. `reason` is a
73
+ * machine-readable token (`origin-mismatch`, `pending-stale`,
74
+ * `pubkey-malformed`, …). Pending state is cleared either way so a stale
75
+ * URL can't poison the next attempt.
76
+ *
77
+ * The returned shape is intentionally tagged so consumers can distinguish
78
+ * "user denied" from "no callback" without inspecting null. Persistence on
79
+ * success uses the same storage layer as relay-mode sessions, so downstream
80
+ * code that consumes `restoreSession()` doesn't need to care which path
81
+ * authenticated the user.
82
+ */
83
+ export declare function handleRedirectCallback(): Promise<ConsumeCallbackResult>;
84
+ /**
85
+ * Clear the stored session and close the active signer.
86
+ */
87
+ export declare function logout(currentSession?: SignetSession): Promise<void>;