diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17aaea4..72df877 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,11 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: .nvmrc - # The tests and build.js only use Node's standard library and the - # bundles committed in src/www/js/lib, so nothing is installed. + cache: npm + # The tests load a few packages (QR and UR decoding, descriptors) from the + # lockfile. Install scripts are not needed for the tests or build.js. + - name: Install + run: npm ci --ignore-scripts - name: Tests run: npm test - name: dist/index.html matches the source diff --git a/README.md b/README.md index d16b2ac..6c7db10 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ node build.js sha256sum dist/index.html ``` -`build.js` needs Node.js only; no packages are installed. The bundled libraries in `src/www/js/lib` are committed build outputs: six are built with esbuild by the `build_*.js` scripts from the pinned versions in `package-lock.json`, the rest are older browserify builds from `libs/`. +`build.js` needs Node.js only; no packages are installed. The bundled libraries in `src/www/js/lib` are committed build outputs: seven are built with esbuild by the `build_*.js` scripts from the pinned versions in `package-lock.json`, the rest are older browserify builds from `libs/`. Before tagging a release, `npm run release:check` runs the tests, rebuilds `dist/index.html` and fails if the committed file differs from the source. diff --git a/build_qrscan.js b/build_qrscan.js new file mode 100644 index 0000000..d7bbace --- /dev/null +++ b/build_qrscan.js @@ -0,0 +1,48 @@ +// Bundle src/www/js/lib/_qrscan_entry.js β src/www/js/lib/qrscan.js using +// esbuild + node-polyfill. Run via `node build_qrscan.js`. +// +// jsQR decodes QR codes; @ngraveio/bc-ur reassembles animated UR codes. Node's +// crypto module gets the same small shim as build_sp.js, and the build fails +// if the full crypto polyfill ends up bundled. + +const path = require('path'); +const esbuild = require('esbuild'); +const { polyfillNode } = require('esbuild-plugin-polyfill-node'); + +const cryptoShim = { + name: 'crypto-shim', + setup(build) { + build.onResolve({ filter: /^crypto$/ }, () => ({ + path: path.join(__dirname, 'src/www/js/lib/_crypto_shim.js'), + })); + }, +}; + +esbuild.build({ + entryPoints: ['src/www/js/lib/_qrscan_entry.js'], + bundle: true, + format: 'iife', + globalName: 'qrScan', + minify: true, + target: 'es2020', + outfile: 'src/www/js/lib/qrscan.js', + metafile: true, + plugins: [ + cryptoShim, + polyfillNode({ + globals: { buffer: true, process: true }, + polyfills: { crypto: false, buffer: true, process: true, util: true, stream: true, events: true, string_decoder: true, assert: true }, + }), + ], + define: { + 'global': 'globalThis', + }, +}).then((result) => { + const leaked = Object.keys(result.metafile.inputs) + .filter((input) => /crypto-browserify|node_modules\/elliptic\//.test(input)); + if (leaked.length) throw new Error(`qrscan.js must not bundle these: ${leaked.join(', ')}`); + console.log('qrscan.js bundled'); +}).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/dist/index.html b/dist/index.html index f193e9a..1f6b327 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1349,8 +1349,10 @@ border: none; outline: none; cursor: pointer; - padding: 10px 16px; - font-size: 1.125rem; + padding: 10px 8px; + font-size: 0.875rem; + font-weight: 600; + line-height: 1.3; transition: background-color 120ms, color 120ms; border-radius: 7px; } @@ -3790,6 +3792,69 @@ .cards { grid-template-columns: 1fr; } } +/* ============================================================ + Load tabs, word boxes, button rows, QR scanner, Nostr keys + ============================================================ */ +.tab button span[aria-hidden="true"] { + font-size: 1.125rem; + margin-right: 4px; +} +.tab button:focus-visible { + outline: 2px solid var(--q-accent-hi); + outline-offset: 2px; +} +.inputMnemonic-word.word-invalid { + border-color: var(--q-danger); + box-shadow: 0 0 0 1px var(--q-danger); +} +.button-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin: 8px 0; +} +.qr-scan__video { + display: block; + width: 100%; + max-height: 50vh; + object-fit: cover; + border-radius: var(--radius-sm); + background: #000; + margin: 8px 0; +} +.qr-scan__progress { + height: 6px; + border-radius: 3px; + background: var(--q-border-soft); + overflow: hidden; + margin: 8px 0; +} +.qr-scan__bar { + height: 100%; + width: 0; + background: var(--q-accent-hi); + transition: width 150ms; +} +.qr-scan__file { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + overflow: hidden; +} +.qr-scan__file:focus-visible + label { + outline: 2px solid var(--q-accent-hi); + outline-offset: 2px; +} +.nostr-keys details { + margin: 8px 0 16px; +} +.nostr-keys summary { + cursor: pointer; + color: var(--q-muted); +} + @@ -3875,6 +3940,22 @@
Three ways to load a seed: Pick a tab below. π€ generates a fresh random mnemonic, π₯ lets you type one you already have, and π² takes raw entropy from dice / coins / cards. Once a seed is loaded you can add a BIP39 passphrase, and the rest of the toolkit unlocks.
+Three ways to load a seed: Pick a tab below. Generate makes a fresh random mnemonic, Enter words lets you type one you already have, and From entropy takes raw entropy from dice, coins or cards. Once a seed is loaded you can add a BIP39 passphrase, and the rest of the toolkit unlocks.
@@ -4160,34 +4241,39 @@
NOTE: You can enter your BIP39 Passphrase below.
- - + + + +Import this into a wallet such as Sparrow or Bitcoin Core to watch this account. It holds public keys only. Wallets that don't accept the combined <0;1> form take the receive and change descriptors below instead.
Note these addresses are derived from the BIP32 Extended Key.
Please be patient, each search can take up to 20 seconds!
+Enter an address you know belongs to this wallet and the path it came from. The search checks the passphrase in the Seed Workspace, or each passphrase you list below.
- + +Each line is tried exactly as typed, spaces included. Most wallets use the first 20 addresses, so 20 is usually enough.
+A Nostr identity is a secp256k1 key pair, so your seed can back it up too. Derive keys the NIP-06 way, which Nostr apps that import seed words use, or as separate unlinkable identities with BIP-85.
+Anyone with an nsec can post as you. Use test seeds here unless you are working offline.
+Load a seed first to derive Nostr keys. Load a seed β
+Path m/44'/1237'/0'/0/0. Most apps that import seed words use account 0. Your BIP39 passphrase, if you entered one, is part of these keys.
Path m/83696968'/128002'/1'/1'. Each identity is unlinkable from the others and from your wallet, and each account is a separate key within an identity. Numbering starts at 1 because the spec reserves 0.