@formstr/signer 0.2.2 → 0.3.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/README.md +109 -5
- package/dist/index.cjs +420 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +413 -8
- package/dist/index.js.map +1 -1
- package/dist/{signer-BepGdtJs.d.cts → signer-CQyqBYxq.d.cts} +286 -2
- package/dist/{signer-BepGdtJs.d.ts → signer-CQyqBYxq.d.ts} +286 -2
- package/dist/ui/index.cjs +36 -2
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +30 -4
- package/dist/ui/index.d.ts +30 -4
- package/dist/ui/index.js +36 -2
- package/dist/ui/index.js.map +1 -1
- package/package.json +3 -2
- package/styles/signer.css +10 -0
package/dist/ui/index.cjs
CHANGED
|
@@ -35,16 +35,22 @@ __export(ui_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(ui_exports);
|
|
37
37
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
38
|
+
function escapeHtml(text) {
|
|
39
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
40
|
+
}
|
|
38
41
|
var TABS = [
|
|
39
42
|
{ id: "create", label: "Create" },
|
|
40
43
|
{ id: "ncryptsec", label: "Existing key" },
|
|
41
44
|
{ id: "extension", label: "Extension" },
|
|
42
45
|
{ id: "bunker", label: "Bunker URI" },
|
|
43
46
|
{ id: "nostrconnect", label: "Remote (QR)" },
|
|
47
|
+
{ id: "nip55web", label: "Signer app" },
|
|
44
48
|
{ id: "android", label: "Android" }
|
|
45
49
|
];
|
|
46
|
-
function renderLoginHtml() {
|
|
47
|
-
const
|
|
50
|
+
function renderLoginHtml(options = {}) {
|
|
51
|
+
const nip55Web = options.nip55Web ?? (options.includeNip55Web === false ? { visible: false } : { visible: true });
|
|
52
|
+
const includeNip55Web = nip55Web.visible;
|
|
53
|
+
const tabs = TABS.filter((t) => t.id !== "nip55web" || includeNip55Web).map(
|
|
48
54
|
(t) => `<button class="nostr-signer__tab nostr-signer__tab--${t.id}" type="button" data-tab="${t.id}">${t.label}</button>`
|
|
49
55
|
).join("");
|
|
50
56
|
return `<div class="nostr-signer__root">
|
|
@@ -101,6 +107,11 @@ function renderLoginHtml() {
|
|
|
101
107
|
<button class="nostr-signer__button nostr-signer__button--secondary" type="button" data-action="nostrconnect-cancel">Cancel</button>
|
|
102
108
|
</div>
|
|
103
109
|
</section>
|
|
110
|
+
${includeNip55Web ? `<section class="nostr-signer__panel nostr-signer__panel--nip55web" data-panel="nip55web" hidden>
|
|
111
|
+
<p class="nostr-signer__hint">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You’ll be asked to approve each signature in the app.</p>
|
|
112
|
+
${nip55Web.warning ? `<p class="nostr-signer__warn" data-region="nip55web-warning" role="status">${escapeHtml(nip55Web.warning)}</p>` : ""}
|
|
113
|
+
<button class="nostr-signer__button nostr-signer__button--primary" type="button" data-action="nip55web-login">Open signer app</button>
|
|
114
|
+
</section>` : ""}
|
|
104
115
|
<section class="nostr-signer__panel nostr-signer__panel--android" data-panel="android" hidden>
|
|
105
116
|
<p class="nostr-signer__hint">Sign in with an Android external signer app (NIP-55) such as Amber.</p>
|
|
106
117
|
<p class="nostr-signer__status" data-region="android-status">Loading installed signers…</p>
|
|
@@ -119,6 +130,7 @@ function renderLoginHtml() {
|
|
|
119
130
|
function attachLoginListeners(rootEl, signer, handlers = {}) {
|
|
120
131
|
const detachers = [];
|
|
121
132
|
let nostrConnectAbort = null;
|
|
133
|
+
let nip55WebAbort = null;
|
|
122
134
|
const on = (el, event, handler) => {
|
|
123
135
|
const wrapped = handler;
|
|
124
136
|
el.addEventListener(event, wrapped);
|
|
@@ -239,6 +251,27 @@ function attachLoginListeners(rootEl, signer, handlers = {}) {
|
|
|
239
251
|
showError(err);
|
|
240
252
|
}
|
|
241
253
|
});
|
|
254
|
+
const nip55WebBtn = rootEl.querySelector(
|
|
255
|
+
'[data-action="nip55web-login"]'
|
|
256
|
+
);
|
|
257
|
+
if (nip55WebBtn) on(nip55WebBtn, "click", async () => {
|
|
258
|
+
clearError();
|
|
259
|
+
const btn = nip55WebBtn;
|
|
260
|
+
btn.disabled = true;
|
|
261
|
+
nip55WebAbort = new AbortController();
|
|
262
|
+
try {
|
|
263
|
+
const account = await signer.loginWithNip55Web({
|
|
264
|
+
signal: nip55WebAbort.signal,
|
|
265
|
+
debug: handlers.onNip55WebDebug
|
|
266
|
+
});
|
|
267
|
+
handlers.onLogin?.(account);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
btn.disabled = false;
|
|
270
|
+
if (err.name !== "AbortError") showError(err);
|
|
271
|
+
} finally {
|
|
272
|
+
nip55WebAbort = null;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
242
275
|
const bunkerForm = q('[data-form="bunker"]');
|
|
243
276
|
on(bunkerForm, "submit", async (ev) => {
|
|
244
277
|
ev.preventDefault();
|
|
@@ -321,6 +354,7 @@ function attachLoginListeners(rootEl, signer, handlers = {}) {
|
|
|
321
354
|
return {
|
|
322
355
|
detach: () => {
|
|
323
356
|
nostrConnectAbort?.abort();
|
|
357
|
+
nip55WebAbort?.abort();
|
|
324
358
|
for (const d of detachers) d();
|
|
325
359
|
},
|
|
326
360
|
selectTab
|
package/dist/ui/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\nexport type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'android', label: 'Android' },\n];\n\nexport function renderLoginHtml(): string {\n const tabs = TABS.map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n ).join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">×</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://<pubkey>?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers…</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":["QRCode"]}
|
|
1
|
+
{"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { Nip55WebSupport } from '../nip55Web.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\n/** Escape text before interpolating it into the HTML template. */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nexport type LoginTab =\n | 'create'\n | 'ncryptsec'\n | 'extension'\n | 'bunker'\n | 'nostrconnect'\n | 'nip55web'\n | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n /**\n * Optional diagnostic stream for the browser NIP-55 (intent/clipboard)\n * flow. Wire it to an on-screen log when testing on a phone — the flow\n * navigates away, so its behaviour is otherwise hard to observe.\n */\n onNip55WebDebug?: (message: string) => void;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'nip55web', label: 'Signer app' },\n { id: 'android', label: 'Android' },\n];\n\nexport interface RenderLoginOptions {\n /**\n * The browser NIP-55 (\"Signer app\") status, from\n * `signer.nip55WebSupport()`. `visible` controls whether the tab is\n * rendered; `warning` renders an advisory note above the button without\n * disabling it.\n *\n * Defaults to shown without a warning. Pass the real status so a native\n * build hides the tab and Firefox for Android shows its warning.\n */\n nip55Web?: Nip55WebSupport;\n /**\n * Include the \"Signer app\" tab (browser NIP-55). Shorthand for the\n * `visible` half of {@link nip55Web}; defaults to `true`. Ignored when\n * `nip55Web` is supplied.\n *\n * @deprecated Prefer `nip55Web`, which can also carry a warning.\n */\n includeNip55Web?: boolean;\n}\n\nexport function renderLoginHtml(options: RenderLoginOptions = {}): string {\n const nip55Web: Nip55WebSupport =\n options.nip55Web ??\n (options.includeNip55Web === false\n ? { visible: false }\n : { visible: true });\n const includeNip55Web = nip55Web.visible;\n const tabs = TABS.filter((t) => t.id !== 'nip55web' || includeNip55Web)\n .map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n )\n .join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">×</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://<pubkey>?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n ${includeNip55Web ? `<section class=\"nostr-signer__panel nostr-signer__panel--nip55web\" data-panel=\"nip55web\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You’ll be asked to approve each signature in the app.</p>\n ${nip55Web.warning ? `<p class=\"nostr-signer__warn\" data-region=\"nip55web-warning\" role=\"status\">${escapeHtml(nip55Web.warning)}</p>` : ''}\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"nip55web-login\">Open signer app</button>\n </section>` : ''}\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers…</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n let nip55WebAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- signer app (NIP-55 over the web) ----\n // Absent when renderLoginHtml({ includeNip55Web: false }) omitted the tab.\n const nip55WebBtn = rootEl.querySelector<HTMLButtonElement>(\n '[data-action=\"nip55web-login\"]',\n );\n if (nip55WebBtn) on(nip55WebBtn, 'click', async () => {\n clearError();\n const btn = nip55WebBtn;\n btn.disabled = true;\n nip55WebAbort = new AbortController();\n try {\n const account = await signer.loginWithNip55Web({\n signal: nip55WebAbort.signal,\n debug: handlers.onNip55WebDebug,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n // A user cancelling (detach) is not an error worth surfacing.\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nip55WebAbort = null;\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n nip55WebAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":["QRCode"]}
|
package/dist/ui/index.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AbstractSimplePool } from 'nostr-tools/abstract-pool';
|
|
2
|
-
import {
|
|
2
|
+
import { p as StoredAccount, R as RelayMismatchHandler, h as Nip55WebSupport, S as Signer } from '../signer-CQyqBYxq.cjs';
|
|
3
3
|
import 'nostr-tools';
|
|
4
4
|
|
|
5
|
-
type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'android';
|
|
5
|
+
type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'nip55web' | 'android';
|
|
6
6
|
interface LoginUiHandlers {
|
|
7
7
|
onLogin?: (account: StoredAccount) => void;
|
|
8
8
|
onCancel?: () => void;
|
|
@@ -13,12 +13,38 @@ interface LoginUiHandlers {
|
|
|
13
13
|
onRelayMismatch?: RelayMismatchHandler;
|
|
14
14
|
/** Initial tab to show. Defaults to 'create'. */
|
|
15
15
|
defaultTab?: LoginTab;
|
|
16
|
+
/**
|
|
17
|
+
* Optional diagnostic stream for the browser NIP-55 (intent/clipboard)
|
|
18
|
+
* flow. Wire it to an on-screen log when testing on a phone — the flow
|
|
19
|
+
* navigates away, so its behaviour is otherwise hard to observe.
|
|
20
|
+
*/
|
|
21
|
+
onNip55WebDebug?: (message: string) => void;
|
|
16
22
|
}
|
|
17
23
|
interface LoginUiBinding {
|
|
18
24
|
detach(): void;
|
|
19
25
|
selectTab(tab: LoginTab): void;
|
|
20
26
|
}
|
|
21
|
-
|
|
27
|
+
interface RenderLoginOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The browser NIP-55 ("Signer app") status, from
|
|
30
|
+
* `signer.nip55WebSupport()`. `visible` controls whether the tab is
|
|
31
|
+
* rendered; `warning` renders an advisory note above the button without
|
|
32
|
+
* disabling it.
|
|
33
|
+
*
|
|
34
|
+
* Defaults to shown without a warning. Pass the real status so a native
|
|
35
|
+
* build hides the tab and Firefox for Android shows its warning.
|
|
36
|
+
*/
|
|
37
|
+
nip55Web?: Nip55WebSupport;
|
|
38
|
+
/**
|
|
39
|
+
* Include the "Signer app" tab (browser NIP-55). Shorthand for the
|
|
40
|
+
* `visible` half of {@link nip55Web}; defaults to `true`. Ignored when
|
|
41
|
+
* `nip55Web` is supplied.
|
|
42
|
+
*
|
|
43
|
+
* @deprecated Prefer `nip55Web`, which can also carry a warning.
|
|
44
|
+
*/
|
|
45
|
+
includeNip55Web?: boolean;
|
|
46
|
+
}
|
|
47
|
+
declare function renderLoginHtml(options?: RenderLoginOptions): string;
|
|
22
48
|
declare function attachLoginListeners(rootEl: HTMLElement, signer: Signer, handlers?: LoginUiHandlers): LoginUiBinding;
|
|
23
49
|
|
|
24
|
-
export { type LoginTab, type LoginUiBinding, type LoginUiHandlers, attachLoginListeners, renderLoginHtml };
|
|
50
|
+
export { type LoginTab, type LoginUiBinding, type LoginUiHandlers, type RenderLoginOptions, attachLoginListeners, renderLoginHtml };
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AbstractSimplePool } from 'nostr-tools/abstract-pool';
|
|
2
|
-
import {
|
|
2
|
+
import { p as StoredAccount, R as RelayMismatchHandler, h as Nip55WebSupport, S as Signer } from '../signer-CQyqBYxq.js';
|
|
3
3
|
import 'nostr-tools';
|
|
4
4
|
|
|
5
|
-
type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'android';
|
|
5
|
+
type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'nip55web' | 'android';
|
|
6
6
|
interface LoginUiHandlers {
|
|
7
7
|
onLogin?: (account: StoredAccount) => void;
|
|
8
8
|
onCancel?: () => void;
|
|
@@ -13,12 +13,38 @@ interface LoginUiHandlers {
|
|
|
13
13
|
onRelayMismatch?: RelayMismatchHandler;
|
|
14
14
|
/** Initial tab to show. Defaults to 'create'. */
|
|
15
15
|
defaultTab?: LoginTab;
|
|
16
|
+
/**
|
|
17
|
+
* Optional diagnostic stream for the browser NIP-55 (intent/clipboard)
|
|
18
|
+
* flow. Wire it to an on-screen log when testing on a phone — the flow
|
|
19
|
+
* navigates away, so its behaviour is otherwise hard to observe.
|
|
20
|
+
*/
|
|
21
|
+
onNip55WebDebug?: (message: string) => void;
|
|
16
22
|
}
|
|
17
23
|
interface LoginUiBinding {
|
|
18
24
|
detach(): void;
|
|
19
25
|
selectTab(tab: LoginTab): void;
|
|
20
26
|
}
|
|
21
|
-
|
|
27
|
+
interface RenderLoginOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The browser NIP-55 ("Signer app") status, from
|
|
30
|
+
* `signer.nip55WebSupport()`. `visible` controls whether the tab is
|
|
31
|
+
* rendered; `warning` renders an advisory note above the button without
|
|
32
|
+
* disabling it.
|
|
33
|
+
*
|
|
34
|
+
* Defaults to shown without a warning. Pass the real status so a native
|
|
35
|
+
* build hides the tab and Firefox for Android shows its warning.
|
|
36
|
+
*/
|
|
37
|
+
nip55Web?: Nip55WebSupport;
|
|
38
|
+
/**
|
|
39
|
+
* Include the "Signer app" tab (browser NIP-55). Shorthand for the
|
|
40
|
+
* `visible` half of {@link nip55Web}; defaults to `true`. Ignored when
|
|
41
|
+
* `nip55Web` is supplied.
|
|
42
|
+
*
|
|
43
|
+
* @deprecated Prefer `nip55Web`, which can also carry a warning.
|
|
44
|
+
*/
|
|
45
|
+
includeNip55Web?: boolean;
|
|
46
|
+
}
|
|
47
|
+
declare function renderLoginHtml(options?: RenderLoginOptions): string;
|
|
22
48
|
declare function attachLoginListeners(rootEl: HTMLElement, signer: Signer, handlers?: LoginUiHandlers): LoginUiBinding;
|
|
23
49
|
|
|
24
|
-
export { type LoginTab, type LoginUiBinding, type LoginUiHandlers, attachLoginListeners, renderLoginHtml };
|
|
50
|
+
export { type LoginTab, type LoginUiBinding, type LoginUiHandlers, type RenderLoginOptions, attachLoginListeners, renderLoginHtml };
|
package/dist/ui/index.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// src/ui/index.ts
|
|
2
2
|
import QRCode from "qrcode";
|
|
3
|
+
function escapeHtml(text) {
|
|
4
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5
|
+
}
|
|
3
6
|
var TABS = [
|
|
4
7
|
{ id: "create", label: "Create" },
|
|
5
8
|
{ id: "ncryptsec", label: "Existing key" },
|
|
6
9
|
{ id: "extension", label: "Extension" },
|
|
7
10
|
{ id: "bunker", label: "Bunker URI" },
|
|
8
11
|
{ id: "nostrconnect", label: "Remote (QR)" },
|
|
12
|
+
{ id: "nip55web", label: "Signer app" },
|
|
9
13
|
{ id: "android", label: "Android" }
|
|
10
14
|
];
|
|
11
|
-
function renderLoginHtml() {
|
|
12
|
-
const
|
|
15
|
+
function renderLoginHtml(options = {}) {
|
|
16
|
+
const nip55Web = options.nip55Web ?? (options.includeNip55Web === false ? { visible: false } : { visible: true });
|
|
17
|
+
const includeNip55Web = nip55Web.visible;
|
|
18
|
+
const tabs = TABS.filter((t) => t.id !== "nip55web" || includeNip55Web).map(
|
|
13
19
|
(t) => `<button class="nostr-signer__tab nostr-signer__tab--${t.id}" type="button" data-tab="${t.id}">${t.label}</button>`
|
|
14
20
|
).join("");
|
|
15
21
|
return `<div class="nostr-signer__root">
|
|
@@ -66,6 +72,11 @@ function renderLoginHtml() {
|
|
|
66
72
|
<button class="nostr-signer__button nostr-signer__button--secondary" type="button" data-action="nostrconnect-cancel">Cancel</button>
|
|
67
73
|
</div>
|
|
68
74
|
</section>
|
|
75
|
+
${includeNip55Web ? `<section class="nostr-signer__panel nostr-signer__panel--nip55web" data-panel="nip55web" hidden>
|
|
76
|
+
<p class="nostr-signer__hint">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You’ll be asked to approve each signature in the app.</p>
|
|
77
|
+
${nip55Web.warning ? `<p class="nostr-signer__warn" data-region="nip55web-warning" role="status">${escapeHtml(nip55Web.warning)}</p>` : ""}
|
|
78
|
+
<button class="nostr-signer__button nostr-signer__button--primary" type="button" data-action="nip55web-login">Open signer app</button>
|
|
79
|
+
</section>` : ""}
|
|
69
80
|
<section class="nostr-signer__panel nostr-signer__panel--android" data-panel="android" hidden>
|
|
70
81
|
<p class="nostr-signer__hint">Sign in with an Android external signer app (NIP-55) such as Amber.</p>
|
|
71
82
|
<p class="nostr-signer__status" data-region="android-status">Loading installed signers…</p>
|
|
@@ -84,6 +95,7 @@ function renderLoginHtml() {
|
|
|
84
95
|
function attachLoginListeners(rootEl, signer, handlers = {}) {
|
|
85
96
|
const detachers = [];
|
|
86
97
|
let nostrConnectAbort = null;
|
|
98
|
+
let nip55WebAbort = null;
|
|
87
99
|
const on = (el, event, handler) => {
|
|
88
100
|
const wrapped = handler;
|
|
89
101
|
el.addEventListener(event, wrapped);
|
|
@@ -204,6 +216,27 @@ function attachLoginListeners(rootEl, signer, handlers = {}) {
|
|
|
204
216
|
showError(err);
|
|
205
217
|
}
|
|
206
218
|
});
|
|
219
|
+
const nip55WebBtn = rootEl.querySelector(
|
|
220
|
+
'[data-action="nip55web-login"]'
|
|
221
|
+
);
|
|
222
|
+
if (nip55WebBtn) on(nip55WebBtn, "click", async () => {
|
|
223
|
+
clearError();
|
|
224
|
+
const btn = nip55WebBtn;
|
|
225
|
+
btn.disabled = true;
|
|
226
|
+
nip55WebAbort = new AbortController();
|
|
227
|
+
try {
|
|
228
|
+
const account = await signer.loginWithNip55Web({
|
|
229
|
+
signal: nip55WebAbort.signal,
|
|
230
|
+
debug: handlers.onNip55WebDebug
|
|
231
|
+
});
|
|
232
|
+
handlers.onLogin?.(account);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
btn.disabled = false;
|
|
235
|
+
if (err.name !== "AbortError") showError(err);
|
|
236
|
+
} finally {
|
|
237
|
+
nip55WebAbort = null;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
207
240
|
const bunkerForm = q('[data-form="bunker"]');
|
|
208
241
|
on(bunkerForm, "submit", async (ev) => {
|
|
209
242
|
ev.preventDefault();
|
|
@@ -286,6 +319,7 @@ function attachLoginListeners(rootEl, signer, handlers = {}) {
|
|
|
286
319
|
return {
|
|
287
320
|
detach: () => {
|
|
288
321
|
nostrConnectAbort?.abort();
|
|
322
|
+
nip55WebAbort?.abort();
|
|
289
323
|
for (const d of detachers) d();
|
|
290
324
|
},
|
|
291
325
|
selectTab
|
package/dist/ui/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\nexport type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'android', label: 'Android' },\n];\n\nexport function renderLoginHtml(): string {\n const tabs = TABS.map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n ).join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">×</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://<pubkey>?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers…</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { Nip55WebSupport } from '../nip55Web.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\n/** Escape text before interpolating it into the HTML template. */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nexport type LoginTab =\n | 'create'\n | 'ncryptsec'\n | 'extension'\n | 'bunker'\n | 'nostrconnect'\n | 'nip55web'\n | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n /**\n * Optional diagnostic stream for the browser NIP-55 (intent/clipboard)\n * flow. Wire it to an on-screen log when testing on a phone — the flow\n * navigates away, so its behaviour is otherwise hard to observe.\n */\n onNip55WebDebug?: (message: string) => void;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'nip55web', label: 'Signer app' },\n { id: 'android', label: 'Android' },\n];\n\nexport interface RenderLoginOptions {\n /**\n * The browser NIP-55 (\"Signer app\") status, from\n * `signer.nip55WebSupport()`. `visible` controls whether the tab is\n * rendered; `warning` renders an advisory note above the button without\n * disabling it.\n *\n * Defaults to shown without a warning. Pass the real status so a native\n * build hides the tab and Firefox for Android shows its warning.\n */\n nip55Web?: Nip55WebSupport;\n /**\n * Include the \"Signer app\" tab (browser NIP-55). Shorthand for the\n * `visible` half of {@link nip55Web}; defaults to `true`. Ignored when\n * `nip55Web` is supplied.\n *\n * @deprecated Prefer `nip55Web`, which can also carry a warning.\n */\n includeNip55Web?: boolean;\n}\n\nexport function renderLoginHtml(options: RenderLoginOptions = {}): string {\n const nip55Web: Nip55WebSupport =\n options.nip55Web ??\n (options.includeNip55Web === false\n ? { visible: false }\n : { visible: true });\n const includeNip55Web = nip55Web.visible;\n const tabs = TABS.filter((t) => t.id !== 'nip55web' || includeNip55Web)\n .map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n )\n .join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">×</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://<pubkey>?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n ${includeNip55Web ? `<section class=\"nostr-signer__panel nostr-signer__panel--nip55web\" data-panel=\"nip55web\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You’ll be asked to approve each signature in the app.</p>\n ${nip55Web.warning ? `<p class=\"nostr-signer__warn\" data-region=\"nip55web-warning\" role=\"status\">${escapeHtml(nip55Web.warning)}</p>` : ''}\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"nip55web-login\">Open signer app</button>\n </section>` : ''}\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers…</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n let nip55WebAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- signer app (NIP-55 over the web) ----\n // Absent when renderLoginHtml({ includeNip55Web: false }) omitted the tab.\n const nip55WebBtn = rootEl.querySelector<HTMLButtonElement>(\n '[data-action=\"nip55web-login\"]',\n );\n if (nip55WebBtn) on(nip55WebBtn, 'click', async () => {\n clearError();\n const btn = nip55WebBtn;\n btn.disabled = true;\n nip55WebAbort = new AbortController();\n try {\n const account = await signer.loginWithNip55Web({\n signal: nip55WebAbort.signal,\n debug: handlers.onNip55WebDebug,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n // A user cancelling (detach) is not an error worth surfacing.\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nip55WebAbort = null;\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n nip55WebAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formstr/signer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nostr signer with login UI for NIP-07, NIP-46, NIP-49 (ncryptsec), and NIP-55",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Nostr signer with login UI for NIP-07, NIP-46, NIP-49 (ncryptsec), and NIP-55 (Android apps, native or browser)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Abhay <toabhayraizada@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/formstr-hq/common-packages/tree/main/packages/signer#readme",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"bunker",
|
|
25
25
|
"nostr-connect",
|
|
26
26
|
"amber",
|
|
27
|
+
"nostrsigner",
|
|
27
28
|
"formstr"
|
|
28
29
|
],
|
|
29
30
|
"publishConfig": {
|
package/styles/signer.css
CHANGED
|
@@ -162,6 +162,16 @@ textarea.nostr-signer__input {
|
|
|
162
162
|
font-size: 0.85rem;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/* Advisory, non-blocking — the action below it stays enabled. */
|
|
166
|
+
.nostr-signer__warn {
|
|
167
|
+
margin: 0 0 12px 0;
|
|
168
|
+
padding: 8px 12px;
|
|
169
|
+
border-radius: 6px;
|
|
170
|
+
background: #fef3c7;
|
|
171
|
+
color: #92400e;
|
|
172
|
+
font-size: 0.85rem;
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
.nostr-signer__qr {
|
|
166
176
|
margin-top: 12px;
|
|
167
177
|
}
|