Commit Graph
63 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Fable 5 a7adef05f9 NIP-07 sign-in: delegate to an installed extension behind the same facade
Implements login-methods.md §LM4. New signers/ layer in the widget:
detectForeignNip07Provider never mistakes BitLogin's own provider for an
extension, and Nip07Signer wraps the snapshotted provider with capability
probing (typed SignerUnsupportedError instead of TypeErrors off missing
nip44), deadlines on every call (SignerTimeoutError -- no silent hangs),
pubkey validation, and unsigned-event normalization.

The element routes its public API through the active signer, so hosts get
the extension backend transparently. Extension sessions are thin (§LM7):
no capsule, no vault (requestNwcConnection resolves null), no persistence,
and window.nostr is left to the extension -- claimSigner stays reserved
for BitLogin-account sessions. bitlogin-login now carries method and
capabilities; publicKey is unchanged for existing hosts.

Password-first pinned (owner decision, §LM9.1): the extension option
renders as a secondary link under the primary sign-in, enforced in
securityPolicy.test.ts alongside a pin that the extension confirm path
never claims window.nostr or writes storage. 14 new signer tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 09:28:24 -04:00
thePR0M3TH3ANandClaude Fable 5 20a3c50dc2 Design doc: the widget as the single login surface (NIP-07, NIP-46)
Proposes aggregating sign-in methods behind the existing provider facade
instead of being one option in a host's login menu. NIP-07 delegates to an
installed extension; NIP-46 connects to a remote signer, memory-only until
the vault review gate clears. Rejects nsec paste as a login method on the
record — §SF10 import is the sanctioned path for raw keys. Non-BitLogin
sessions are signer-only: no capsule, no vault, no portable state.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 09:20:49 -04:00
thePR0M3TH3ANandClaude Opus 5 205b0678cc Pin that pre-removal passwords still sign in
Manual passwords were removed from the widget on 2026-07-31. That change
took away the CONTROLS -- you can no longer type your own password when
creating, recovering, or rotating -- but it did not change the derivation
chain and it did not add any check on the login path. Accounts created with
a hand-typed password therefore still sign in exactly as before.

That distinction is load-bearing and was previously unwritten. BitLogin
keeps no server-side record, so if sign-in ever stopped accepting a password
shape it had previously issued, those accounts would be reachable only
through their recovery phrase. Nothing in the suite said otherwise, and
securityPolicy.test.ts pins only the absence of the creation/recovery/
rotation controls.

Four tests: register-then-login with a hand-typed password, wrong password
still refused, the real migration path (sign in with the old typed password,
rotate onto a generated one, old one stops working), and derivation across
the password shapes the old UI accepted -- short, spaced, symbol-laden,
whitespace-padded, unicode, and 256 characters. No length floor, charset
rule, or entropy gate exists on the derivation path, which is exactly what
keeps those accounts reachable.

Separately confirmed by diff rather than by test: normalize.ts, argon2id.ts,
hkdf.ts and scalarExpand.ts are unchanged between efd994c -- the build
currently vendored by bitroad, bitunlock, bitvid and satisfied -- and HEAD.
Had any of them moved, every password would break, generated ones included.

Suite 185 -> 189.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 22:04:01 -04:00
thePR0M3TH3ANandClaude Opus 5 98b2967116 Ban Math.random package-wide and enforce it in CI
Neither remaining Math.random call was a secret. The backup-quiz picker
chooses which three recovery words to re-type -- an attacker who cannot see
the phrase gains nothing from predicting them -- and the relay subscription
id is a public wire identifier scoped to one socket, where collision is the
only failure mode.

They are changed anyway, because the point is the rule rather than these two
call sites. A Math.random ban that carries a standing exception is worth
much less than one that does not: the exception is what a future misuse
hides behind, and reviewers stop reading a warning they have already agreed
to ignore twice. Both now use the CSPRNG (randomUniformInt, and
bytesToHex(randomBytes(8)) for the subscription id, same 16-hex-character
shape as before), which costs nothing here.

