Files
bitlogin/docs/nwc-connections.md
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

465 lines
14 KiB
Markdown

# BitLogin NWC Connections
**Status:** Experimental design
**Document version:** 0.1
**Date:** July 22, 2026
**Implementation status:** PARTIALLY implemented. The parser/exporter and the
canonical credential shipped in `packages/core/src/vault/nwc.ts`, and the
request/offer flows shipped in the widget. NOT implemented: declared
capabilities, budget and expiry fields, `get_info` refresh, the suspend state,
broker mode, and the `wallet.*` API in §12 — those sections remain design.
## 1. Summary
This document defines the proposed BitLogin Connection Vault profile for Nostr
Wallet Connect (NWC) credentials.
The profile allows a user to:
- Import an NWC connection once.
- Recover that connection on another BitLogin device.
- Keep separate wallet connections for separate applications.
- Inspect known wallet capabilities, budget information, and expiration.
- Explicitly reveal or broker wallet operations to compatible applications.
- Remove a stored connection without changing the BitLogin identity.
NWC credentials are stored as independent encrypted Connection Vault records.
They are never stored directly in the BitLogin credential or recovery capsule.
See `connection-vault.md` for the common record, encryption, synchronization,
and recovery model.
## 2. Terminology
- **Wallet service:** The NWC wallet endpoint that receives encrypted requests.
- **Client key:** The Nostr private key embedded in the NWC URI and used to sign
requests to the wallet service.
- **Connection URI:** The complete `nostr+walletconnect://` URI supplied by the
wallet provider.
- **Application binding:** Optional BitLogin metadata associating one stored
connection with a particular application origin or Nostr application key.
- **Reveal mode:** BitLogin returns the raw NWC URI to an application.
- **Broker mode:** BitLogin keeps the NWC secret and performs approved NWC
requests on behalf of the application.
## 3. Security model
An NWC URI is a bearer-style credential containing active signing authority.
Anyone who obtains the client secret can make every request permitted by the
wallet service for that connection.
BitLogin therefore treats the complete NWC URI and its client secret as
sensitive values.
The wallet service remains authoritative for:
- Supported NWC methods.
- Spending budgets.
- Maximum payment size.
- Rate limits.
- Expiration.
- Revocation.
- Any other wallet-side policy.
BitLogin may display cached or declared values, but it must not claim to enforce
wallet policy unless every request is brokered through an isolated BitLogin
execution boundary.
## 4. One connection per application
BitLogin should strongly encourage users and applications to create separate
NWC connections rather than reuse one universal credential.
Recommended arrangement:
```text
BitRoad -> commerce wallet connection
Satisfied -> AI inference spending connection
Game -> low-budget in-game payment connection
Dashboard -> read-only or reporting connection
```
Separate connections improve:
- Revocation.
- Budget assignment.
- Auditability.
- Application isolation.
- Privacy between unrelated applications.
- Damage containment after credential compromise.
Reusing one client key across applications should be shown as an advanced and
potentially unsafe choice.
## 5. Accepted import format
The initial profile accepts a standard NWC connection URI:
```text
nostr+walletconnect://<wallet-service-pubkey>?relay=<encoded-relay-url>&secret=<client-secret>&lud16=<encoded-lightning-address>
```
Example with non-secret placeholder values:
```text
nostr+walletconnect://0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef?relay=wss%3A%2F%2Frelay.example&secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&lud16=adam%40example.com
```
The imported URI may include multiple `relay` query parameters. Unknown query
parameters must be retained for lossless export unless they are syntactically
invalid or explicitly prohibited by a later profile version.
The client must never place an imported URI in:
- Browser history.
- A page URL.
- Analytics.
- Logs.
- Error strings.
- Clipboard history without an explicit user copy action.
- Unencrypted local or relay storage.
## 6. Canonical internal credential
After import, BitLogin parses the URI and stores a structured credential object
rather than relying only on the original string.
```json
{
"schema": "bitlogin.connection.nwc.v1",
"wallet_service_pubkey": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"client_secret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"relays": [
"wss://relay.example"
],
"lud16": "[email protected]",
"extra_parameters": {},
"declared_capabilities": [
"pay_invoice",
"get_balance"
],
"declared_budget": {
"amount_msats": 1000000,
"period": "monthly"
},
"expires_at": null,
"original_uri": null
}
```
Required fields:
- `schema`
- `wallet_service_pubkey`
- `client_secret`
- `relays`
Optional fields:
- `lud16`
- `extra_parameters`
- `declared_capabilities`
- `declared_budget`
- `expires_at`
- `original_uri`
`original_uri` should ordinarily be null. It may be retained only when lossless
round-trip compatibility cannot otherwise be achieved. When present, it is as
sensitive as the client secret.
## 7. Validation
An NWC import must fail if:
1. The scheme is not exactly `nostr+walletconnect`.
2. The wallet-service public key is not exactly 32 bytes encoded as 64 lowercase
or uppercase hexadecimal characters.
3. The client secret is not exactly 32 bytes encoded as 64 hexadecimal
characters or is not a valid secp256k1 private scalar.
4. No relay is present.
5. A relay URL does not use `wss://`, except for explicitly enabled local
development using `ws://localhost` or loopback addresses.
6. A relay URL contains credentials.
7. The total URI or decoded metadata exceeds implementation limits.
8. Duplicate parameters conflict in a way the profile cannot interpret.
9. Percent encoding is malformed.
The parser should normalize:
- Hexadecimal keys to lowercase.
- Relay URL host casing according to URL rules.
- Duplicate identical relay URLs into one entry while preserving first-seen
order.
- Empty optional parameters to null or omission.
The parser must not silently repair a malformed secret or wallet public key.
## 8. Complete Connection Vault record
Example decrypted record:
```json
{
"schema": "bitlogin.connection.v1",
"connection_id": "Vx7LgdZCsVbT_2uvB0YoGA",
"connection_type": "nwc",
"state": "active",
"label": "Satisfied spending wallet",
"created_at": 1784750000,
"updated_at": 1784750000,
"credential": {
"schema": "bitlogin.connection.nwc.v1",
"wallet_service_pubkey": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"client_secret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"relays": [
"wss://relay.example"
],
"lud16": "[email protected]",
"extra_parameters": {},
"declared_capabilities": [
"pay_invoice",
"get_balance"
],
"declared_budget": {
"amount_msats": 1000000,
"period": "monthly"
},
"expires_at": null,
"original_uri": null
},
"application_binding": {
"origin": "https://satisfied.example",
"app_pubkey": null
},
"notes": null
}
```
Labels, application bindings, declared capabilities, and declared budgets are
BitLogin metadata. They do not alter the actual authority granted by the wallet
service.
## 9. Capability discovery
After import, BitLogin may connect to the wallet service and request supported
NWC methods according to the current NWC protocol.
The result may be cached as `declared_capabilities`, but the interface must make
clear that:
- Wallet capabilities can change.
- A listed method may still fail.
- A missing cached method does not necessarily prove the wallet no longer
supports it.
- Wallet-side policy is authoritative.
Capability refreshes update only the relevant NWC record.
## 10. Budgets and expiration
When the wallet service communicates budget or expiration information, BitLogin
may store a display-oriented snapshot:
```json
{
"declared_budget": {
"amount_msats": 1000000,
"period": "monthly"
},
"expires_at": 1787356800
}
```
`expires_at` is a Unix timestamp in seconds.
These fields are descriptive unless BitLogin broker mode independently limits
all outgoing operations. Reveal mode necessarily gives the application the raw
credential, after which BitLogin cannot enforce its own budget.
## 11. Export
### 11.1 Reconstructed NWC URI
BitLogin may reconstruct a standard URI for explicit export:
```text
nostr+walletconnect://<wallet-service-pubkey>?relay=<relay>&secret=<client-secret>&lud16=<lud16>
```
Rules:
- Include one `relay` parameter per relay.
- Percent-encode all query values.
- Preserve unknown parameters from `extra_parameters`.
- Never include BitLogin-only labels, notes, budgets, or application bindings.
- Require user confirmation immediately before reveal or copy.
- Clear the displayed URI after the shortest practical interval.
### 11.2 Encrypted BitLogin export
A normal BitLogin account export includes the encrypted signed Connection Vault
record, not a plaintext NWC URI.
A plaintext export must be separately selected and prominently marked as
sensitive.
## 12. Application API
### 12.1 Listing metadata
An application may request non-secret connection metadata:
```javascript
const wallets = await bitlogin.wallet.list();
```
Possible result:
```json
[
{
"id": "Vx7LgdZCsVbT_2uvB0YoGA",
"label": "Satisfied spending wallet",
"lud16": "[email protected]",
"capabilities": ["pay_invoice", "get_balance"],
"expiresAt": null
}
]
```
The result must not include the client secret or full URI.
### 12.2 Access request
```javascript
const grant = await bitlogin.wallet.requestAccess({
connectionId: "Vx7LgdZCsVbT_2uvB0YoGA",
methods: ["pay_invoice", "get_balance"],
reason: "Pay for AI inference",
mode: "broker"
});
```
The user-facing prompt should display:
- Requesting origin or application identity.
- Selected wallet label.
- Requested methods.
- Requested maximum amount when relevant.
- Whether the raw credential will be revealed.
- Whether approval is once, for the session, or persistent.
### 12.3 Brokered execution
Conceptual API:
```javascript
const result = await bitlogin.wallet.execute({
grantId: grant.id,
method: "pay_invoice",
params: {
invoice: "lnbc..."
}
});
```
A broker should reject:
- Methods not present in the grant.
- Expired grants.
- Requests from a different origin or application identity.
- Payment amounts above the approved BitLogin-side ceiling.
- Requests after the connection is suspended, deleted, expired, or revoked.
This BitLogin-side check is additive. The wallet service must still enforce its
own policy.
### 12.4 Reveal mode
Conceptual API:
```javascript
const uri = await bitlogin.wallet.revealConnection({
connectionId: "Vx7LgdZCsVbT_2uvB0YoGA",
reason: "Connect this wallet to the application"
});
```
Before returning the URI, BitLogin must warn:
> This application will receive a wallet credential. It can perform every
> action permitted by that connection until you revoke it at the wallet.
Reveal approval should not silently become a permanent grant.
## 13. Browser integration limitations
A same-origin embedded widget cannot keep an NWC URI secret from its host page
while simultaneously returning that URI to the host page.
For meaningful broker isolation, NWC signing and relay communication should run
inside:
- A BitLogin browser extension.
- A native BitLogin client.
- A cross-origin authorization window or iframe.
- A remote signer or broker with an authenticated protocol.
The initial widget may support metadata listing and explicit reveal mode, but it
must not present same-origin JavaScript isolation as a strong security boundary.
## 14. Revocation and deletion
BitLogin cannot generally revoke an NWC connection merely by deleting its local
or relay record. The connection must be revoked by the wallet service.
The user interface should provide:
1. **Open wallet to revoke** when a provider URL or supported mechanism exists.
2. **Mark suspended** to prevent BitLogin from offering the connection.
3. **Remove from BitLogin** to tombstone and delete the portable record.
After provider-side revocation, BitLogin should mark the connection deleted or
revoked and clear any persistent application grants.
A future provider-specific adapter may automate revocation, but the profile must
not assume all NWC wallet services support a common revocation API.
## 15. Privacy considerations
A Connection Vault record is authored by a dedicated vault identity rather than
the public everyday Nostr key. Nevertheless, correlation remains possible
through:
- Relay selection.
- IP address.
- Connection timing.
- Record publication timing.
- Wallet relay traffic.
- Reused NWC client keys.
- Reused wallet service pubkeys.
Clients should avoid fetching public profile data and private Connection Vault
records over the same relay connection where practical.
Users seeking stronger compartmentalization should use separate NWC connections
for separate applications.
## 16. Test vectors
Before implementation, the project should publish test vectors covering:
- Parsing one and multiple relay parameters.
- Percent-encoded `lud16` values.
- Unknown parameter preservation.
- Uppercase-to-lowercase hex normalization.
- Invalid public keys and client secrets.
- Invalid secp256k1 scalars.
- Insecure and malformed relay URLs.
- Canonical credential serialization.
- Encrypted Connection Vault event construction.
- Exported URI reconstruction.
- Tombstone replacement.
All examples in this document use non-production placeholder secrets and must
never be interpreted as usable wallet credentials.