Commit Graph
7 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Fable 5 fa9e2d9665 Automated passkey e2e: real Chromium, real WebAuthn ceremonies, virtual PRF
Discharges most of the 'live-fire passkey on a real device' TODO without
hardware. Playwright drives headless Chromium with a CDP virtual
authenticator (hasPrf: true) against the built demo served locally and
pointed at three in-process mock relays, so the test is hermetic -- no
public relays, no platform passkey provider, no network.

Covered end to end: navigator.credentials.create, PRF extension results
crossing the browser boundary, worker-side derivation, account
registration (asserted at the PROTOCOL layer -- kind-30078 capsules
really land on the relays, not just a UI state), the Tier B2 phrase card
appearing for a first session and not a returning one,
window.bitlogin.activeSession() reporting, logout, then
navigator.credentials.get signing back in to the SAME npub. A second
test proves PRF is per-credential: a different passkey derives a
different account.

Two things the harness taught us, both now encoded: WebAuthn rejects a
bare 127.0.0.1 as an RP id (localhost only), and the options menu's
disclosure state survives re-renders by design, so the driver opens it
conditionally.

Runs inside npm test and skips itself when the Playwright browser isn't
installed, so CI and other machines stay green. MockRelay gains
storedEventKinds() for protocol-level assertions. Remaining real-device
gap is recorded in TODO: a genuine platform authenticator and the
create-time-PRF-absent fallback, which the virtual authenticator does
not exercise.

268 tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 14:57:10 -04:00
thePR0M3TH3ANandClaude Fable 5 f0b5f40fd4 Audit response: fix BL-16 through BL-20 and the low findings, same day
BL-16 (high): a remote signer's auth_url is attacker-controlled input that
became a clickable href. sanitizeAuthUrl in core now admits only https
(or loopback http) -- javascript:/data:/blob:/custom schemes are dropped
without consuming the surfacing slot -- and the widget link shows the
hostname. Hostile-scheme tests at both layers.

BL-17: the post-creation PRF fallback assertion is pinned via
allowCredentials to the credential that was just created, so a multi-
passkey account sheet can no longer silently bind an older passkey's
account.

BL-18: queryOnce's timeout path sent CLOSE before settling, unguarded --
a relay dying mid-query stranded the promise and everything queued behind
the worker's serial queue. The send is guarded; settling is unconditional.
MockRelay.close() now terminates live sockets (it previously waited on
them forever), and a suppressEose switch models the silent relay.

BL-19: relays are no longer trusted to honor limits. Buffered queries
enforce filter.limit locally, drop replayed duplicate ids, and cap at
1000 events; a 20-copy replay now yields one event.

BL-20: the .semgrep.yml try/catch fallback pattern never parsed, so the
entropy rules silently did not run. Rewritten in parseable form and
verified under the pinned Semgrep 1.171.0: fires on hostile code, exits
0 on the clean tree.

Lows: NIP-07 and NIP-46 verify returned signed events (signature, pinned
session identity, field equality with the request); Nip46Client.close()
wipes the client and conversation keys (§11.10); passkey labels carry a
short suffix so multiple accounts are distinguishable; postcss bumped to
8.5.25 (GHSA-fxqj-rqcc-2cmp); GitHub Actions pinned to commit SHAs.
Deferred as interop (recorded in TODO): NIP-46 protocol logout and
switch_relays. 12 new adversarial tests; 256 total.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 13:10:20 -04:00
thePR0M3TH3ANandClaude Fable 5 49682dd75c NIP-46 sign-in: remote-signer sessions behind the same facade, memory-only
Implements login-methods.md §LM5 phase one. Core gains nostr/nip46.ts -- a
Nip46Client speaking kind-24133 RPC with NIP-44 transport encryption,
bunker:// parsing, nostrconnect listen (secret echo is proof-of-scan),
auth_url surfacing, and typed deadline errors -- plus subscribeLive on
RelayConnection with re-REQ on reconnect, and live post-EOSE delivery in
the MockRelay so tests exercise real round trips against a fake bunker.

The ephemeral client key and Nip46Client live in the crypto worker
(§LM5.4); nip46* actions bypass the serial queue so a signer waiting
minutes for human approval cannot block other worker calls, and the
signer's auth_url reaches the UI as the worker's first notification frame.

The widget's connect screen runs both legs at once: a nostrconnect QR --
rendered as dot-style inline SVG with rounded finder eyes, per owner
preference -- for phone signers, and a bunker:// paste box (password
input; the URI carries a token). window.nostr is now an element-routed
provider, so a NIP-46 session claims the slot and hosts reading it get
the remote signer transparently; NIP-07 still leaves the slot to the
extension. Sessions stay thin per §LM7: no vault, no persistence.
Persistence remains gated on the external vault review (§LM5.3).

14 new tests (core fake-bunker e2e, QR geometry, signer delegation) and
two policy pins: bunker option below the password path, element stays
web-storage-free.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 10:31:36 -04:00
thePR0M3TH3ANandClaude Fable 5 c3e55add16 Widget: requestNwcConnection() -- vault authorization layer, reveal mode
The Phase D surface from vault-ux.md, scoped to NWC and honest about its
boundary: an embedded same-origin widget cannot broker (§CV12.3), so the
API reveals, says so in its consent copy, and leans on the wallet-side
budget as the real security boundary.