With no exception left, the rule can be absolute. .semgrep.yml adds three
rules over packages/*/src/**, run in CI as a separate step after
--config auto:

- bitlogin-no-math-random: no Math.random anywhere, secret or not
- bitlogin-no-crypto-fallback: no `?? fallback`, `|| fallback`, or
  catch-and-substitute around getRandomValues -- the COLDCARD defect class
- bitlogin-no-time-or-id-seeded-secrets: no hashing Date.now() or
  performance.now() into a key

Verified the rules fire rather than merely passing, which is the failure
mode of a lint rule nobody tests: reintroducing the old
Math.floor(Math.random() * pool.length) produces 1 blocking finding and exit
1, and the tree is clean at 0 findings with it restored. Path patterns are
anchored (/packages/...) so semgrep's upcoming Semgrepignore v2 change does
not silently alter their scope.

Typecheck and the full suite pass (185 tests).

This closes the entropy audit for this repo. The only gate remaining is an
independent review of the RNG path by someone other than the author.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 17:30:24 -04:00
thePR0M3TH3ANandClaude Opus 5 e131f0313b Take nonce and IV control off the public crypto surface
nip44Encrypt and nip04Encrypt took optional nonceOverride / ivOverride
parameters. No production caller ever passed one -- but they were exported
from @bitlogin/core/crypto, so "encrypt with a nonce I chose" was a
discoverable feature of the package rather than an internal detail.

That is worth closing because neither primitive degrades gracefully. NIP-44
is ChaCha20: a nonce reused under one conversation key recovers plaintext
outright. NIP-04 is AES-CBC, where a repeated IV leaks block-equality
between messages and a predictable one gives an attacker considerably more.
A parameter that silently accepts a fixed nonce should not sit one
autocomplete away from application code.

The public functions now take no nonce or IV and always draw from the
CSPRNG. The capability moved to crypto/testing.ts as
__unsafeNip44EncryptWithNonce / __unsafeNip04EncryptWithIv, which is absent
from both crypto/index.ts and the package.json exports map. crypto/index.ts
also switches from `export *` to an explicit re-export list for these two
modules, so the shared encryptWithNonce / encryptWithIv helpers stay off the
surface even though their modules must export them for the test entry point
to reach them.

Closing this cost nothing: exactly one test used an override -- a
nonce-length validation check -- and ivOverride had no callers at all. The
official NIP-44 vectors do not need it.

Verified from a throwaway consumer package linking the BUILT @bitlogin/core
rather than from source. Deep imports of crypto/testing.js,
dist/crypto/testing.js and crypto/nip44.js all fail with
ERR_PACKAGE_PATH_NOT_EXPORTED; the crypto surface contains neither
encryptWithNonce nor encryptWithIv; and nip44Encrypt/nip04Encrypt report
arity 2/3, i.e. no nonce or IV parameter.

Adds four regression tests pinning the closed surface, including that 64
calls with identical plaintext produce 64 distinct nonces and IVs.

knownAnswer.test.ts stayed green throughout, which is the signal that
matters here: no capsule bytes change and no existing account is affected.
Typecheck, build and the full suite pass (185 tests).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 17:20:05 -04:00
thePR0M3TH3ANandClaude Opus 5 b85e1572ea Pin fail-closed RNG behaviour and randomUniformInt's uniformity
crypto/random.ts is the single chokepoint for every secret this codebase
mints -- recovery entropy, everyday keys, vault roots, AEAD nonces, account
ids, generated passphrases. It is correct today: it throws when no secure
source exists rather than falling back, and rejection-samples to avoid modulo
bias. Nothing in CI would have reported it if either stopped being true.

That is the gap this closes. The existing suite is strong on known-answer
vectors -- HKDF, ScalarExpand, Argon2id, BIP-39, Schnorr, GCM tamper
detection, NIP-44 official vectors -- but every one of them is satisfied by a
deterministic stream. Same input, same output. That is exactly how a
predictable RNG survives a health check that only asks whether the output
looks random.

Adds two blocks, nine tests:

- Fail-closed: no crypto object, crypto without getRandomValues, and
  getRandomValues present but not callable must each throw, and
  randomEntropy128 / generatePrivateKey / randomUniformInt must abort rather
  than substitute a value. Plus a check that normal operation resumes once a
  source is available again, so the guard cannot be satisfied by breaking
  everything permanently.
- Uniformity: chi-square over 200k draws at modulus 94, full-range coverage
  at the EFF wordlist modulus of 7776, rejection of invalid bounds, and the
  degenerate modulus of 1.

Both mutation-verified rather than assumed. Replacing the throw with a
zero-filling stub turns five fail-closed tests red. Replacing rejection
sampling with a bare `value % maxExclusive` gives chi-square 5614 against a
threshold of 147 -- while leaving every other test in the suite green, which
is the whole reason the distribution test has to exist.

Tests only; no source or behaviour change. Suite 172 -> 181.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 16:29:21 -04:00
thePR0M3TH3ANandClaude Opus 5 ba49673ef9 Entropy audit: RNG integration review and open hardening items
Audits every security-sensitive random value in packages/ against a
Coldcard-class checklist: where each bit of unpredictability originates,
whether that source is present in the production build, and what happens
when it fails.

No predictable-fallback defect. crypto/random.ts is the single chokepoint
for every secret this codebase mints and it throws rather than degrading;
randomUniformInt rejection-samples, so wordlist and charset selection carry
no modulo bias; generatePrivateKey rejection-samples valid scalars. Crypto
deps are exact-pinned and CI already emits an SBOM.

Three hardening items recorded, in the order they should be done:

- M2: nothing tests that the RNG fails closed. Additive, cannot touch the
  derivation chain, and it is what protects M1's fix from regressing later.
- M1: nonceOverride/ivOverride are reachable from the public
  @bitlogin/core/crypto surface. No production caller passes them, but
  nonce reuse in NIP-44/NIP-04 is immediate plaintext recovery, so the
  parameter should not be one autocomplete away from application code.
- L1: Math.random in the backup-quiz word picker. Not a secret, but it
  forces a standing exception in any Math.random ban rule.

Docs only; no source or behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 16:26:35 -04:00
thePR0M3TH3ANandClaude Fable 5 30d4ac4b83 Open items, and the standing rule about the derivation chain
Records the Dependabot PR to close and the manual-password decision to
confirm, plus the deferred hardening tiers and the gates still in force.

The part worth keeping visible: knownAnswer.test.ts failing is never "update
the expected value" -- it means the change is account-breaking. The rest of
the crypto suite only checks self-consistency, which stays green through
exactly the upgrade that would lock every user out.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 14:52:30 -04:00
thePR0M3TH3ANandClaude Fable 5 6eb1d7d640 Pin the derivation chain, and stop Dependabot grouping crypto majors
A Dependabot PR failed the build by bundling @noble/hashes 1.8->2.2,
@noble/curves 1.9->2.2, @scure/base 1.2->2.2, @scure/bip39 1.6->2.2,
TypeScript 5->7, Vite 6->8 and Vitest 3->4 into a single change. Production
was never affected -- only that preview -- and the cause is real rather than
flaky: noble v2 exports only ".js"-suffixed subpaths, while this code imports
the extensionless "@noble/hashes/sha2" that 1.8 provides.

Failing to compile was the LUCKY outcome. The tests could not have caught the
dangerous version of this. Account capsules live on public relays, and the
login name plus password derive the locator, signing scalar and capsule key
deterministically -- so those outputs are a compatibility contract with every
account that already exists. The crypto suite only checked self-consistency
(same input, same output within one run), which stays green even if HKDF,
base64url framing, BIP-39 seeding or JCS ordering all change. A grouped major
that compiled would have shipped and locked every existing user out, with no
server-side record to migrate them from.

So this adds knownAnswer.test.ts, pinning the actual outputs of HKDF-Extract,
HKDF-Expand, ScalarExpand (scalar, counter and resulting public key), BIP-39
phrase and seed, base64url, and JCS ordering. The vectors are captured from
the shipping implementation, and the header says plainly what a failure means:
not "update the expected value", but "this upgrade is account-breaking".

dependabot.yml now groups only minor and patch updates. Majors are still
proposed, individually, so each can be judged on its own. Majors of the
derivation chain itself (@noble/*, @scure/*, hash-wasm, canonicalize) are
ignored entirely -- that is deliberate work with the vectors green, not an
automated PR waiting to be merged on a quiet afternoon.

172/172 tests, typecheck clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 14:46:46 -04:00
thePR0M3TH3ANandClaude Fable 5 7d2209d706 Second-audit hardening: phrase-gated sudo key, deterministic migration, policy tests
Authored by ChatGPT/Codex in a separate review pass. Reviewed and verified
here before committing rather than taken on trust; findings below.

The substantive change is that the vault sudo key now lives ONLY in the
phrase-gated recovery capsule. A newly written password credential capsule
carries just connection_vault_root, so an ordinary login restores
connectable-tier records without putting personal-tier material behind a
password capsule that is downloadable and therefore offline-guessable forever.
Pre-hardening capsules stay readable, password login does not return their
sudo key, and the next rotation scrubs it. That closes the gate this project
had explicitly set on personal-tier UI.

Legacy vault-enable migration now derives roots deterministically from
phrase-protected material and account_id, so two concurrent attempts cannot
mint divergent roots -- the failure mode the docs previously mitigated with
"enable the vault once, from one device".

Manual (user-chosen) passwords are REMOVED, not merely discouraged, and a new
securityPolicy.test.ts pins that along with several things tests do not
usually reach: the deployment CSP (default-src 'none', frame-ancestors
'none') plus six baseline headers, demo pages free of inline scripts and
remote fonts, and README documenting same-origin self-hosting only rather
than a remote demo embed. llms.txt was updated to match, which I checked
because that test only reads README.

What I verified, since a passing suite proves less than it appears to:

- All 12 fixes from the 2026-07-30 audit survived the rewrite of recover.ts,
  changePassword.ts, validation.ts, relay.ts, session.ts and element.ts.
  Checked semantically where grep cannot help: changePassword still checks
  publish success BEFORE building the tombstone, and vaultSetBinding still
  unbinds on origin === null rather than silently rebinding to the worker
  global.
- No dependency changes; the package.json diff is scripts only.
- Argon2id and KDF parameters unchanged. The NIP-44 diff ADDS a base64 length
  cap before decoding, a memory-DoS guard.
- Validation increased rather than decreased: 44 throws added, 33 removed.
- No disabled or skipped tests, no secrets in the diff.

typecheck clean, 165/165 tests, build green, and the account manager exercised
in a browser: creation correctly refused when relay quorum could not be met,
and on retry failed at the recovery capsule with "No login credential was
published" -- the §18.1 fail-closed ordering doing its job against a flaky
public relay set.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 13:54:36 -04:00
thePR0M3TH3ANandClaude Fable 5 efd994c4de Fix revoke-rebinds bug; correct the website's custody and revoke claims
The audit's most important finding was not a doc issue at all. In
vaultSetBinding the identifier `origin` resolved to the WORKER GLOBAL
self.origin rather than the intended value, so the dashboard's "Revoke
app access" REBOUND the record to the calling page instead of clearing
it -- the precise cross-origin adoption the binding guard exists to
prevent, wearing the label of the button meant to prevent it. It
typechecked because the DOM lib declares that global. I introduced this
hours ago: a string replace intended for one call site silently matched
two. Fixed, with the reasoning written where the next reader will hit it.

Relay/account audit fixes:
- Phrase recovery permanently tripped the rollback alarm. The mark is
  keyed by the everyday pubkey (unchanged by recovery) while recovery
  legitimately restarts at generation 0, and raiseHighWaterMark keeps
  the maximum -- so anyone who had ever rotated a password met "possibly
  revoked credentials" on EVERY later login, forever, with no way to
  clear it. The phrase is the master authority; it now resets the mark.
- quorumMet counted TCP connections, not answers: a query timeout
  RESOLVED, so a connectable-but-silent relay counted as responding and
  every "not enough relays answered" guard was near-vacuous. Only EOSE
  is an answer now.
- Relay responses were never checked against the address requested, so
  one junk event from one relay blocked registration, rotation, and
  recovery for every login name and password a user tried. Events must
  now match author, kind, and d-tag.

Website honesty (the surface a stranger reads first, and the one that
had drifted furthest from the spec):
- docs.html and llms.txt still carried "private keys never leave the
  worker" -- false on its face, since the widget ships a reveal-nsec
  button. Both now state the same limit the README does.
- "you revoke" became honest everywhere: removing a connection deletes
  the stored copy; spending authority is revoked at the wallet.
- index.html dropped a pre-softening recovery claim, now says an account
  lives only while relays retain its capsule, and says "independently
  unaudited" out loud.
- llms.txt gains the two warnings that were missing entirely: losing
  BOTH password and phrase is permanent, and removal is not revocation.
- Stale headers: nwc-connections.md no longer claims "Not implemented",
  docs/README no longer says the authorization layer is unbuilt, the
  test count and spec version are current, and the demo domain points
  at bitlogin.network.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 09:47:01 -04:00
thePR0M3TH3ANandClaude Fable 5 d040c2f5af Tier 1 hardening: no more silent hangs, no writes before consent
Reliability, which is where most of these reach users:
- The worker RPC had no message validation, no error/messageerror
  listener, no timeout, and terminate() left every pending promise
  unsettled. A malformed frame threw inside the listener (posting no
  response at all) and a dead worker stranded its callers -- the
  documented innerHTML-remount hazard manifested as a permanent freeze
  rather than an error. Both sides now validate the envelope, calls
  time out at 60s, and worker death rejects everything outstanding.
- requestNwcConnection fired no pending event, so a host using the
  RECOMMENDED hidden-mount pattern navigated a hidden element to a
  consent screen nobody could see and waited forever. Now symmetric
  with bitlogin-offer-pending.

Consent integrity:
- vaultOfferCheck published an origin rebinding to relays -- a durable
  mutation of the user's account -- from a plain host API call before
  any consent screen existed. It is read-only now, and a wallet stored
  under a DIFFERENT origin no longer reports as already-saved, so the
  binding can only change through the consented path.
- offerNwcConnection's guard sat above three awaits; two concurrent
  calls both passed it and the second dropped the first's promise. The
  slot is claimed synchronously.

Vault reads fail closed:
- listConnections threw rather than returning a relay-shaped subset
  when quorum is not met -- a short listing is indistinguishable from
  "you have no connections", and one relay should not get to say that.
- The 500-record page limit is reported (truncated) instead of silently
  deciding which connections exist.

Supply chain:
- All eight crypto dependencies pinned to exact versions (they held
  users' keys behind caret ranges), and Bitcoin Connect with them.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 09:40:49 -04:00
thePR0M3TH3ANandClaude Fable 5 b414cc53d2 Security audit: fix 8 findings across crypto, vault, and widget
An adversarial audit (five independent reviewers over crypto/protocol,
the vault, the widget, relay flows, and doc honesty) found real bugs in
code this repo shipped. Fixed here, worst first:

CRITICAL - the widget worker ignored application_binding.origin on
vaultRevealNwc, so any caller naming a connection id could pull a
wallet credential the user granted to a DIFFERENT site. The worker now
derives its own origin (self.location) instead of trusting a payload
field -- which also closes binding forgery via vaultSaveNwc /
vaultSetBinding / vaultOfferCheck, where a host could bind another
site's wallet to itself and inherit its one-tap approval.

CRITICAL - VaultSession.destroy() left the vault root live (the root
regenerates every derived key) and, worse, left the session USABLE with
a zeroed prk: deriveRecordKey/deriveVaultSigningKey over 32 zero bytes
are deterministic and world-computable, so a stale reference could seal
a wallet secret under a key any observer can derive and report success.
Now wipes the root and fails loudly.

CRITICAL - changePassword published the tombstone that DESTROYS the old
capsule before checking that the new one reached quorum: a failed
rotation could leave neither password working, phrase-only recovery.
Reordered to fail closed with the old password intact, plus a
regression test that drives it with a partially-hostile relay set.

HIGH - enableConnectionVault never checked quorum on the recovery read,
so one stale relay could make it mint a SECOND vault root and orphan
every stored credential -- the outcome its own header calls
unforgivable.

HIGH - the rollback high-water mark used strict `<` and never compared
the event id it stored, so a same-second sibling of a revocation could
resurrect a revoked credential forever. Ties now break on event id.

HIGH - §11.10 claimed derived secrets are wiped; wipe() had two callers
and neither was on the password or recovery path. The Argon2id output,
both PRKs, and the 64-byte BIP-39 seed now die at their derivation
site.

MEDIUM - one malformed record threw out of the listing loop and made
every OTHER connection unlistable; recovery lacked the
already-registered-at-this-locator guard that registration and rotation
both carry; the recovery chain check silently passed on gaps and forks.
Synthetic (untrusted) clicks and submits no longer drive consent UI.

Docs corrected where they overstated: the README's "private keys never
leave the worker" now says plainly that a same-origin host can reach
exportIdentity, and connection-vault.md gains §19 recording six OPEN
limitations -- no CAS on capsule writes, per-device-only rollback
state, tombstones not destroying prior ciphertext, the sudo key
shipping with the root, fetches not failing closed, and a plaintext
mark store.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 09:00:24 -04:00
thePR0M3TH3ANandClaude Fable 5 2cf2ee9351 Widget: dispatch bitlogin-offer-pending when the offer screen shows
Hosts that keep the element hidden until needed (the recommended
permanent-mount pattern) listen for this to reveal it; the silent
outcomes (duplicate, unavailable) never fire it, so nothing flashes
for a no-op. Already shipped in the vendored builds consuming this --
the source line lagged the artifact, which this commit corrects.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 23:43:55 -04:00
thePR0M3TH3ANandClaude Fable 5 6b48de6168 Widget: offerNwcConnection() -- consent-gated offer-to-save
The inverse of requestNwcConnection: an app that obtained an NWC URI by
its own means (its own wallet chooser, its own paste box) offers the
user a portable copy. The credential is already in the app's hands, so
the only question on screen is whether a copy enters the user's vault
-- and that write happens in BitLogin's own chrome or not at all, never
silently, because a page that can pump records into someone's account
without a tap is a spam vector wearing a feature's clothes.

Dedupe by connection identity (wallet pubkey + secret; relays and
labels may drift without making a new connection -- sameNwcCredential
in core): a duplicate resolves 'already-saved' with a silent
origin-binding refresh and no UI, which is what makes the call safe to
fire after every successful connect. 'unavailable' (no session, no
vault root) is a quiet no-op by contract.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 21:17:06 -04:00
thePR0M3TH3ANandClaude Fable 5 6841d647c2 Site: document requestNwcConnection and the Connection Vault
Integration guide gains the wallet-connection step (reveal-mode honesty,
CSP requirement, legacy-account caveat), llms.txt gains the API entry
and event, the landing page gains a "Wallets that follow you" feature,
and the file-copy step now says five files.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 14:14:00 -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 Fable 5 a465e1d8fa Implement the Connection Vault protocol layer (phases A-C)
connection-vault.md 5-13 as code, with the implementation-time decisions
recorded in a new section 18 and vault-ux.md holding the agreed UX
walkthroughs as the authorization layer's acceptance criteria.

Capsules gain paired optional fields connection_vault_root and
vault_sudo_key (both or neither, validated); registration mints them,
login/change-password/recovery carry them forward, and
enableConnectionVault is the phrase-gated migration for existing
accounts -- recovery capsule written first, fail closed, idempotent by
reusing roots found in either capsule so a partial failure can never
mint twice and strand records.

src/vault/ holds the finalized key hierarchy (HKDF/ScalarExpand
constants pinned by test vectors that must only ever be reverted, never
updated), per-connection encrypted records signed by a derived vault
identity with the capsule JCS+padding+AES-GCM pipeline and shared
buckets so record types are indistinguishable by size, the NWC profile
with lossless URI parse/export, relay sync reusing the publish-and-
verify quorum bar, and per-record high-water marks so a stale replica
is withheld and reported rather than silently resurrecting a revoked
credential.

The two-tier model makes sudo mode cryptographically real: personal-
tier record keys require vault_sudo_key as HKDF input, VaultSession
holds it only inside an enableSudo/endSudo window (wiping on the way in
and out), and outside a window personal records are invisible -- the
same trial-decrypt returns null, not an error. The app-facing API is
structurally blind to the tier by construction.

118 tests passing, including four end-to-end scenarios against mock
relays: clean-device restore, password-change carry, legacy-account
migration (wrong phrase refused), and rollback detection.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 12:29:46 -04:00
thePR0M3TH3AN e9a5a41ff7 fix(widget): encrypt persistent session cache 2026-07-25 23:28:41 -04:00
thePR0M3TH3AN fe9a7afde2 Add BitLogin documentation index 2026-07-22 20:56:59 -04:00
thePR0M3TH3AN 82539c2057 Document relay and messaging portability 2026-07-22 20:56:45 -04:00
thePR0M3TH3AN f656f014bb Document experimental portable entitlements 2026-07-22 20:56:07 -04:00
thePR0M3TH3AN 66c97820db Document experimental portable application state 2026-07-22 20:55:24 -04:00
thePR0M3TH3AN 6f0bc2a056 Document experimental multi-account switching 2026-07-22 20:54:46 -04:00
thePR0M3TH3AN 0c9011e064 Document experimental S3C connection string format 2026-07-22 17:41:33 -04:00
thePR0M3TH3AN 6000cfd06f Document experimental NWC Connection Vault profile 2026-07-22 17:40:33 -04:00
thePR0M3TH3AN 98a8c60821 Document experimental BitLogin Connection Vault 2026-07-22 17:39:45 -04:00
thePR0M3TH3ANandClaude Sonnet 5 681cec7734 Document session persistence in README and CHANGELOG
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-22 07:47:49 -04:00
thePR0M3TH3ANandClaude Sonnet 5 9e3787d301 Add local session persistence so a page reload doesn't require re-login
Every reload previously destroyed the crypto worker, forcing the full
login-name + password flow again -- re-running Argon2id and a relay
quorum read, both deliberately expensive (the whole point of Argon2id
is to resist offline brute force), on every single page navigation.
No real site can expect users to accept that.

After a successful register/login/completeRecovery/changePassword,
cache the everyday private key plus the active credential/recovery
capsule events in IndexedDB (persistSession() in cryptoWorker.ts).
connectedCallback() now calls a new "restoreSession" worker action
right after "configure", before rendering the welcome screen; if a
valid cache exists, the widget lands straight on the dashboard and
fires "bitlogin-login" exactly as a fresh login would, with no
password prompt. Logout clears the cache.

This trades "nothing persists locally" for "the everyday private key
sits decrypted in this browser's IndexedDB until logout" -- the same
tradeoff every NIP-07 extension and crypto wallet already makes for
the same reason, and scoped by IndexedDB's own per-origin isolation
(one site's cached session is never visible to another, same boundary
window.nostr already relies on). The recovery phrase and login
name/password are never cached.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-22 07:47:07 -04:00
thePR0M3TH3ANandClaude Sonnet 5 92a87ff13e CHANGELOG: document the NIP-04 addition
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 23:29:10 -04:00
thePR0M3TH3ANandClaude Sonnet 5 283840261f Add NIP-04 support for drop-in NIP-07 parity
signEvent() already handles any event kind and NIP-44 was already
implemented, but window.nostr.nip04 (and the equivalent element-scoped
nip04Encrypt/nip04Decrypt) didn't exist anywhere -- every real NIP-07
extension exposes it, and sites with legacy DM code paths still expect
it. A site whose own code called window.nostr.nip04.encrypt(...) while
BitLogin was the active signer got a bare "Cannot read properties of
undefined" instead of a working call.

- packages/core/src/crypto/nip04.ts: AES-256-CBC + secp256k1 ECDH
  shared secret (raw X coordinate, no HKDF -- this is legacy NIP-04,
  not NIP-44), "<ciphertext>?iv=<iv>" payload shape, cross-checked
  against the NIP-04 spec's algorithm description
- NostrSigner.nip04Encrypt/nip04Decrypt, mirroring the existing
  nip44Encrypt/nip44Decrypt methods
- crypto worker: new nip04Encrypt/nip04Decrypt actions and protocol
  payload types
- <bitlogin-auth>.nip04Encrypt/nip04Decrypt (element-scoped, like the
  existing nip44 methods) and window.nostr.nip04.encrypt/decrypt

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 23:28:51 -04:00
thePR0M3TH3ANandClaude Sonnet 5 66cae69c7e README: document the wasm-unsafe-eval CSP requirement and the DOM-move gotcha
Both discovered via a live bitunlock integration bug this session:
BitUnlock's script-src lacked 'wasm-unsafe-eval' (silently broke every
password-hashing action), and its attempt to relocate the static
#bitloginMount via replaceWith() (to work around the innerHTML gotcha)
turned out to trigger the exact disconnectedCallback problem it was
trying to avoid -- moving a custom element still tears down and
recreates it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 22:46:53 -04:00
thePR0M3TH3ANandClaude Sonnet 5 e3270012e9 CHANGELOG: document the style-src CSP fix
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 22:09:45 -04:00
thePR0M3TH3ANandClaude Sonnet 5 e8712f9aa9 Fix widget styles being silently dropped under strict style-src CSP
element.ts re-injected the widget's CSS as a fresh inline <style> tag
on every render() call. A host with a strict style-src (e.g.
`style-src 'self'`, no 'unsafe-inline') silently drops an inline
<style>'s rules entirely -- every element inside the shadow root fell
back to unstyled browser defaults. The visible symptom: the brand SVG
rendered at its raw intrinsic ~590x119px instead of the intended
20px-tall lockup, since its `display: block; height: 20px` rule never
took effect.

Build the stylesheet once as a CSSStyleSheet and assign it via
adoptedStyleSheets instead -- not subject to style-src the way a
parsed inline <style> element is, since it's never parsed as untrusted
markup by the HTML parser. render() no longer touches styling at all.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 22:09:30 -04:00
thePR0M3TH3ANandClaude Sonnet 5 863f035990 Add CHANGELOG.md and an "Updating your integration" README section
Vendoring sites had no documented way to learn a fix landed here or how
to pull it in. Adds a changelog (integrator-relevant entries only, each
noting whether it applies to a site's config) and a README section
explaining why vendoring is manual by design -- a shared auto-updating
script would make a bad deploy here an instant multi-site incident for
a widget that handles private key crypto.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 21:31:45 -04:00
thePR0M3TH3ANandClaude Sonnet 5 681f622ef8 Expand built-in vault/discovery relay lists for redundancy
relay.nostr.band (shared by both lists) is currently unreachable, and
publishAndVerify() enforces a fixed minAcks/minReadbacks=2 floor
regardless of relay count -- so a 3-relay vault list had zero margin
the moment one relay went down, surfacing as "Registration did not
reach the required relay acknowledgement and readback quorum."

Add nostr.wine and relay.snort.social to the vault list, and
nostr-pub.wellorder.net to discovery, so losing any single relay no
longer risks failing registration.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 21:28:38 -04:00
thePR0M3TH3AN a4e9b1d2ae Fix .link buttons collapsing onto one line at wide --bl-max-width
button.link overrode width back to auto (from the general button
rule's width: 100%) but never set display: block, so it stayed at the
<button> default of inline-block. At narrow widths (the default
380px card) two adjacent .link buttons happened to wrap onto separate
lines simply because they didn't fit side by side, masking the bug.
At --bl-max-width: none in a wide host container (found integrating
BitLogin into BitUnlock's /connect page), "Import an existing Nostr
key" and "Forgot password? Recover with phrase" rendered running into
each other on the same line.
2026-07-21 19:17:32 -04:00
thePR0M3TH3AN b2f4b96b84 Simplify hero credential card example
Drop the "river ·" login-name prefix in front of the example
passphrase -- it read as an unexplained, unlabeled extra word rather
than a distinct login name, and the separator dot didn't make the
distinction clear either. The card already says "One credential";
showing just the passphrase matches that label with nothing left to
be confused about.
2026-07-21 18:48:54 -04:00
thePR0M3TH3AN ed8d8eb1be Stamp genuine successes with the brand's brass ring + checkmark
Signing in, creating an account, recovering one, and rotating a
password all used to just silently swap to the dashboard screen --
nothing marked the moment as a success versus any other navigation.
These are exactly the security-relevant "you're safely in/done"
moments, so mark them with the same brass ring + checkmark mark used
in the logo: a brief overlay stamps the destination screen with the
mark and a short label ("Signed in", "Account created", "Account
recovered", "Password updated"), then fades on its own.

Deliberately narrow: this only fires on those four real completions,
not on every validation step or plain navigation (e.g. the "Back to
dashboard" button), so it stays a rare, meaningful beat instead of
becoming visual noise. It's also inert by design -- pointer-events:
none means it can never block interacting with the screen underneath,
a JS timer (not just the CSS animation) guarantees it's removed even
if the animation can't run, and prefers-reduced-motion drops the
motion but keeps the same brief hold-then-clear timing.
2026-07-21 18:44:58 -04:00
thePR0M3TH3AN f725e2cd83 Drop the "B" monogram, use one true logo lockup everywhere
The monogram read as a Bitcoin symbol, which BitLogin has nothing to
do with -- revert the icon/mark back to the brass ring + checkmark
(now brass/gold to match the ring, not white) with no letterform.

Also stop composing the icon and wordmark as two independently-sized
elements in each place they appear (a --mark height here, a
--wordmark height there) -- that let their relative size and spacing
drift apart between the site nav and the widget's header. Bake both
into a single SVG (assets/lockup.svg) with the layout fixed once, and
use that same file's markup everywhere a caller only ever sets one
overall height. Site nav and the widget's welcome screen now render
from the exact same source.

Also fills in the hero credential card's placeholder passphrase
("a few good words") with a realistic 6-word example matching what
the generator actually produces, and centers the last two cards of
the 6-card "Why this is different" grid under the middle two columns
instead of left-aligning them under columns 1-2.
2026-07-21 18:35:38 -04:00
thePR0M3TH3AN 2b272eb093 Give BitLogin a real vector logo, consistent across the site and widget
Replace the placeholder gradient-circle favicon and the widget's plain
"BitLogin" text heading with an actual brand: a monogram seal (a "B"
drawn from Fraunces, the same serif already used for headings, set in
a violet seal with a brass ring) plus a vector wordmark traced from
the live Fraunces glyphs.

Both are baked as static SVG path data (via fontkit) rather than
depending on the webfont at render time, so the widget can show the
exact same wordmark the site uses without loading any font of its
own -- solving the "vector logo instead of a typeface" problem for a
component meant to drop into arbitrary host pages.

- assets/icon.svg: updated favicon/app-icon with the new monogram
- assets/mark.svg (new): transparent-background version for inline
  use in a host site's own "Sign in with BitLogin" button
- assets/wordmark.svg (new): text-only logotype, fill=currentColor
- assets/lockup.svg (new): mark + wordmark combined, for anyone
  wanting a single-file logo
- All three site pages' nav now use the same inlined mark + wordmark
  instead of the ad-hoc checkmark-circle SVG that was duplicated
  across them
- <bitlogin-auth>'s welcome screen shows the same lockup instead of
  plain text, themed via --bl-fg like everything else in the widget
- docs.html gains a short "showing it as one option among several"
  step pointing integrators at mark.svg for a compact button icon
2026-07-21 17:54:21 -04:00
thePR0M3TH3AN 01fc7c5ed3 Refuse to overwrite an existing account on register or password change
registerAccount() never checked whether a credential capsule already
existed at the locator address it was about to publish to. That
address is a NIP-33 replaceable event fully determined by login name +
password, so registering with a login name + password combination that
was already registered -- by mistake (double-clicking "create account",
confusing it with "sign in"), or by coincidence with someone else's
account under a shared login name -- silently replaced the existing
account's identity binding. The old everyday identity became
unreachable through that login unless the original recovery phrase was
still on hand, with nothing warning the user beforehand.

changePassword() had the identical gap on the new locator: rotating to
a password that happened to already be registered under the same login
name would silently destroy that other account.

Both flows now query for an existing capsule at the target locator
before publishing, and refuse with AccountAlreadyExistsError if one is
found (or a retry-oriented error if no relay could be reached to check).
Two new end-to-end tests reproduce both scenarios and confirm the
pre-existing account survives untouched.

Bumps docs/spec.md to v0.6 (§15.6, §18.1).
2026-07-21 16:54:32 -04:00
thePR0M3TH3AN 8c01a121df Never overwrite an existing profile or relay list on import
After every successful registration -- including nsec imports -- the
widget unconditionally published a default kind 0 profile ({name:
loginName}) plus kind 10002/10050 relay lists. All three are
NIP-01/NIP-65/NIP-17 replaceable events, so importing an nsec that
already had a real profile (display name, bio, avatar, NIP-05) and/or
relay preferences published elsewhere silently erased all of it,
replacing it with just the chosen BitLogin login name and the vault
relays.

publishInitialProfile() (moved into @bitlogin/core so it can be
covered by the existing MockRelay-based test harness) now queries for
an existing event of each kind before publishing, and only fills in
whichever one is genuinely missing. A fresh registration has nothing
to find, so behavior there is unchanged; an import now leaves
whatever the identity already had untouched.

Bumps docs/spec.md to v0.5 (§15.8, §28.1) and adds two new core tests
covering the fresh-identity and existing-profile-preserved cases.
2026-07-21 16:39:00 -04:00
thePR0M3TH3AN 207f4cba10 Redesign demo site around a "one login, every site" pitch
The old landing page led with generic Nostr-signer framing and a
templated violet/pink gradient look. Reframe the pitch around the
thing that's actually differentiated: one login name and password
that works on every BitLogin site, explicitly positioned against
NIP-07 extensions and NIP-46 bunkers, with Nostr terminology kept
light for the non-technical end users who'll actually be typing a
password rather than integrating a widget.

Visual redesign: warm parchment/brass accent alongside the existing
violet, Fraunces display serif paired with the existing system-ui
body font, and a new "one credential, many doors" hero illustration
with a staggered stamp-in animation (prefers-reduced-motion aware)
replacing the generic feature-grid-only layout. Applied consistently
across index.html, docs.html, and account.html.
2026-07-21 16:17:37 -04:00
thePR0M3TH3ANandClaude Sonnet 5 ce6ade73b4 docs: bring the Integrate page and version references up to date
docs.html was missing everything from this session's work:
- The disconnectedCallback/crypto-worker-teardown gotcha (found via the
  BitRoad integration) -- now a callout right where the element is first
  mounted, since that's the step where it actually bites.
- Talking to the element directly (getPublicKey/signEvent/nip44Encrypt/
  nip44Decrypt as instance methods) as an alternative to window.nostr,
  with BitRoad's adapter as the worked example.
- claimSigner() failing gracefully (returns false, doesn't throw) when a
  NIP-07 extension has locked window.nostr down -- was previously
  undocumented entirely; this is the exact bug just fixed.
- Theming via CSS custom properties, with the full property list and
  BitRoad's mapping as a real example.

Also fixed two stale "(v0.3)" references to docs/spec.md in README.md and
CONTRIBUTING.md -- the spec was bumped to v0.4 earlier this session for the
rollback-detection changes but these two mentions were missed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 15:53:17 -04:00
thePR0M3TH3ANandClaude Sonnet 5 1dbb4fca51 Fail closed (not open-and-crash) when window.nostr can't be claimed
Some NIP-07 extensions install window.nostr as a non-configurable,
non-writable property to stop another script from overwriting it.
claimSigner() did a plain, unguarded `window.nostr = ...` assignment, so on
a page where such an extension is active, this threw
"Cannot assign to read only property 'nostr' of object '#<Window>'" --
and because it ran synchronously before dispatching bitlogin-login or
moving to the dashboard screen, it froze account creation/import at the
final "verify your recovery phrase" step, with that raw TypeError as the
only feedback shown. Reported from a real BitRoad integration session.

claimSigner() and releaseSigner() now catch this and return false instead
of throwing; connectedCallback's initial claim attempt is guarded the same
way. Every caller (registration, login, recovery, password change) now
completes sign-in regardless -- a host page using this element's own
methods directly (getPublicKey/signEvent/nip44Encrypt/nip44Decrypt) was
never actually affected by a failed window.nostr takeover, only a page that
reads window.nostr itself. The dashboard now shows a plain-language warning
when this happens, reusing the existing rollback/relay-disagreement warning
banner mechanism.

Also fixed while auditing this area: the "Copy" button on the generated-
password screen silently did nothing if the clipboard write failed (e.g.
Firefox's stricter permissions, an unfocused document) -- it now flashes
"Copied" or "Copy failed -- select the text manually" either way.

Verified live in Chrome: reproduced the exact crash by locking down
window.nostr the way a real extension does, confirmed account
creation/import now completes end-to-end (in both the standalone demo and
embedded in BitRoad) with the new warning shown instead of a stuck flow.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 15:44:52 -04:00
thePR0M3TH3ANandClaude Sonnet 5 1eff23aca8 Expose widget theming as CSS custom properties, and a max-width override
<bitlogin-auth> renders in a shadow root, but every color/radius/font value
already flowed through a CSS custom property on :host -- these inherit
through the shadow boundary, so a host page can already override the
widget's look from ordinary light-DOM CSS. This adds the two properties
that weren't yet exposed:

- --bl-accent-fg (default: white): text color on top of --bl-accent. Was
  hardcoded on button.primary, so a host with a light accent color and its
  own dark-on-accent convention had no way to match it.
- --bl-max-width (default: 380px): the widget stays a fixed-width card by
  default (unchanged for existing embedders), but a host page that wants it
  to fill a wider container can now override this instead of being stuck
  with the hardcoded cap.

Also routes the hardcoded font-family through --bl-font-family so that's
themeable too. Documented in the README with a worked example (BitRoad,
which maps every one of these onto its own design tokens to render the
widget in its own colors at full width).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 15:05:55 -04:00
thePR0M3TH3ANandClaude Sonnet 5 6640e7adf6 Link the GitHub repo and MIT license from the demo site footer
Every page's footer now links to the repo (github.com/PR0M3TH3AN/bitlogin)
and its LICENSE file, so visitors can see this is open source without
digging through docs.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 14:39:26 -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
thePR0M3TH3AN e17885d0e5 update 2026-07-21 13:03:12 -04:00