aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-26 12:06:00 -0700
committerAdam Soltys <[email protected]>2026-08-26 12:06:00 -0700
commit0427a1b02c21fd5fa9f5df8a86e435dbb84a8451 (patch)
tree32e5601682e6f65e46a8811cfc2ea7184ba17119 /src
parent6be9a45c4d085988c8bfaf210df762dbc83c8ab1 (diff)
downloadcoinosv3-0427a1b02c21fd5fa9f5df8a86e435dbb84a8451.tar.gz
coinosv3-0427a1b02c21fd5fa9f5df8a86e435dbb84a8451.zip
The page arrives light and paints at once: index.html shrinks to 46KB with a static boot shell baked in — brand row, and the profile frame when the path names one — while the 789KB bundle moves to a content-addressed app-<hash>.js the browser keeps (a repeat visit re-downloads kilobytes, not the app); the nostr-tools barrel import goes subpath, and the NIP-46 signer client ships as its own lazy file fetched on first bunker login
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
Diffstat (limited to 'src')
-rw-r--r--src/concord.js4
-rw-r--r--src/features/nostrlogin.js19
-rw-r--r--src/nip46-global.js9
-rw-r--r--src/nostr-login.js23
4 files changed, 48 insertions, 7 deletions
diff --git a/src/concord.js b/src/concord.js
index 9f6803d..74c1313 100644
--- a/src/concord.js
+++ b/src/concord.js
@@ -9,7 +9,9 @@ import { hkdf } from "@noble/hashes/hkdf";
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { finalizeEvent, generateSecretKey, getPublicKey, getEventHash, verifyEvent } from "nostr-tools/pure";
-import { nip44 } from "nostr-tools";
+// subpath, NOT the "nostr-tools" barrel — the barrel drags every NIP (and a
+// second copy of the noble crypto stack) into the bundle for one import
+import * as nip44 from "nostr-tools/nip44";
import * as nip19 from "nostr-tools/nip19";
import { base64urlnopad } from "@scure/base";
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index 5195778..9c4156f 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -162,11 +162,19 @@ export function nostrLoginFeature(ctx) {
// from another device); when the app connects, login continues by itself.
let nc = null;
let ncRun = null;
- function startNostrConnect(run) {
+ let ncToken = null; // guards the async gap while the nip46 module loads
+ async function startNostrConnect(run) {
if (nc) nc.cancel();
ncRun = run;
- nc = nostrConnect();
- const mine = nc;
+ const token = (ncToken = {});
+ let mine;
+ try { mine = await nostrConnect(); } catch {
+ if (token === ncToken && ui.nostrConnectOpen) { ui.nostrConnectOpen = false; render(); }
+ return;
+ }
+ if (token !== ncToken || !ncRun) { mine.cancel(); return; }
+ nc = mine;
+ render();
mine.ready.then((signer) => {
if (nc !== mine || !ncRun) return;
ui.nostrConnectOpen = false;
@@ -179,6 +187,7 @@ export function nostrLoginFeature(ctx) {
function stopNostrConnect() {
if (nc) { nc.cancel(); nc = null; }
ncRun = null;
+ ncToken = null;
}
function nostrConnectSection(run) {
@@ -188,7 +197,9 @@ export function nostrLoginFeature(ctx) {
onClick: () => { ui.nostrConnectOpen = true; startNostrConnect(run); render(); },
}, t('nlConnectApp'));
}
- if (!nc) return null;
+ // the nip46 module is still fetching — hold the spot with a spinner
+ if (!nc) return h('div', { class: 'row gap6', style: 'align-items:center;justify-content:center;padding:8px' },
+ h('span', { class: 'spinner sm' }));
return h('div', { class: 'col', style: 'gap:10px;align-items:center' },
h('a', {
class: 'btn btn-block btn-primary', href: nc.uri,
diff --git a/src/nip46-global.js b/src/nip46-global.js
new file mode 100644
index 0000000..a841905
--- /dev/null
+++ b/src/nip46-global.js
@@ -0,0 +1,9 @@
+// Standalone bundle for the NIP-46 remote-signer client (bunker:// and
+// nostrconnect:// logins) — emitted as a separate dist/nip46.js and loaded on
+// demand by nostr-login.js, the first time a bunker interaction happens.
+// It's the heaviest, rarest corner of the login stack (and drags a second
+// copy of the noble crypto stack via nostr-tools' own pins), so it stays out
+// of the main bundle and off the initial load.
+import * as nip46 from 'nostr-tools/nip46';
+
+window.__nip46 = nip46;
diff --git a/src/nostr-login.js b/src/nostr-login.js
index 301bbeb..6958da0 100644
--- a/src/nostr-login.js
+++ b/src/nostr-login.js
@@ -38,9 +38,25 @@ import { entropyToMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { getPublicKey, finalizeEvent, nip44 } from './nostr.js';
import { SimplePool } from 'nostr-tools/pool';
-import { BunkerSigner, parseBunkerInput, createNostrConnectURI } from 'nostr-tools/nip46';
import { generateSecretKey } from 'nostr-tools/pure';
+// NIP-46 loads lazily from dist/nip46.js (same pattern as the QR decoder):
+// bunker logins are rare, and the module is heavy. Tests running outside a
+// browser preset globalThis.__nip46 with the real module instead.
+let _nip46 = null;
+function loadNip46() {
+ if (_nip46) return _nip46;
+ if (typeof globalThis !== 'undefined' && globalThis.__nip46) return (_nip46 = Promise.resolve(globalThis.__nip46));
+ _nip46 = new Promise((resolve, reject) => {
+ const s = document.createElement('script');
+ s.src = 'nip46.js';
+ s.onload = () => (window.__nip46 ? resolve(window.__nip46) : reject(new Error('signer module unavailable')));
+ s.onerror = () => { _nip46 = null; reject(new Error('could not load the remote-signer module')); };
+ document.head.appendChild(s);
+ });
+ return _nip46;
+}
+
const SEED_DTAG = 'coinos:wallet:v1';
const BACKUP_KIND = 30078;
const DERIVE_TAG = 'coinos:wallet-seed:v1';
@@ -91,6 +107,7 @@ export async function extensionSigner() {
}
export async function bunkerSigner(uri, { onAuth } = {}) {
+ const { BunkerSigner, parseBunkerInput } = await loadNip46();
const bp = await parseBunkerInput(String(uri || '').trim());
if (!bp) throw new Error('not a bunker:// address');
if (!bp.relays || !bp.relays.length) throw new Error('that bunker address lists no relays');
@@ -111,6 +128,7 @@ export async function bunkerSigner(uri, { onAuth } = {}) {
// prompting the user again.
export async function resumeBunker(session, { onAuth, timeoutMs = 8000 } = {}) {
if (!session || !session.local || !session.bp || !session.bp.pubkey) return null;
+ const { BunkerSigner } = await loadNip46();
const local = hex.decode(session.local);
const signer = BunkerSigner.fromBunker(local, session.bp, {
onauth: (url) => { if (onAuth) onAuth(url); },
@@ -147,7 +165,8 @@ function bunkerAdapter(signer, pubkey, local) {
// relay — no bunker URL to copy. Returns the URI (render it as a deep link
// and a QR), a promise that resolves to a signer adapter when the app
// answers, and a cancel.
-export function nostrConnect({ relays = ['wss://relay.coinos.io', 'wss://nos.lol'], timeoutMs = 180_000 } = {}) {
+export async function nostrConnect({ relays = ['wss://relay.coinos.io', 'wss://nos.lol'], timeoutMs = 180_000 } = {}) {
+ const { BunkerSigner, createNostrConnectURI } = await loadNip46();
const local = generateSecretKey();
const secret = hex.encode(crypto.getRandomValues(new Uint8Array(16)));
const uri = createNostrConnectURI({