One public call owns the whole ceremony. If nobody is signed in, the
sign-in flow runs first and the request resumes via the goto("dashboard")
hook exactly once; if the account has a connection bound to the
requesting origin, the user gets the one-tap approval screen the vault
exists for; otherwise a guided import -- Bitcoin Connect's wallet
chooser or a pasted URI. The BC integration lifts Satisfied's
production discipline verbatim: chooser only, lazy chunks (never
downloaded unless the flow opens), no numeric budget in
authorizationUrlOptions (the unit is not interoperable), disconnect and
clear bc:config so no plaintext copy outlives the ceremony.

The worker holds the vault root (never the sudo key -- wiped on
arrival) and gains vaultStatus/List/SaveNwc/FindForOrigin/RevealNwc/
SetBinding/Delete actions; the root rides the encrypted session cache
so restored sessions keep working, with a vaultEnabled marker so a
stale cache says "sign in again" instead of falsely claiming the
account has no vault. Publishing failures are loud -- a connection that
reached no relay is not saved anywhere and says so. The dashboard gains
Wallet connections management with "revoke app access" and "remove
from BitLogin" honestly separated from wallet-side revocation.
VaultSession gains buildDeletionRequest so the NIP-09 courtesy ask is
signed without exposing the signing key.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 13:10:39 -04:00
thePR0M3TH3ANandClaude Sonnet 5 1dc5e6802c Fail closed on credential-generation rollback; fix NIP-44 extended-length payloads
Found during a real integration pass (embedding this widget in BitRoad, a
separate Nostr commerce app):

- Password rotation's old-locator tombstone (§18.1) is only advisory -- a
  relay that never processes it can keep serving the pre-rotation capsule
  indefinitely. loginWithPassword/changePassword previously accepted that
  stale capsule with only a non-blocking warning. A device that has already
  seen the newer generation now refuses it (RollbackDetectedError) unless the
  caller explicitly passes acknowledgeRollback: true for the narrower
  relay-lag case. The widget surfaces this as a blocking confirmation screen
  before claiming the signer, not a banner shown after the fact.
- NIP-44 capped plaintext at 65535 bytes; added the extended-length prefix
  for payloads up to 2^32-1 bytes, verified byte-for-byte against the
  nostr-tools reference implementation (conversation keys, padding at every
  bucket boundary, round trips in both directions).
- <bitlogin-auth> now exposes nip44Encrypt/nip44Decrypt directly on the
  element instance, alongside the existing getPublicKey/signEvent, so a host
  page can reach every capability without racing other providers for the
  single window.nostr slot.

docs/spec.md bumped to v0.4 with the full writeup (§16.2, §29.7). 75/75
tests passing (14 new NIP-44 tests, 2 new rollback e2e tests).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 14:36:35 -04:00
Claude 21dd474781 Add GitHub Pages deployment workflow for the demo site
Builds core -> widget -> demo and publishes packages/demo/dist via
actions/upload-pages-artifact + deploy-pages. Also makes the manifest and
service worker's precache list path-relative (instead of root-absolute)
so the same build works whether it's served from a domain root or a
GitHub Pages project-site subpath like /bitlogin/.

Repo still needs Settings -> Pages -> Source set to "GitHub Actions" once
if it has never been enabled before.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
2026-07-21 03:53:52 +00:00
Claude 5ee63ae6b0 Implement BitLogin as a modular protocol library, embeddable widget, and demo site
Builds out the v0.3 spec as an npm workspace monorepo any static site can
consume for login and Nostr signing:

- packages/core: framework-free protocol implementation — Argon2id/HKDF/
  ScalarExpand/AES-256-GCM/JCS crypto primitives, NIP-01 event handling and a
  quorum-aware relay pool, credential/recovery capsule construction and
  validation, and the full account lifecycle (registration, any-device
  login with rollback/relay-disagreement detection, phrase recovery with
  recovery-capsule refresh, password rotation with mandatory tombstone +
  NIP-09 deletion, replica repair, recovery export, NIP-19 encoding, a
  NIP-44 v2 implementation, and a NIP-07-shaped signer). 49 tests cover the
  spec's Phase 0 byte-level vectors plus full create/login/recover/rotate
  end-to-end scenarios against an in-memory mock relay.

- packages/widget: a <bitlogin-auth> custom element with a dedicated crypto
  Web Worker (private keys never touch the main thread) and a window.nostr
  NIP-07-compatible provider, so existing Nostr web apps work against it
  with zero code changes. Built with Vite into two relocatable static files.

- packages/demo: a plain static site (no build step of its own) consuming
  the widget — a branding/landing page, an integration guide, and an
  account-manager page covering create/login/recover, password rotation,
  and identity export.

All flows were verified twice: protocol-level tests against a mock relay,
and the actual built widget driven end-to-end in real Chromium (registration,
phrase confirmation, event signing, clean-device login, window.nostr, and
phrase recovery all reproduce the identical everyday pubkey).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
2026-07-21 03:44:10 +00:00