Files
bitlogin/docs/TODO.md
thePR0M3TH3ANandClaude Opus 4.8 522d81ec56 docs: record design-review note (assurance bar before promotion)
Outside review flagged BitLogin as highest-upside/highest-risk. Record the
guidance: external cryptographic review as a gate before 'use for real'
positioning; keep copy off impossibility claims; hold the passphrase-entropy
and Argon2id/AES-GCM choices.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
2026-08-11 23:28:59 -04:00

456 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BitLogin — open items
Updated: 2026-08-04
## Login-surface expansion (2026-08-04) — shipped, with owner follow-ups
One login surface, four methods (`login-methods.md`, `passkey-login.md`):
password (primary), NIP-07 extension, NIP-46 remote signer (memory-only),
and passkey via WebAuthn PRF. OAuth on-ramps were built, then removed the
same day for requiring per-site provider registration
(`centralized-onramps.md`, superseded, kept on the record). Also shipped:
the deployment-CSP `'wasm-unsafe-eval'` fix (live password flows were
broken since 2026-07-31), the service-worker `cache: "reload"` precache
fix, `window.bitlogin.activeMethod()/activeSession()`, and welcome/sign-in
UI polish.
## Security audit response (2026-08-04, same day)
All confirmed findings fixed and regression-tested: BL-16 (auth_url now
https-only via `sanitizeAuthUrl`, hostname shown on the link, hostile-scheme
tests), BL-17 (post-creation PRF fallback pinned via `allowCredentials` to
the created credential), BL-18 (dead-socket CLOSE can no longer strand a
query promise), BL-19 (local `filter.limit` enforcement + duplicate-id drop
+ 1000-event ceiling per buffered query), BL-20 (.semgrep.yml try/catch
pattern was unparseable — fixed and verified firing on hostile code under
the pinned Semgrep 1.171.0). Lows: NIP-07 and NIP-46 now verify returned
signed events against the request (signature, identity, field equality);
`Nip46Client.close()` wipes the client and conversation keys; passkey
labels are distinguishable; postcss ≥8.5.25; Actions pinned to commit SHAs.
Second-pass response (same day): BL-21 — bunker relays capped at 8 and
deduplicated; relay frames over 256K chars dropped before JSON.parse; all
cheap checks (id dedup, capacity) run before Schnorr verification, plus a
per-query verification budget. BL-22 — superseded/abandoned nostrconnect
attempts are cancelled via AbortSignal and their ephemeral keys wiped
(ownership transfers, not dangles, on success); per-candidate conversation
keys wiped in a finally. BL-23 — `security:ci` now runs the custom Semgrep
rules (`security:semgrep`; requires a local semgrep install, same pin as
CI). BL-24 — the courtesy protocol `logout` (current NIP-46 defines it,
explicitly not a security boundary) is now sent on teardown; this corrects
the earlier "no stable method name" note. Still deferred, interop only:
`switch_relays`.
Third-pass response (same day): BL-25 — live subscriptions now carry a
bounded already-seen id cache (2048, FIFO eviction; a replayed retained
event costs one verification and one delivery, ever) and a fixed-window
verification budget (100 per 10s; distinct-event spray is quarantined for
the window). BL-26 — teardown order inverted: the courtesy logout event is
pre-signed, then close() IMMEDIATELY rejects every in-flight request and
wipes keys, then the pre-signed event is delivered asynchronously and
bounded — nothing can resolve after logout begins (regression-tested).
BL-27 — a nostrconnect listen failing naturally (timeout, unreachable
relays) now discards the pending ephemeral key in a guarded catch instead
of orphaning it. BL-28 — the hosted verify job installs the pinned Semgrep
before running security:ci (it was red at the previous HEAD, skipping SBOM
and checksums). Informational — bunker URIs are bounded: 4096 chars total,
256-char secret, 512-char relay URLs (oversized relays filtered, oversized
URI/secret rejected).
Owner action items:
1. **Live-fire the NIP-46 QR with Amber** on bitlogin.network (flow was
verified against a fake bunker in tests, not yet against a real phone
signer end-to-end).
2. **Live-fire passkey sign-in on a real device.** *Partly discharged
2026-08-04:* `packages/widget/src/e2e/passkey.e2e.test.ts` drives real
headless Chromium through the real WebAuthn ceremonies with a CDP
virtual authenticator (`hasPrf: true`), against the built demo and
in-process mock relays — create passkey → register → dashboard npub →
capsules verified on the relays → logout → sign in with the same passkey
→ same npub; plus a second passkey deriving a different account. Runs in
`npm test`, skips automatically without `npx playwright install
chromium`. What that still does NOT cover: a real platform authenticator
(Google Password Manager, iCloud Keychain, Bitwarden, a hardware key)
and the create-time-PRF-absent fallback path, which Chromium's virtual
authenticator does not exercise (it returns PRF results at creation).
3. **Passkey derivation is a new compatibility contract** —
`packages/widget/src/passkey.test.ts` known-answer vectors carry the
same never-update-the-expected-value rule as `knownAnswer.test.ts`.
4. NIP-46 credential *persistence* (vault record) stays gated on the
external security review, as before.
## Entropy audit (2026-07-31)
[`entropy-audit-2026-07-31.md`](./entropy-audit-2026-07-31.md) — RNG and
randomness-integration audit. **No predictable-fallback defect:** `random.ts`
is the single chokepoint, throws rather than degrading, and rejection-samples to
avoid modulo bias. Three hardening items, in order:
- **M2 — done.** Nine tests in `packages/core/src/crypto/crypto.test.ts` pin
fail-closed behaviour and `randomUniformInt`'s uniformity. Mutation-verified:
a zero-filling stub in place of the throw turns five of them red, and
swapping rejection sampling for bare modulo gives chi-square 5614 against a
threshold of 147.
- **M1 — done.** `nip44Encrypt` / `nip04Encrypt` no longer take a nonce or IV;
the capability lives in the non-exported `crypto/testing.ts`, and
`crypto/index.ts` re-exports explicitly instead of `export *`. Only one test
ever used an override and `ivOverride` had no callers, so nothing was lost.
Verified from a real consumer against the built package: all three deep-import
routes fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`. `knownAnswer.test.ts` green
throughout — the derivation chain is untouched.
- **L1 — done.** Both `Math.random` calls (backup-quiz picker, relay subscription
id) now use the CSPRNG, so the ban has no standing exception. `.semgrep.yml`
enforces it in CI alongside two more rules: no fallback around
`getRandomValues`, and no timestamp-seeded secrets. Verified the rules fire —
reintroducing the old call gives 1 blocking finding and exit 1.
**The entropy audit is closed for this repo.** The only gate left is an
independent review of the RNG path by someone other than the author, which is
not something a coding agent can sign off.
## Owner action items
1. **Close the Dependabot PR on
`dependabot/npm_and_yarn/npm-security-and-maintenance-83024bc549`.** It
bundles seven major bumps into one change (`@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, Vitest 3→4) and fails the build: noble v2 dropped
the extensionless subpath exports this code imports. It should be closed,
not merged or fixed — `.github/dependabot.yml` now groups only minor/patch
and ignores majors of the derivation chain, so this PR will not recur.
The four `dependabot/github_actions/*` PRs are unaffected and fine to merge.
2. **Decide whether the manual-password removal is final.** User-chosen
passwords were removed entirely (not merely discouraged) in the 2026-07-31
hardening, and `packages/widget/src/securityPolicy.test.ts` pins that. The
security argument is sound — a user-chosen password against a downloadable
capsule is offline-guessable forever — but it is a product decision that
changes what users can do, and it is now enforced by a test rather than by
preference.
## Standing rule: the derivation chain is a compatibility contract
Account capsules live on public relays. The login name plus password derive the
locator, signing scalar and capsule key deterministically, so those outputs are
a contract with every account that already exists — change them and users are
locked out permanently, with no server-side record to migrate from.
`packages/core/src/crypto/knownAnswer.test.ts` pins HKDF-Extract, HKDF-Expand,
ScalarExpand (scalar, counter, and resulting public key), BIP-39 phrase and
seed, base64url framing, and JCS ordering.
**A failure there is never "update the expected value".** It means the change
being attempted is account-breaking and needs a migration plan or rejection.
The rest of the crypto suite only checks self-consistency — same input, same
output within one run — which stays green through exactly the kind of upgrade
that would lock everyone out.
## Deferred hardening
Carried from the 2026-07-30 audit; none are live defects.
- **Tier 2** — `packages/widget/src/element.ts` is ~1,600 lines with no direct
test coverage. `securityPolicy.test.ts` now asserts a few properties of its
source text, which is a start, not coverage.
- **Tier 3** — compare-and-swap on capsule writes (relays offer no atomic
primitive, so this needs a protocol-level answer), and an encrypted
high-water-mark store.
- **Tier 4** — moving high-value authorization into a cross-origin or extension
boundary. An embedded widget is same-origin with its host page, so a
compromised host can reach the worker directly. Documented honestly in
`llms.txt` and the README rather than papered over.
## Gates still in force
- **External security review before the vault holds anything beyond a
budget-capped NWC connection.** The S3 and password profiles stay designs
until then.
- Personal-tier UI was gated on the sudo key leaving the credential capsule.
That gate is now **met** — the sudo key lives only in the phrase-gated
recovery capsule — but the UI still needs a separately reviewed key
ceremony before it ships.
## Additive browser distribution and integrator API roadmap
These are additive compatibility goals. The existing multi-file build,
`<bitlogin-auth>` behavior, `window.nostr` provider, derivation chain, session
formats, and current consuming applications are compatibility contracts. Do
not replace or silently alter them while adding standalone distribution or a
new public API. Every release must continue to produce and test the standard
build before the standalone build is considered successful.
### Priority 1 — Distribution
- [ ] Add a standalone browser build target: `npm run build:standalone`.
- [ ] Produce one self-contained `bitlogin.js` for standalone consumers.
- [ ] Keep the standard multi-file build unchanged and available alongside it:
`dist/standard/` and `dist/standalone/`.
- [ ] Remove standalone runtime dependence on external `cryptoWorker.js`,
hashed shared chunks, or other sibling files; preserve Web Worker
isolation internally where possible using a Blob/module worker.
- [ ] Support this minimal static-site integration:
`<script type="module" src="./bitlogin.js"></script>` and
`<bitlogin-auth></bitlogin-auth>`.
- [ ] Verify standalone hosting from arbitrary static HTTPS origins, including
GitHub Pages, Cloudflare Pages, Netlify, Vercel, and ordinary nginx.
- [ ] Verify localhost development usage.
- [ ] Document `file://` limitations clearly, including the recommended local
HTTPS or local development-server path.
- [ ] Define CSP requirements for standalone Blob/module workers and report
incompatible policies instead of silently degrading crypto isolation.
### Priority 2 — Developer API
- [ ] Define and document one canonical signer interface with capability-based
methods: `getPublicKey()`, `signEvent()`, `getRelays()`, and supported
`nip04.encrypt/decrypt`, `nip44.encrypt/decrypt`, and NWC operations.
- [ ] Keep `window.nostr` compatibility, including its existing NIP-07-shaped
behavior, as an adapter over the canonical signer rather than a breaking
replacement.
- [ ] Expose a clean additive `window.bitlogin` API, including
`activeSession()`, `login()`, `logout()`, and `getSigner()`.
- [ ] Add and document stable lifecycle event aliases: `bitlogin:ready`,
`bitlogin:login`, `bitlogin:logout`, and `bitlogin:error`. Preserve the
existing hyphenated events (`bitlogin-login`, `bitlogin-logout`, and
related current event names) for compatibility; the colon-named events
must be additive and must not replace them silently.
- [ ] Version and test event payloads; preserve existing event names and
payload fields used by current apps while adding new fields additively.
- [ ] Expose capability detection, for example `signer.capabilities()`, for
`signEvent`, `nip04`, `nip44`, `nip46`, `nwc`, and `passkey`.
- [ ] Ensure capability reporting describes the active session and never
implies support for a method that the active signer cannot perform.
### Priority 3 — Embedding and UI
- [ ] Support a headless mode so applications can use BitLogin without
rendering `<bitlogin-auth>`.
- [ ] Keep `<bitlogin-auth>` as the default easy-to-integrate UI.
- [ ] Allow applications to open the login dialog programmatically, including
`await bitlogin.login()`.
- [ ] Allow configurable login methods, for example
`<bitlogin-auth methods="passkey,password,nip07,nip46"></bitlogin-auth>`.
- [ ] Add basic theming hooks: CSS custom properties, light/dark support,
accent color, and compact mode.
- [ ] Expose supported configuration through documented attributes/options;
do not require integrators to reach into widget internals or private DOM.
- [ ] Preserve current login-method defaults and existing method ordering unless
an explicit versioned compatibility decision says otherwise.
### Priority 4 — Build, versioning, and releases
- [ ] Give browser artifacts predictable filenames: `bitlogin.js` and,
if useful, `bitlogin.min.js`.
- [ ] Expose version information through `window.bitlogin.version` and the
module/API surface where applicable.
- [ ] Emit integrity/hash manifests for releases and document exactly which
artifact each hash covers.
- [ ] Create downloadable release artifacts: `bitlogin-standard.zip` and
`bitlogin-standalone.js`.
- [ ] Publish release notes specifically for integrators, including changed
browser requirements, CSP requirements, API additions, and compatibility
notes.
- [ ] Evaluate public package exports for the widget, standalone entry point,
and core package (for example `@bitlogin/widget`,
`@bitlogin/widget/standalone`, and `@bitlogin/core`) without exposing
unsupported private paths or weakening package encapsulation.
### Priority 5 — Integration and compatibility testing
- [ ] Add a minimal vanilla HTML integration test for both standard and
standalone builds.
- [ ] Test standalone flows for password account, passkey account, recovery
phrase, NIP-07 extension, and NIP-46 bunker.
- [ ] Test `getPublicKey()`, `signEvent()`, `logout()`, and session restore
through both the canonical signer and the compatibility adapter.
- [ ] Test signed kind-1 publication against real or mock relays.
- [ ] Test multiple BitLogin-enabled applications on separate origins and
confirm origin-scoped storage/session behavior remains isolated.
- [ ] Test CSP compatibility, including the documented Blob/module-worker
requirements and a fail-closed result when a required policy is absent.
- [ ] Test Blob Worker behavior across Chromium, Firefox, and Safari.
- [ ] Test passkeys on real platform authenticators in addition to the existing
virtual-authenticator coverage.
- [ ] Run the existing derivation-chain known-answer tests and regression suite
unchanged; never update expected values to make a compatibility failure
pass.
- [ ] Verify current consuming applications (including AdZap, Satisfied,
BitUnlock, BitRoad, and Bitvid where their checkouts are available) still
load the standard artifact and retain login, signing, logout, and session
restore behavior.
- [ ] Add a release gate that builds/tests the standard artifact before and
after standalone bundling, proving standalone work cannot replace or
mutate the standard output accidentally.
### Priority 6 — Documentation
- [ ] Put a "30-second integration" section at the top of the README.
- [ ] Provide a complete single-page standard and standalone example.
- [ ] Provide a headless example.
- [ ] Provide a Nostr publishing example:
`const signer = await bitlogin.getSigner();` followed by
`const event = await signer.signEvent(...);`.
- [ ] Provide a migration guide from the standard build to the standalone
build, with a rollback path that requires no application-code rewrite.
- [ ] Clearly explain the security tradeoff between the standard worker/chunk
build and the standalone bundled build.
- [ ] Clearly state that standalone is a distribution convenience, not a
stronger security boundary; an embedding host remains trusted and
same-origin with the widget unless a separate isolation architecture is
used.
### Definition of done
A developer can download one JavaScript file, add approximately 3–5 lines of
HTML/JavaScript, and obtain a real BitLogin-backed Nostr signer without npm,
Vite, React, or a backend. Existing standard-build consumers continue to work
without source changes, and the compatibility and security claims above are
backed by automated tests and release artifacts.
### Context warnings to resolve
- [ ] Replace unresolved package references such as `@file:bitlogin/widget`,
`@file:bitlogin/widget/standalone`, and `@file:bitlogin/core` with actual
documented package paths or mark them as examples only; do not publish
links to files or exports that do not exist.
## BitBlocks App Kit compatibility layer
Do this only after BitLogin, BitGate, and BitFeed each have clean standalone
APIs. BitBlocks is an optional integration layer, not a replacement for any of
the three libraries. It must not merge their implementations or make an app
depend on the kit when it only needs BitLogin.
- [ ] Create a tiny optional BitBlocks integration layer.
- [ ] Keep BitLogin, BitGate, and BitFeed as separate replaceable libraries;
the App Kit must compose them rather than absorb them.
- [ ] Provide shared initialization, for example:
const app = await BitBlocks.create({
login: {},
gate: {},
feed: {},
relays: [...]
});
- [ ] Standardize the viewer context exchanged between the components without
changing BitLogin's signer/session contract.
- [ ] Centralize optional common Nostr plumbing in the kit: relay connections,
subscriptions, EOSE handling, reconnects, deduplication, timeouts,
publishing, OK responses, profile lookup, follow-list lookup, and
mute-list lookup.
- [ ] Expose `app.viewer`, `app.signer`, `app.relays`, `app.gate`, and
`app.feed` through documented interfaces.
- [ ] Keep each underlying library independently usable and independently
versioned.
- [ ] Do not make applications depend on BitBlocks when they use only
BitLogin's identity/signing capabilities.
- [ ] Add integration tests proving the kit is an adapter over the standalone
APIs, not a second implementation of identity, policy, or feed logic.
### BitBlocks definition of done
A developer or AI agent can take `bitlogin.js`, `bitgate.js`, `bitfeed.js`, and
`bitblocks.js` and build a functional static Nostr application with almost no
infrastructure or integration boilerplate, while each individual library
remains usable without BitBlocks.
## Feedback from integrating into Tessera (2026-08-11)
Notes from embedding `@bitlogin/widget` in the Tessera static site. Grouped by
how much they cost.
### High-value
- **[DONE 2026-08-11] Lightweight session-state read without the full widget.**
The single biggest cost: to know *whether a user is signed in* on a page, you
used to have to mount the whole widget (bitlogin.js + crypto worker + wasm,
~1.6MB) because it was the only thing that restored the per-origin session.
**Shipped:** the widget now writes a cleartext, public-only record
(`{ pubkey, npub, name? }` — never key material) beside its encrypted session
in IndexedDB and clears it on logout, and a new `@bitlogin/widget/session`
entry point (`bitlogin-session.js`, ~1 KB gzip, zero deps, no worker/wasm)
exposes `readCachedSession(): Promise<{ pubkey, npub, name? } | null>`. The
same call is mirrored on `window.bitlogin.readCachedSession()` for pages that
already load the full widget. See `src/publicSession.ts`, `src/session.ts`,
and the README "Reading who's signed in without loading the widget" section.
Remaining polish: have the widget also record the profile display `name` once
fetched (currently `name` is only set when the caller supplies it), and add an
`@bitlogin/widget/session` `.d.ts` (tracked under the Small item below).
- **[DONE 2026-08-11] A headless session-restore API.** Shipped
`@bitlogin/widget/headless` (`bitlogin-headless.js`): `createHeadlessSigner()`
→ `restore()` / `getPublicKey()` / `signEvent()` / `nip44Encrypt|Decrypt` /
`isUnlocked()` / `logout()` / `destroy()`, a DOM-free wrapper over the crypto
worker. It restores the cached session and signs with no custom element and no
`window.nostr`; it deliberately can't interactively log in (`restore()`
returns null when nothing is cached → mount `<bitlogin-auth>` once). Covered by
`headless.test.ts` (scripted-worker); also re-exported from `@bitlogin/widget`;
README "Signing without the widget (headless)". Deploy alongside
`cryptoWorker.js` + shared chunks.
### Medium
- **[DONE 2026-08-11] The unmount-kills-the-worker footgun now has a guardrail.**
The docs warned that removing `<bitlogin-auth>` tears down the crypto worker
and hangs every later signing call with no error. **Shipped both asks:**
(a) `disconnectedCallback` now logs a `console.error` when the element is
removed while a session is active, naming the unmount and pointing at the
README "Integration gotcha"; and (b) `WorkerClient.terminate(reason)` records
the reason and `signEvent`/`getPublicKey`/etc. — whether in-flight at teardown
or issued afterward — reject with that message instead of hanging (the
`dead`-flag path already prevented the infinite hang; this makes the *cause*
legible and unifies the message). Covered by `worker/workerClient.test.ts`;
README gotcha paragraphs updated to say "rejects loudly," not "hangs forever."
- **[DONE 2026-08-11] Styling hooks / CSS parts.** The widget renders in a
shadow root, so a host could recolor it via the `:host` custom properties but
couldn't restyle structure. **Shipped:** a stable set of `::part()` hooks —
`card`, `button` (+`button-primary`/`button-secondary`), `link`, `input`,
`checkbox`, `heading`, `text`, `notice` (+`notice-warn`/`-info`/`-error`) —
stamped centrally per render in `element.ts` `exposeParts()` (mapped by class
so new elements stay exposed without drift). Documented as a contract in the
README "Theming" section. Verified in-browser that
`bitlogin-auth::part(...)` matches through the shadow boundary. Custom
properties remain the first tool for color/radius/font; parts are the escape
hatch for structure (padding/border/shadow/shape).
- **[RESOLVED — already the behavior] Session-change event on restore.**
Confirmed: `bitlogin-login` already fires on silent session restore across a
page load, not just interactive login, so a host listener needn't distinguish
the two (README "Sessions persist across a page reload"). No new event needed.
For pages that don't mount the widget at all, use `readCachedSession()` above.
### Small
- **[DONE 2026-08-11]** Ship a `.d.ts` (or documented event/method surface) for
the element. **Shipped:** the widget now emits type declarations from source
(`tsconfig.types.json` → `dist-types/`, wired via `build:types` and the
package `types`/`exports` conditions for both `@bitlogin/widget` and
`@bitlogin/widget/session`) — zero-drift because they're generated, not
hand-written. Added the type surface integrators actually want: a
`HTMLElementTagNameMap["bitlogin-auth"]` augmentation (typed
`querySelector`/`createElement`), typed `addEventListener` overloads via a
declaration-merged interface, and exported event-detail types
(`BitLoginLoginEventDetail` etc. + `BitLoginEventMap`) in `src/events.ts`.
Verified with a consumer conformance typecheck; README "TypeScript" section
added.
## Design review — external critique (2026-08-11)
From an outside review of the BitBlocks stack. BitLogin was called the highest
upside AND highest risk in the stack.
- **[agree] Highest assurance bar.** A BitLogin failure is identity compromise,
not "the feed looked weird." Keep promotion conservative and the
alpha/unaudited labeling prominent until an outside cryptographic/security
review. Treat an external audit as a **gate** before any "use this for real"
positioning — this is the one block where marketing must trail assurance.
- **[agree] No impossibility claims in copy.** Relay retention is best-effort;
our own docs already say to keep the recovery export. Keep the site's language
on "portable / no single operator controls," never "cannot be deleted." (See
the umbrella claim-precision note.)
- The choices the review praised — high-entropy passphrase (not raw human
passwords), Argon2id + AES-256-GCM, `window.nostr` compatibility so Nostr
"disappears into the background" — are the right ones. Hold that line; don't
soften the passphrase-entropy stance for UX.