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
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