diff options
| -rw-r--r-- | build.js | 82 | ||||
| -rw-r--r-- | src/app.js | 25 | ||||
| -rw-r--r-- | src/features/hats.js | 2 | ||||
| -rw-r--r-- | src/features/index.js | 35 | ||||
| -rw-r--r-- | src/features/names.js | 2 | ||||
| -rw-r--r-- | src/nip98.js | 15 | ||||
| -rw-r--r-- | src/nostr-login.js | 8 | ||||
| -rw-r--r-- | tools/feature-registry-check.js | 51 | ||||
| -rw-r--r-- | tools/minimal-smoke.js | 7 | ||||
| -rw-r--r-- | tools/nostr-login-test.js | 2 |
10 files changed, 151 insertions, 78 deletions
@@ -42,7 +42,7 @@ const SW = `const CACHE = 'cold-{{VERSION}}'; // module are available offline too. app-{{VERSION}}.js is the main bundle — // content-addressed, so a new deploy precaches a new name and the old cache // is dropped wholesale. -const SHELL = ['./', 'manifest.webmanifest', 'icon-192.png', 'icon-512.png', 'jsqr.js', 'nip46.js', 'app-{{VERSION}}.js']; +const SHELL = ['./', 'manifest.webmanifest', 'icon-192.png', 'icon-512.png', 'jsqr.js', 'nip46.js', 'app-{{VERSION}}.js'{{EXTRA_SHELL}}]; self.addEventListener('install', (e) => { self.skipWaiting(); @@ -146,6 +146,12 @@ const SW_REGISTER = `navigator.serviceWorker.addEventListener('controllerchange',function(){_ask(navigator.serviceWorker.controller)});` + `addEventListener('load',function(){navigator.serviceWorker.register('sw.js',{updateViaCache:'none'}).catch(function(){})})}</script>`; +// Lazy bundles load as classic scripts into the page's global scope, where +// the main bundle's minified top-level names already live — two Bun outputs +// can mint the same identifier and die with "already been declared". An IIFE +// wrapper gives each its own scope; they publish through window.* on purpose. +const iife = (js) => `(function(){${js}\n})();`; + // Bundle the standalone jsQR decoder (sets window.jsQR) for lazy loading. export async function buildJsQr({ minify = true } = {}) { const result = await Bun.build({ @@ -157,7 +163,7 @@ export async function buildJsQr({ minify = true } = {}) { for (const log of result.logs) console.error(log); throw new Error('jsqr bundle failed'); } - return result.outputs[0].text(); + return iife(await result.outputs[0].text()); } // Standalone bundle for the NIP-46 remote-signer client — loaded on demand by @@ -172,7 +178,7 @@ export async function buildNip46({ minify = true } = {}) { for (const log of result.logs) console.error(log); throw new Error('nip46 bundle failed'); } - return result.outputs[0].text(); + return iife(await result.outputs[0].text()); } // The notify-only push handler for builds without the ark+nwc features (the @@ -228,14 +234,36 @@ export async function buildSwNwc({ minify = true } = {}) { // because it drives ark's headless pay/balance seam. const ALL_FEATURES = { gifts: 'giftsFeature', ark: 'arkFeature', nostrlogin: 'nostrLoginFeature', names: 'namesFeature', zaps: 'zapsFeature', nwc: 'nwcFeature', hats: 'hatsFeature', sync: 'syncFeature', messages: 'messagesFeature' }; +// Features with no first-frame surface load as separate chunks right after +// boot (dynamic import; real files in the split build, inlined in the +// single-file build). Hook precedence is POSITION in the features array, so +// each deferred feature holds its slot with an empty placeholder object — +// every `f[hook]` probe skips it — and is filled in place when it arrives. +// NOT deferrable: ark (balance on the home screen), messages (deep-linked +// profiles + the boot shell), gifts is deferrable because app.js awaits the +// deferred load before the bootUrl check when the path looks like a gift. +const DEFERRED_FEATURES = ['gifts', 'nwc', 'hats']; + export function enabledFeatures(spec = process.env.HAL_FEATURES) { if (spec == null) return Object.keys(ALL_FEATURES); return spec.split(',').map((x) => x.trim().toLowerCase()).filter((x) => x in ALL_FEATURES); } -function featureIndexSource(enabled) { - return enabled.map((f) => `import { ${ALL_FEATURES[f]} } from './${f}.js';`).join('\n') - + `\nexport function buildFeatures(ctx) { return [${enabled.map((f) => `${ALL_FEATURES[f]}(ctx)`).join(', ')}]; }\n`; +export function featureIndexSource(enabled) { + const lazy = enabled.filter((f) => DEFERRED_FEATURES.includes(f)); + const eager = enabled.filter((f) => !lazy.includes(f)); + return eager.map((f) => `import { ${ALL_FEATURES[f]} } from './${f}.js';`).join('\n') + ` +export function buildFeatures(ctx) { + return [${enabled.map((f) => (lazy.includes(f) ? '{}' : `${ALL_FEATURES[f]}(ctx)`)).join(', ')}]; +} +export async function loadDeferredFeatures(ctx, features) { + const late = []; + await Promise.all([${lazy.map((f) => ` + import('./${f}.js').then((m) => { late.push(features[${enabled.indexOf(f)}] = m.${ALL_FEATURES[f]}(ctx)); })`).join(',')} + ]); + return late; +} +`; } const featurePlugin = (enabled) => ({ @@ -274,18 +302,21 @@ function gitCommit() { return 'dev'; } -async function bundleAppJs({ minify, features, staging }) { +async function bundleApp({ minify, features, staging, splitting = false }) { const result = await Bun.build({ entrypoints: ['./src/app.js'], target: 'browser', minify, + // The split build emits real ESM chunks (deferred features arrive as their + // own files); the single-file build inlines the dynamic imports instead. + ...(splitting ? { splitting: true, format: 'esm', naming: { chunk: 'chunk-[hash].[ext]' } } : {}), plugins: [featurePlugin(enabledFeatures(features)), flagsPlugin(staging)], }); if (!result.success) { for (const log of result.logs) console.error(log); throw new Error('bundle failed'); } - return result.outputs[0].text(); + return result.outputs; } // The brand mark for the static boot shell, lifted from brandHeader in @@ -328,7 +359,8 @@ ${pwa ? SW_REGISTER + '\n' : ''}</body> // Self-contained single file (dev server, test tools, save-and-open-offline). export async function buildHtml({ minify = true, pwa = minify, features = process.env.HAL_FEATURES, staging = isStaging() } = {}) { - let js = await bundleAppJs({ minify, features, staging }); + const outputs = await bundleApp({ minify, features, staging }); + let js = await outputs[0].text(); // Guard against a literal </script> inside the bundle closing our tag early. js = js.replaceAll('</script', '<\\/script'); const css = await Bun.file('./src/style.css').text(); @@ -337,15 +369,23 @@ export async function buildHtml({ minify = true, pwa = minify, features = proces } // The hosted split build: a small no-cache index.html (CSS + boot shell) that -// loads the bundle from a content-addressed app-<version>.js. The page stays -// tiny on every load while the heavy bundle is cached by name — a repeat -// visit re-downloads kilobytes, not the whole app. +// loads the app as a content-addressed ESM entry plus chunks — the deferred +// features ride in their own files, fetched right after boot. The page stays +// tiny on every load while the heavy bundles are cached by name — a repeat +// visit re-downloads kilobytes, not the app. export async function buildSplit({ minify = true, features = process.env.HAL_FEATURES, staging = isStaging() } = {}) { - const js = (await bundleAppJs({ minify, features, staging })).replaceAll('{{COMMIT}}', gitCommit()); + const outputs = await bundleApp({ minify, features, staging, splitting: true }); + const entry = outputs.find((o) => o.kind === 'entry-point') || outputs[0]; + const js = (await entry.text()).replaceAll('{{COMMIT}}', gitCommit()); + const chunks = await Promise.all(outputs.filter((o) => o !== entry).map(async (o) => ({ + name: o.path.split('/').pop(), + text: (await o.text()).replaceAll('{{COMMIT}}', gitCommit()), + }))); const css = await Bun.file('./src/style.css').text(); - const html = (await pageHtml({ css, staging, pwa: true, scriptHtml: '<script src="app-{{VERSION}}.js" defer></script>' })) + // module scripts defer by default; the boot shell paints while this loads + const html = (await pageHtml({ css, staging, pwa: true, scriptHtml: '<script type="module" src="app-{{VERSION}}.js"></script>' })) .replaceAll('{{COMMIT}}', gitCommit()); - return { html, js }; + return { html, js, chunks }; } if (import.meta.main) { @@ -353,15 +393,17 @@ if (import.meta.main) { // Hashed with {{VERSION}} still a placeholder, so the page, the bundle // filename and the worker are all stamped with the same token — the page // cannot contain a hash of itself. - const { html: rawHtml, js: rawJs } = await buildSplit({ minify: true }); - const version = Bun.hash(rawHtml + rawJs).toString(36); + const { html: rawHtml, js: rawJs, chunks } = await buildSplit({ minify: true }); + const version = Bun.hash(rawHtml + rawJs + chunks.map((c) => c.text).join('')).toString(36); const html = rawHtml.replaceAll('{{VERSION}}', version); await Bun.write('dist/index.html', html); await Bun.write(`dist/app-${version}.js`, rawJs.replaceAll('{{VERSION}}', version)); + for (const c of chunks) await Bun.write('dist/' + c.name, c.text); // Drop stale content-addressed bundles from earlier local builds so dist/ // mirrors a deploy (the deploy hook rsyncs dist/ wholesale). + const keep = new Set([`app-${version}.js`, ...chunks.map((c) => c.name)]); for (const f of await readdir('dist')) { - if (/^app-[a-z0-9]+\.js$/.test(f) && f !== `app-${version}.js`) await Bun.file('dist/' + f).delete(); + if (/^(app|chunk)-[A-Za-z0-9]+\.js$/.test(f) && !keep.has(f)) await Bun.file('dist/' + f).delete(); } // Lazy-loaded QR decoder — kept out of index.html, fetched only when a @@ -387,6 +429,7 @@ if (import.meta.main) { const nwcSw = feats.includes('nwc') && feats.includes('ark'); const swBody = SW .replaceAll('{{VERSION}}', version) + .replaceAll('{{EXTRA_SHELL}}', chunks.map((c) => `, '${c.name}'`).join('')) .replaceAll('{{NWC_WAKE_FALLBACK}}', nwcSw ? '' : SW_WAKE_ONLY); await Bun.write('dist/sw.js', nwcSw ? swBody + '\n' + await buildSwNwc() : swBody); for (const f of STATIC) await Bun.write('dist/' + f, Bun.file('static/' + f)); @@ -396,6 +439,7 @@ if (import.meta.main) { const kb = (Buffer.byteLength(html) / 1024).toFixed(0); const jkb = (Buffer.byteLength(rawJs) / 1024).toFixed(0); - console.log(`✓ dist/index.html written (${kb} KB) + app-${version}.js (${jkb} KB) — offline: keep the two side by side (or install the PWA)`); + const ckb = (chunks.reduce((n, c) => n + Buffer.byteLength(c.text), 0) / 1024).toFixed(0); + console.log(`✓ dist/index.html (${kb} KB) + app-${version}.js (${jkb} KB) + ${chunks.length} deferred chunk(s) (${ckb} KB) — offline use: install the PWA`); console.log(`✓ PWA: manifest.webmanifest, sw.js (cold-${version}), ${STATIC.length} icons, jsqr.js, nip46.js, ${locales.length} locales`); } @@ -8,12 +8,12 @@ import { Wallet, newMnemonic, isValidMnemonic, accountXpubFor, cacheKeyFor, utxo import { qrSvg } from './qr.js'; import { makeSearcher, resultRows, searchable, punkUrl } from './recipient-search.js'; import { npubOf } from './nostr.js'; -import { nip98Header } from './nostr-login.js'; +import { nip98Header } from './nip98.js'; import { NOSTR_MARK } from './features/nostrlogin.js'; import { scanQr } from './scan.js'; import { dataSources, getSource, setSource, getNetwork, setNetwork, NETWORKS } from './api.js'; import { STAGING } from './build-flags.js'; -import { buildFeatures } from './features/index.js'; +import { buildFeatures, loadDeferredFeatures } from './features/index.js'; import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js'; import { fmtBtc, @@ -1093,6 +1093,7 @@ async function activateAccount(acc, opts = {}) { // threads, decrypted DMs) belong to the old keys, and surviving into this // account they'd both leak the old account's messages on screen and block // the new account's own subscriptions from ever starting. + _featuresInited = false; for (const f of FEATURES) { try { f.stop && f.stop(); } catch {} } activeId = acc.id; // A gift link generates this wallet only to claim into. Keep it provisional @@ -1119,6 +1120,7 @@ async function activateAccount(acc, opts = {}) { persistAccounts(); autoSave(acc); for (const f of FEATURES) { try { f.init && f.init(); } catch {} } // per-feature wallet lifecycle + _featuresInited = true; // a deferred feature landing after this point gets its init() on arrival // A freshly generated identity provably has no profile yet — let features // skip the "is there a kind-0?" wait (avatar would sit blank meanwhile). if (opts.generated) featureHook('identityGenerated'); @@ -1783,6 +1785,7 @@ function lock({ offerPassword = false } = {}) { wallet.stopRealtime(); // A locked session must not keep listening as the account that just left — // and whatever logs in next must not inherit its threads or subscriptions. + _featuresInited = false; for (const f of FEATURES) { try { f.stop && f.stop(); } catch {} } clearAccounts(); vaultPassword = null; @@ -1909,6 +1912,7 @@ function softLock() { } } const keepId = activeId; + _featuresInited = false; for (const f of FEATURES) { try { f.stop && f.stop(); } catch {} } vaultPassword = null; accounts = watch.slice(); @@ -4398,6 +4402,18 @@ const ctx = { }, }; const FEATURES = buildFeatures(ctx); +// Deferred features (gifts, NWC, hats) hold placeholder slots in FEATURES — +// hook precedence is position — and arrive as separate chunks right after +// boot, filled in place. If a wallet opened before they landed, each newcomer +// gets the init() it missed; if the wallet locked again, it doesn't. +let _featuresInited = false; +const _deferredReady = (async () => { + try { + const late = await loadDeferredFeatures(ctx, FEATURES); + if (_featuresInited) for (const f of late) { try { f.init && f.init(); } catch {} } + if (late.length) render(); + } catch (e) { console.error('deferred features failed to load', e); } +})(); // apply text direction, then restore a wallet left open in this tab — otherwise // show the unlock screen. @@ -4467,8 +4483,11 @@ document.addEventListener('visibilitychange', () => { else onAppVisible(); }); window.addEventListener('load', () => setTimeout(() => { if (activeAccount()) claimMigratedName(); }, 1500)); -loadLocale(getLang()).finally(() => { +loadLocale(getLang()).finally(async () => { applyBootAutoLogout(); // clear an overdue session before we read it for claim targets + // A gift link's feature is deferred — wait for it before asking who owns + // the URL, or the claim would fall through to a normal boot and be lost. + if (/^\/(g|ag|lg)\//.test(location.pathname)) await _deferredReady; if (featureHook('bootUrl')) return; // a feature consumed the URL (e.g. a gift claim) if (!restoreAccountsState()) render(); }); diff --git a/src/features/hats.js b/src/features/hats.js index bb9b8b0..43151ad 100644 --- a/src/features/hats.js +++ b/src/features/hats.js @@ -13,7 +13,7 @@ import { t } from '../i18n.js'; import { fmtSats } from '../format.js'; -import { nip98Header } from '../nostr-login.js'; +import { nip98Header } from '../nip98.js'; import { getNetwork } from '../api.js'; const REGISTRAR = 'https://names.coinos.io'; diff --git a/src/features/index.js b/src/features/index.js index 5ecb7c0..a32bced 100644 --- a/src/features/index.js +++ b/src/features/index.js @@ -3,24 +3,33 @@ // build.js replaces this module's contents based on HAL_FEATURES, so a // disabled feature's code — and its network endpoints — never enters the // bundle. See featureIndexSource() in build.js. - -import { giftsFeature } from './gifts.js'; -import { nostrLoginFeature } from './nostrlogin.js'; +// +// This checked-in copy must stay byte-equivalent (comments aside) to +// featureIndexSource(all features) — tools/feature-registry-check.js enforces +// it. Deferred features (gifts, nwc, hats) hold their array position with an +// empty placeholder — hook precedence is position — and are filled in place +// by loadDeferredFeatures right after boot, as separate chunks in the split +// build. NB order is meaningful where hooks stack: gifts' receive takeover +// and balance line come before ark's; zaps sits after ark so an npub prefers +// an instant Ark zap; nwc sits after ark because it drives ark's headless +// pay/balance seam; names sits between ark and zaps so a pasted user@domain +// resolves over DNS (BIP-353) before zaps treats it as a Lightning address. import { arkFeature } from './ark.js'; +import { nostrLoginFeature } from './nostrlogin.js'; import { namesFeature } from './names.js'; import { zapsFeature } from './zaps.js'; import { syncFeature } from './sync.js'; -import { nwcFeature } from './nwc.js'; -import { hatsFeature } from './hats.js'; import { messagesFeature } from './messages.js'; -// NB order is meaningful where hooks stack: gifts' receive takeover and -// balance line come before ark's, matching the pre-plugin layout. zaps sits -// after ark so an npub prefers an instant Ark zap and only falls through to a -// Lightning zap when Ark can't serve it. nwc sits after ark because it -// drives ark's headless pay/balance seam. -// names sits between ark and zaps: a pasted user@domain resolves over DNS -// (BIP-353) before the zaps feature treats it as a plain Lightning address. export function buildFeatures(ctx) { - return [giftsFeature(ctx), arkFeature(ctx), nostrLoginFeature(ctx), namesFeature(ctx), zapsFeature(ctx), nwcFeature(ctx), hatsFeature(ctx), syncFeature(ctx), messagesFeature(ctx)]; + return [{}, arkFeature(ctx), nostrLoginFeature(ctx), namesFeature(ctx), zapsFeature(ctx), {}, {}, syncFeature(ctx), messagesFeature(ctx)]; +} +export async function loadDeferredFeatures(ctx, features) { + const late = []; + await Promise.all([ + import('./gifts.js').then((m) => { late.push(features[0] = m.giftsFeature(ctx)); }), + import('./nwc.js').then((m) => { late.push(features[5] = m.nwcFeature(ctx)); }), + import('./hats.js').then((m) => { late.push(features[6] = m.hatsFeature(ctx)); }) + ]); + return late; } diff --git a/src/features/names.js b/src/features/names.js index cf2aa3c..56fc77d 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -12,7 +12,7 @@ // record falls back to the Lightning-address flow (zaps feature). import { resolveBip353, parsePaymentName, parseBip21 } from '../bip353.js'; -import { nip98Header } from '../nostr-login.js'; +import { nip98Header } from '../nip98.js'; import { qrSvg } from '../qr.js'; import { isArkAddress } from './ark.js'; import { getNetwork } from '../api.js'; diff --git a/src/nip98.js b/src/nip98.js new file mode 100644 index 0000000..aa0343e --- /dev/null +++ b/src/nip98.js @@ -0,0 +1,15 @@ +// NIP-98 HTTP Auth header (kind 27235): the signature covers the exact URL, +// method and body hash, so a captured header is useless anywhere else. Its +// own module — every build needs it (registrar calls), but only some builds +// need the login/backup machinery around it in nostr-login.js. +import { hex } from '@scure/base'; +import { sha256 } from '@noble/hashes/sha256'; + +export async function nip98Header(signer, url, method, bodyText) { + const tags = [['u', url], ['method', method.toUpperCase()]]; + if (bodyText) tags.push(['payload', hex.encode(sha256(new TextEncoder().encode(bodyText)))]); + const evt = await signer.signEvent({ + kind: 27235, created_at: Math.floor(Date.now() / 1000), tags, content: '', + }); + return 'Nostr ' + btoa(JSON.stringify(evt)); +} diff --git a/src/nostr-login.js b/src/nostr-login.js index 6958da0..b032c73 100644 --- a/src/nostr-login.js +++ b/src/nostr-login.js @@ -270,11 +270,3 @@ export async function walletForSigner(signer) { // request with a nostr key. `u` and `method` bind the signature to this exact // request and `payload` to its body, so a captured header can't be replayed // against a different endpoint or a tampered body. -export async function nip98Header(signer, url, method, bodyText) { - const tags = [['u', url], ['method', method.toUpperCase()]]; - if (bodyText) tags.push(['payload', hex.encode(sha256(new TextEncoder().encode(bodyText)))]); - const evt = await signer.signEvent({ - kind: 27235, created_at: Math.floor(Date.now() / 1000), tags, content: '', - }); - return 'Nostr ' + btoa(JSON.stringify(evt)); -} diff --git a/tools/feature-registry-check.js b/tools/feature-registry-check.js index 9b7ac5e..0931156 100644 --- a/tools/feature-registry-check.js +++ b/tools/feature-registry-check.js @@ -1,40 +1,29 @@ -// Guard: src/features/index.js and build.js's ALL_FEATURES must agree. +// Guard: src/features/index.js and build.js's featureIndexSource must agree. // They drifted once (nwc was added to index.js only, so its i18n strings // shipped but its code never did — the settings card silently never rendered, // because build.js SWAPS OUT features/index.js for a generated module). +// +// The strongest form of agreement is equality: the checked-in module must be +// code-identical (comments and whitespace aside) to what featureIndexSource +// generates for the full feature set — imports, construction order, deferred +// placeholder slots, and the deferred loader all included. // bun tools/feature-registry-check.js import { readFileSync } from 'node:fs'; +import { featureIndexSource, enabledFeatures } from '../build.js'; -const idx = readFileSync(new URL('../src/features/index.js', import.meta.url), 'utf8'); -const build = readFileSync(new URL('../build.js', import.meta.url), 'utf8'); +const strip = (s) => s + .replace(/\/\/[^\n]*/g, '') // line comments + .replace(/\s+/g, ' ') // whitespace runs + .replace(/, /g, ',').replace(/ ,/g, ',') + .trim(); -const inIndex = [...idx.matchAll(/import \{ (\w+Feature) \} from '\.\/([\w-]+)\.js'/g)] - .map((m) => ({ fn: m[1], name: m[2] })); -const registry = build.match(/const ALL_FEATURES = \{([^}]*)\}/s)?.[1] || ''; -const inBuild = [...registry.matchAll(/(\w+):\s*'(\w+Feature)'/g)].map((m) => ({ name: m[1], fn: m[2] })); +const idx = readFileSync(new URL('../src/features/index.js', import.meta.url), 'utf8'); +const gen = featureIndexSource(enabledFeatures(undefined)); -let ok = true; -for (const f of inIndex) { - if (!inBuild.find((b) => b.fn === f.fn)) { - console.log(`x ${f.fn} (${f.name}.js) is in features/index.js but NOT in build.js ALL_FEATURES — it will not ship`); - ok = false; - } -} -for (const b of inBuild) { - if (!inIndex.find((f) => f.fn === b.fn)) { - console.log(`x ${b.fn} is in build.js ALL_FEATURES but not imported in features/index.js`); - ok = false; - } -} -// Compare the ORDER FEATURES ARE CONSTRUCTED IN (hook precedence depends on -// it), not the order they happen to be imported in. -const built = [...(idx.match(/return \[([^\]]*)\]/s)?.[1] || '') - .matchAll(/(\w+Feature)\(ctx\)/g)].map((m) => m[1]); -const orderIdx = built.join(','); -const orderBuild = inBuild.map((b) => b.fn).join(','); -if (ok && orderIdx !== orderBuild) { - console.log(`x feature ORDER differs (hook precedence depends on it)\n index.js: ${orderIdx}\n build.js: ${orderBuild}`); - ok = false; +if (strip(idx) !== strip(gen)) { + console.log('x src/features/index.js does not match build.js featureIndexSource (full set)'); + console.log('\n--- generated (normalized) ---\n' + strip(gen)); + console.log('\n--- checked in (normalized) ---\n' + strip(idx)); + process.exit(1); } -if (ok) console.log(`ok ${inIndex.length} features registered consistently`); -process.exit(ok ? 0 : 1); +console.log(`ok features/index.js matches the generator (${enabledFeatures(undefined).length} features, deferred slots included)`); diff --git a/tools/minimal-smoke.js b/tools/minimal-smoke.js index bd0d588..f3f896c 100644 --- a/tools/minimal-smoke.js +++ b/tools/minimal-smoke.js @@ -29,7 +29,10 @@ check('no ark protocol code', !html.includes('Ark VTXO mailbox authorization')); check('no swap protocol code', !html.includes('swap/reverse')); check('no musig code', !html.includes('MuSig/noncecoef')); check('no silent-payment engine', !html.includes('BIP0352') && !html.includes('__SP_WORKER__')); -check('no nostr sync code', !html.includes('30078') && !html.includes('relay.coinos.io')); +// The sync EVENT KIND must be gone; the coinos relay hostname alone is not a +// sync marker anymore — recipient search (send-to-npub, core UX) carries +// PROFILE_RELAYS for profile lookups even in the minimal build. +check('no nostr sync code', !html.includes('30078')); const server = Bun.serve({ port: 5198, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); @@ -62,6 +65,8 @@ try { await sleep(400); console.log('\n[1] import + core wallet'); + await clickText('button', 'Get started'); // the front door gained a step + await sleep(300); await clickText('button', 'Import existing'); await sleep(200); await page.type('textarea', generateMnemonic(wordlist)); diff --git a/tools/nostr-login-test.js b/tools/nostr-login-test.js index 468ffb4..25983a4 100644 --- a/tools/nostr-login-test.js +++ b/tools/nostr-login-test.js @@ -58,7 +58,7 @@ check('another account cannot decrypt it', denied); console.log('\n[NIP-98 auth]'); { - const { nip98Header } = await import('../src/nostr-login.js'); + const { nip98Header } = await import('../src/nip98.js'); const { verifyEvent } = await import('nostr-tools/pure'); const { sha256: sha } = await import('@noble/hashes/sha256'); const body = JSON.stringify({ name: 'x' }); |
