16 Commits

Author SHA1 Message Date
Your Name
966d9d0456 documentation changes 2025-09-20 11:08:45 -04:00
Your Name
ccff136edb Single Source of Truth Architecture - Complete authentication state management with storage-based getAuthState() as sole authoritative source 2025-09-20 10:39:43 -04:00
Your Name
8f34c2de73 Seem to have most everything working well. Got persistant state after page refresh, and implmented logout call 2025-09-19 16:09:05 -04:00
Your Name
ca75df8bb4 Fixed issue with bunker. Made the modal more beautiful. 2025-09-19 12:24:13 -04:00
Your Name
c747f1f315 . 2025-09-18 10:18:32 -04:00
Your Name
2a66b5eeec Fixed name display 2025-09-16 18:13:01 -04:00
Your Name
fa9688b17e Implement logging in via seed phrase 2025-09-16 15:51:08 -04:00
Your Name
a0e18c34d6 Add comprehensive sign.html test and update documentation
- Add examples/sign.html with comprehensive extension compatibility testing
- Update README.md with profile fetching API documentation
- Update .gitignore for better file management
- Update examples/button.html and examples/modal.html with latest features

This completes the single-extension architecture implementation with:
- nos2x compatibility through true single-extension mode
- Method switching between extension/local/NIP-46/readonly
- Enhanced profile fetching for floating tab
- Comprehensive debugging and testing capabilities
2025-09-16 12:40:15 -04:00
Your Name
995c3f526c Removed interference with extensions. Had to go back to only allowing handling single extension. 2025-09-16 11:55:47 -04:00
Your Name
77ea4a8e67 cleaned up visuals 2025-09-15 14:52:21 -04:00
Your Name
12d4810f4c login with exposed api for web page fixed. 2025-09-15 14:24:14 -04:00
Your Name
517974699d browser extension signing fixed 2025-09-15 13:51:41 -04:00
Your Name
bac621bbaa button example 2025-09-14 18:51:27 -04:00
Your Name
9f0b0638e5 Added some good examples 2025-09-14 16:15:35 -04:00
Your Name
b59bf17372 Fully functioning theme system. Bugs fixed 2025-09-14 13:23:52 -04:00
Your Name
3b1eb7f951 added embedded option 2025-09-13 15:21:18 -04:00
26 changed files with 13730 additions and 3388 deletions

10
.gitignore vendored
View File

@@ -1,6 +1,6 @@
# Dependencies
node_modules/
nostr-tools/
# IDE and OS files
.idea/
@@ -18,10 +18,4 @@ Thumbs.db
log.txt
Trash/
# Environment files
.env
# Aider files
.aider.chat.history.md
.aider.input.history
.aider.tags.cache.v3/
nostr-login/

211
17.md
View File

@@ -1,211 +0,0 @@
NIP-17
======
Private Direct Messages
-----------------------
`draft` `optional`
This NIP defines an encrypted direct messaging scheme using [NIP-44](44.md) encryption and [NIP-59](59.md) seals and gift wraps.
## Direct Message Kind
Kind `14` is a chat message. `p` tags identify one or more receivers of the message.
```jsonc
{
"id": "<usual hash>",
"pubkey": "<sender-pubkey>",
"created_at": "<current-time>",
"kind": 14,
"tags": [
["p", "<receiver-1-pubkey>", "<relay-url>"],
["p", "<receiver-2-pubkey>", "<relay-url>"],
["e", "<kind-14-id>", "<relay-url>"] // if this is a reply
["subject", "<conversation-title>"],
// rest of tags...
],
"content": "<message-in-plain-text>",
}
```
`.content` MUST be plain text. Fields `id` and `created_at` are required.
An `e` tag denotes the direct parent message this post is replying to.
`q` tags MAY be used when citing events in the `.content` with [NIP-21](21.md).
```json
["q", "<event-id> or <event-address>", "<relay-url>", "<pubkey-if-a-regular-event>"]
```
Kind `14`s MUST never be signed. If it is signed, the message might leak to relays and become **fully public**.
## File Message Kind
```jsonc
{
"id": "<usual hash>",
"pubkey": "<sender-pubkey>",
"created_at": "<current-time>",
"kind": 15,
"tags": [
["p", "<receiver-1-pubkey>", "<relay-url>"],
["p", "<receiver-2-pubkey>", "<relay-url>"],
["e", "<kind-14-id>", "<relay-url>", "reply"], // if this is a reply
["subject", "<conversation-title>"],
["file-type", "<file-mime-type>"],
["encryption-algorithm", "<encryption-algorithm>"],
["decryption-key", "<decryption-key>"],
["decryption-nonce", "<decryption-nonce>"],
["x", "<the SHA-256 hexencoded string of the file>"],
// rest of tags...
],
"content": "<file-url>"
}
```
Kind `15` is used for sending encrypted file event messages:
- `file-type`: Specifies the MIME type of the attached file (e.g., `image/jpeg`, `audio/mpeg`, etc.) before encryption.
- `encryption-algorithm`: Indicates the encryption algorithm used for encrypting the file. Supported algorithms: `aes-gcm`.
- `decryption-key`: The decryption key that will be used by the recipient to decrypt the file.
- `decryption-nonce`: The decryption nonce that will be used by the recipient to decrypt the file.
- `content`: The URL of the file (`<file-url>`).
- `x` containing the SHA-256 hexencoded string of the encrypted file.
- `ox` containing the SHA-256 hexencoded string of the file before encryption.
- `size` (optional) size of the encrypted file in bytes
- `dim` (optional) size in pixels in the form `<width>x<height>`
- `blurhash`(optional) the [blurhash](https://github.com/woltapp/blurhash) to show while the client is loading the file
- `thumb` (optional) URL of thumbnail with same aspect ratio (encrypted with the same key, nonce)
- `fallback` (optional) zero or more fallback file sources in case `url` fails (encrypted with the same key, nonce)
Just like kind `14`, kind `15`s MUST never be signed.
## Chat Rooms
The set of `pubkey` + `p` tags defines a chat room. If a new `p` tag is added or a current one is removed, a new room is created with a clean message history.
Clients SHOULD render messages of the same room in a continuous thread.
An optional `subject` tag defines the current name/topic of the conversation. Any member can change the topic by simply submitting a new `subject` to an existing `pubkey` + `p` tags room. There is no need to send `subject` in every message. The newest `subject` in the chat room is the subject of the conversation.
## Encrypting
Following [NIP-59](59.md), the **unsigned** `kind:14` & `kind:15` chat messages must be sealed (`kind:13`) and then gift-wrapped (`kind:1059`) to each receiver and the sender individually.
```js
{
"id": "<usual hash>",
"pubkey": randomPublicKey,
"created_at": randomTimeUpTo2DaysInThePast(),
"kind": 1059, // gift wrap
"tags": [
["p", receiverPublicKey, "<relay-url>"] // receiver
],
"content": nip44Encrypt(
{
"id": "<usual hash>",
"pubkey": senderPublicKey,
"created_at": randomTimeUpTo2DaysInThePast(),
"kind": 13, // seal
"tags": [], // no tags
"content": nip44Encrypt(unsignedKind14, senderPrivateKey, receiverPublicKey),
"sig": "<signed by senderPrivateKey>"
},
randomPrivateKey, receiverPublicKey
),
"sig": "<signed by randomPrivateKey>"
}
```
The encryption algorithm MUST use the latest version of [NIP-44](44.md).
Clients MUST verify if pubkey of the `kind:13` is the same pubkey on the `kind:14`, otherwise any sender can impersonate others by simply changing the pubkey on `kind:14`.
Clients SHOULD randomize `created_at` in up to two days in the past in both the seal and the gift wrap to make sure grouping by `created_at` doesn't reveal any metadata.
The gift wrap's `p` tag can be the receiver's main pubkey or an alias key created to receive DMs without exposing the receiver's identity.
Clients CAN offer disappearing messages by setting an `expiration` tag in the gift wrap of each receiver or by not generating a gift wrap to the sender's public key
## Publishing
Kind `10050` indicates the user's preferred relays to receive DMs. The event MUST include a list of `relay` tags with relay URIs.
```jsonc
{
"kind": 10050,
"tags": [
["relay", "wss://inbox.nostr.wine"],
["relay", "wss://myrelay.nostr1.com"],
],
"content": "",
// other fields...
}
```
Clients SHOULD publish kind `14` events to the `10050`-listed relays. If that is not found that indicates the user is not ready to receive messages under this NIP and clients shouldn't try.
## Relays
It's advisable that relays do not serve `kind:1059` to clients other than the ones tagged in them.
It's advisable that users choose relays that conform to these practices.
Clients SHOULD guide users to keep `kind:10050` lists small (1-3 relays) and SHOULD spread it to as many relays as viable.
## Benefits & Limitations
This NIP offers the following privacy and security features:
1. **No Metadata Leak**: Participant identities, each message's real date and time, event kinds, and other event tags are all hidden from the public. Senders and receivers cannot be linked with public information alone.
2. **No Public Group Identifiers**: There is no public central queue, channel or otherwise converging identifier to correlate or count all messages in the same group.
3. **No Moderation**: There are no group admins: no invitations or bans.
4. **No Shared Secrets**: No secret must be known to all members that can leak or be mistakenly shared
5. **Fully Recoverable**: Messages can be fully recoverable by any client with the user's private key
6. **Optional Forward Secrecy**: Users and clients can opt-in for "disappearing messages".
7. **Uses Public Relays**: Messages can flow through public relays without loss of privacy. Private relays can increase privacy further, but they are not required.
8. **Cold Storage**: Users can unilaterally opt-in to sharing their messages with a separate key that is exclusive for DM backup and recovery.
The main limitation of this approach is having to send a separate encrypted event to each receiver. Group chats with more than 100 participants should find a more suitable messaging scheme.
## Implementation
Clients implementing this NIP should by default only connect to the set of relays found in their `kind:10050` list. From that they should be able to load all messages both sent and received as well as get new live updates, making it for a very simple and lightweight implementation that should be fast.
When sending a message to anyone, clients must then connect to the relays in the receiver's `kind:10050` and send the events there but can disconnect right after unless more messages are expected to be sent (e.g. the chat tab is still selected). Clients should also send a copy of their outgoing messages to their own `kind:10050` relay set.
## Examples
This example sends the message `Hola, que tal?` from `nsec1w8udu59ydjvedgs3yv5qccshcj8k05fh3l60k9x57asjrqdpa00qkmr89m` to `nsec12ywtkplvyq5t6twdqwwygavp5lm4fhuang89c943nf2z92eez43szvn4dt`.
The two final GiftWraps, one to the receiver and the other to the sender, respectively, are:
```json
{
"id":"2886780f7349afc1344047524540ee716f7bdc1b64191699855662330bf235d8",
"pubkey":"8f8a7ec43b77d25799281207e1a47f7a654755055788f7482653f9c9661c6d51",
"created_at":1703128320,
"kind":1059,
"tags":[
["p", "918e2da906df4ccd12c8ac672d8335add131a4cf9d27ce42b3bb3625755f0788"]
],
"content":"AsqzdlMsG304G8h08bE67dhAR1gFTzTckUUyuvndZ8LrGCvwI4pgC3d6hyAK0Wo9gtkLqSr2rT2RyHlE5wRqbCOlQ8WvJEKwqwIJwT5PO3l2RxvGCHDbd1b1o40ZgIVwwLCfOWJ86I5upXe8K5AgpxYTOM1BD+SbgI5jOMA8tgpRoitJedVSvBZsmwAxXM7o7sbOON4MXHzOqOZpALpS2zgBDXSAaYAsTdEM4qqFeik+zTk3+L6NYuftGidqVluicwSGS2viYWr5OiJ1zrj1ERhYSGLpQnPKrqDaDi7R1KrHGFGyLgkJveY/45y0rv9aVIw9IWF11u53cf2CP7akACel2WvZdl1htEwFu/v9cFXD06fNVZjfx3OssKM/uHPE9XvZttQboAvP5UoK6lv9o3d+0GM4/3zP+yO3C0NExz1ZgFmbGFz703YJzM+zpKCOXaZyzPjADXp8qBBeVc5lmJqiCL4solZpxA1865yPigPAZcc9acSUlg23J1dptFK4n3Tl5HfSHP+oZ/QS/SHWbVFCtq7ZMQSRxLgEitfglTNz9P1CnpMwmW/Y4Gm5zdkv0JrdUVrn2UO9ARdHlPsW5ARgDmzaxnJypkfoHXNfxGGXWRk0sKLbz/ipnaQP/eFJv/ibNuSfqL6E4BnN/tHJSHYEaTQ/PdrA2i9laG3vJti3kAl5Ih87ct0w/tzYfp4SRPhEF1zzue9G/16eJEMzwmhQ5Ec7jJVcVGa4RltqnuF8unUu3iSRTQ+/MNNUkK6Mk+YuaJJs6Fjw6tRHuWi57SdKKv7GGkr0zlBUU2Dyo1MwpAqzsCcCTeQSv+8qt4wLf4uhU9Br7F/L0ZY9bFgh6iLDCdB+4iABXyZwT7Ufn762195hrSHcU4Okt0Zns9EeiBOFxnmpXEslYkYBpXw70GmymQfJlFOfoEp93QKCMS2DAEVeI51dJV1e+6t3pCSsQN69Vg6jUCsm1TMxSs2VX4BRbq562+VffchvW2BB4gMjsvHVUSRl8i5/ZSDlfzSPXcSGALLHBRzy+gn0oXXJ/447VHYZJDL3Ig8+QW5oFMgnWYhuwI5QSLEyflUrfSz+Pdwn/5eyjybXKJftePBD9Q+8NQ8zulU5sqvsMeIx/bBUx0fmOXsS3vjqCXW5IjkmSUV7q54GewZqTQBlcx+90xh/LSUxXex7UwZwRnifvyCbZ+zwNTHNb12chYeNjMV7kAIr3cGQv8vlOMM8ajyaZ5KVy7HpSXQjz4PGT2/nXbL5jKt8Lx0erGXsSsazkdoYDG3U",
"sig":"a3c6ce632b145c0869423c1afaff4a6d764a9b64dedaf15f170b944ead67227518a72e455567ca1c2a0d187832cecbde7ed478395ec4c95dd3e71749ed66c480"
}
```
```json
{
"id":"162b0611a1911cfcb30f8a5502792b346e535a45658b3a31ae5c178465509721",
"pubkey":"626be2af274b29ea4816ad672ee452b7cf96bbb4836815a55699ae402183f512",
"created_at":1702711587,
"kind":1059,
"tags":[
["p", "44900586091b284416a0c001f677f9c49f7639a55c3f1e2ec130a8e1a7998e1b"]
],
"content":"AsTClTzr0gzXXji7uye5UB6LYrx3HDjWGdkNaBS6BAX9CpHa+Vvtt5oI2xJrmWLen+Fo2NBOFazvl285Gb3HSM82gVycrzx1HUAaQDUG6HI7XBEGqBhQMUNwNMiN2dnilBMFC3Yc8ehCJT/gkbiNKOpwd2rFibMFRMDKai2mq2lBtPJF18oszKOjA+XlOJV8JRbmcAanTbEK5nA/GnG3eGUiUzhiYBoHomj3vztYYxc0QYHOx0WxiHY8dsC6jPsXC7f6k4P+Hv5ZiyTfzvjkSJOckel1lZuE5SfeZ0nduqTlxREGeBJ8amOykgEIKdH2VZBZB+qtOMc7ez9dz4wffGwBDA7912NFS2dPBr6txHNxBUkDZKFbuD5wijvonZDvfWq43tZspO4NutSokZB99uEiRH8NAUdGTiNb25m9JcDhVfdmABqTg5fIwwTwlem5aXIy8b66lmqqz2LBzJtnJDu36bDwkILph3kmvaKPD8qJXmPQ4yGpxIbYSTCohgt2/I0TKJNmqNvSN+IVoUuC7ZOfUV9lOV8Ri0AMfSr2YsdZ9ofV5o82ClZWlWiSWZwy6ypa7CuT1PEGHzywB4CZ5ucpO60Z7hnBQxHLiAQIO/QhiBp1rmrdQZFN6PUEjFDloykoeHe345Yqy9Ke95HIKUCS9yJurD+nZjjgOxZjoFCsB1hQAwINTIS3FbYOibZnQwv8PXvcSOqVZxC9U0+WuagK7IwxzhGZY3vLRrX01oujiRrevB4xbW7Oxi/Agp7CQGlJXCgmRE8Rhm+Vj2s+wc/4VLNZRHDcwtfejogjrjdi8p6nfUyqoQRRPARzRGUnnCbh+LqhigT6gQf3sVilnydMRScEc0/YYNLWnaw9nbyBa7wFBAiGbJwO40k39wj+xT6HTSbSUgFZzopxroO3f/o4+ubx2+IL3fkev22mEN38+dFmYF3zE+hpE7jVxrJpC3EP9PLoFgFPKCuctMnjXmeHoiGs756N5r1Mm1ffZu4H19MSuALJlxQR7VXE/LzxRXDuaB2u9days/6muP6gbGX1ASxbJd/ou8+viHmSC/ioHzNjItVCPaJjDyc6bv+gs1NPCt0qZ69G+JmgHW/PsMMeL4n5bh74g0fJSHqiI9ewEmOG/8bedSREv2XXtKV39STxPweceIOh0k23s3N6+wvuSUAJE7u1LkDo14cobtZ/MCw/QhimYPd1u5HnEJvRhPxz0nVPz0QqL/YQeOkAYk7uzgeb2yPzJ6DBtnTnGDkglekhVzQBFRJdk740LEj6swkJ",
"sig":"c94e74533b482aa8eeeb54ae72a5303e0b21f62909ca43c8ef06b0357412d6f8a92f96e1a205102753777fd25321a58fba3fb384eee114bd53ce6c06a1c22bab"
}
```

297
44.md
View File

@@ -1,297 +0,0 @@
NIP-44
======
Encrypted Payloads (Versioned)
------------------------------
`optional`
The NIP introduces a new data format for keypair-based encryption. This NIP is versioned
to allow multiple algorithm choices to exist simultaneously. This format may be used for
many things, but MUST be used in the context of a signed event as described in NIP-01.
*Note*: this format DOES NOT define any `kind`s related to a new direct messaging standard,
only the encryption required to define one. It SHOULD NOT be used as a drop-in replacement
for NIP-04 payloads.
## Versions
Currently defined encryption algorithms:
- `0x00` - Reserved
- `0x01` - Deprecated and undefined
- `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64
## Limitations
Every nostr user has their own public key, which solves key distribution problems present
in other solutions. However, nostr's relay-based architecture makes it difficult to implement
more robust private messaging protocols with things like metadata hiding, forward secrecy,
and post compromise secrecy.
The goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed
event. When applying this NIP to any use case, it's important to keep in mind your users' threat
model and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE
messaging software and limit use of nostr to exchanging contacts.
On its own, messages sent using this scheme have a number of important shortcomings:
- No deniability: it is possible to prove an event was signed by a particular key
- No forward secrecy: when a key is compromised, it is possible to decrypt all previous conversations
- No post-compromise security: when a key is compromised, it is possible to decrypt all future conversations
- No post-quantum security: a powerful quantum computer would be able to decrypt the messages
- IP address leak: user IP may be seen by relays and all intermediaries between user and relay
- Date leak: `created_at` is public, since it is a part of NIP-01 event
- Limited message size leak: padding only partially obscures true message length
- No attachments: they are not supported
Lack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking
relays to delete stored messages after a certain duration has elapsed.
## Version 2
NIP-44 version 2 has the following design characteristics:
- Payloads are authenticated using a MAC before signing rather than afterwards because events are assumed
to be signed as specified in NIP-01. The outer signature serves to authenticate the full payload, and MUST
be validated before decrypting.
- ChaCha is used instead of AES because it's faster and has
[better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/).
- ChaCha is used instead of XChaCha because XChaCha has not been standardized. Also, xChaCha's improved collision
resistance of nonces isn't necessary since every message has a new (key, nonce) pair.
- HMAC-SHA256 is used instead of Poly1305 because polynomial MACs are much easier to forge.
- SHA256 is used instead of SHA3 or BLAKE because it is already used in nostr. Also BLAKE's speed advantage
is smaller in non-parallel environments.
- A custom padding scheme is used instead of padmé because it provides better leakage reduction for small messages.
- Base64 encoding is used instead of another encoding algorithm because it is widely available, and is already used in nostr.
### Encryption
1. Calculate a conversation key
- Execute ECDH (scalar multiplication) of public key B by private key A
Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point
- Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')`
- HKDF output will be a `conversation_key` between two users.
- It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)`
2. Generate a random 32-byte nonce
- Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator)
- Don't generate a nonce from message content
- Don't re-use the same nonce between messages: doing so would make them decryptable,
but won't leak the long-term key
3. Calculate message keys
- The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes long
- Use HKDF-expand, with sha256, `PRK=conversation_key`, `info=nonce` and `L=76`
- Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76)
4. Add padding
- Content must be encoded from UTF-8 into byte array
- Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes
- Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]`
- Padding algorithm is related to powers-of-two, with min padded msg size of 32 bytes
- Plaintext length is encoded in big-endian as first 2 bytes of the padded blob
5. Encrypt padded content
- Use ChaCha20, with key and nonce from step 3
6. Calculate MAC (message authentication code)
- AAD (additional authenticated data) is used - instead of calculating MAC on ciphertext,
it's calculated over a concatenation of `nonce` and `ciphertext`
- Validate that AAD (nonce) is 32 bytes
7. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)`
Encrypted payloads MUST be included in an event's payload, hashed, and signed as defined in NIP 01, using schnorr
signature scheme over secp256k1.
### Decryption
Before decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be
a valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact
validation rules, refer to BIP-340.
1. Check if first payload's character is `#`
- `#` is an optional future-proof flag that means non-base64 encoding is used
- The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`,
implementations MUST indicate that the encryption version is not yet supported
2. Decode base64
- Base64 is decoded into `version, nonce, ciphertext, mac`
- If the version is unknown, implementations must indicate that the encryption version is not supported
- Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars
- Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes
3. Calculate conversation key
- See step 1 of [encryption](#Encryption)
4. Calculate message keys
- See step 3 of [encryption](#Encryption)
5. Calculate MAC (message authentication code) with AAD and compare
- Stop and throw an error if MAC doesn't match the decoded one from step 2
- Use constant-time comparison algorithm
6. Decrypt ciphertext
- Use ChaCha20 with key and nonce from step 3
7. Remove padding
- Read the first two BE bytes of plaintext that correspond to plaintext length
- Verify that the length of sliced plaintext matches the value of the two BE bytes
- Verify that calculated padding from step 3 of the [encryption](#Encryption) process matches the actual padding
### Details
- Cryptographic methods
- `secure_random_bytes(length)` fetches randomness from CSPRNG.
- `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869)
with SHA256 hash function comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)`.
- `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439) with
starting counter set to 0.
- `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104).
- `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a ⋅ B`), defined in
[BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki).
The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method
`bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid,
on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]`.
NIP44 doesn't do hashing of the output: keep this in mind, because some libraries hash it using sha256.
As an example, in libsecp256k1, unhashed version is available in `secp256k1_ec_pubkey_tweak_mul`
- Operators
- `x[i:j]`, where `x` is a byte array and `i, j <= 0` returns a `(j - i)`-byte array with a copy of the
`i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`.
- Constants `c`:
- `min_plaintext_size` is 1. 1 byte msg is padded to 32 bytes.
- `max_plaintext_size` is 65535 (64kB - 1). It is padded to 65536 bytes.
- Functions
- `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding)
- `concat` refers to byte array concatenation
- `is_equal_ct(a, b)` is constant-time equality check of 2 byte arrays
- `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back
- `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array
- `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array
- `zeros(length)` creates byte array of length `length >= 0`, filled with zeros
- `floor(number)` and `log2(number)` are well-known mathematical methods
### Implementation pseudocode
The following is a collection of python-like pseudocode functions which implement the above primitives,
intended to guide implementers. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.
```py
# Calculates length of the padded byte array.
def calc_padded_len(unpadded_len):
next_power = 1 << (floor(log2(unpadded_len - 1))) + 1
if next_power <= 256:
chunk = 32
else:
chunk = next_power / 8
if unpadded_len <= 32:
return 32
else:
return chunk * (floor((len - 1) / chunk) + 1)
# Converts unpadded plaintext to padded bytearray
def pad(plaintext):
unpadded = utf8_encode(plaintext)
unpadded_len = len(plaintext)
if (unpadded_len < c.min_plaintext_size or
unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length')
prefix = write_u16_be(unpadded_len)
suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len)
return concat(prefix, unpadded, suffix)
# Converts padded bytearray to unpadded plaintext
def unpad(padded):
unpadded_len = read_uint16_be(padded[0:2])
unpadded = padded[2:2+unpadded_len]
if (unpadded_len == 0 or
len(unpadded) != unpadded_len or
len(padded) != 2 + calc_padded_len(unpadded_len)): raise Exception('invalid padding')
return utf8_decode(unpadded)
# metadata: always 65b (version: 1b, nonce: 32b, max: 32b)
# plaintext: 1b to 0xffff
# padded plaintext: 32b to 0xffff
# ciphertext: 32b+2 to 0xffff+2
# raw payload: 99 (65+32+2) to 65603 (65+0xffff+2)
# compressed payload (base64): 132b to 87472b
def decode_payload(payload):
plen = len(payload)
if plen == 0 or payload[0] == '#': raise Exception('unknown version')
if plen < 132 or plen > 87472: raise Exception('invalid payload size')
data = base64_decode(payload)
dlen = len(d)
if dlen < 99 or dlen > 65603: raise Exception('invalid data size');
vers = data[0]
if vers != 2: raise Exception('unknown version ' + vers)
nonce = data[1:33]
ciphertext = data[33:dlen - 32]
mac = data[dlen - 32:dlen]
return (nonce, ciphertext, mac)
def hmac_aad(key, message, aad):
if len(aad) != 32: raise Exception('AAD associated data must be 32 bytes');
return hmac(sha256, key, concat(aad, message));
# Calculates long-term key between users A and B: `get_key(Apriv, Bpub) == get_key(Bpriv, Apub)`
def get_conversation_key(private_key_a, public_key_b):
shared_x = secp256k1_ecdh(private_key_a, public_key_b)
return hkdf_extract(IKM=shared_x, salt=utf8_encode('nip44-v2'))
# Calculates unique per-message key
def get_message_keys(conversation_key, nonce):
if len(conversation_key) != 32: raise Exception('invalid conversation_key length')
if len(nonce) != 32: raise Exception('invalid nonce length')
keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76)
chacha_key = keys[0:32]
chacha_nonce = keys[32:44]
hmac_key = keys[44:76]
return (chacha_key, chacha_nonce, hmac_key)
def encrypt(plaintext, conversation_key, nonce):
(chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)
padded = pad(plaintext)
ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded)
mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)
return base64_encode(concat(write_u8(2), nonce, ciphertext, mac))
def decrypt(payload, conversation_key):
(nonce, ciphertext, mac) = decode_payload(payload)
(chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)
calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)
if not is_equal_ct(calculated_mac, mac): raise Exception('invalid MAC')
padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext)
return unpad(padded_plaintext)
# Usage:
# conversation_key = get_conversation_key(sender_privkey, recipient_pubkey)
# nonce = secure_random_bytes(32)
# payload = encrypt('hello world', conversation_key, nonce)
# 'hello world' == decrypt(payload, conversation_key)
```
### Audit
The v2 of the standard was audited by [Cure53](https://cure53.de) in December 2023.
Check out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf)
and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf).
### Tests and code
A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.
We publish extensive test vectors. Instead of having it in the document directly, a sha256 checksum of vectors is provided:
269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json
Example of a test vector from the file:
```json
{
"sec1": "0000000000000000000000000000000000000000000000000000000000000001",
"sec2": "0000000000000000000000000000000000000000000000000000000000000002",
"conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",
"nonce": "0000000000000000000000000000000000000000000000000000000000000001",
"plaintext": "a",
"payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
}
```
The file also contains intermediate values. A quick guidance with regards to its usage:
- `valid.get_conversation_key`: calculate conversation_key from secret key sec1 and public key pub2
- `valid.get_message_keys`: calculate chacha_key, chacha_nonce, hmac_key from conversation_key and nonce
- `valid.calc_padded_len`: take unpadded length (first value), calculate padded length (second value)
- `valid.encrypt_decrypt`: emulate real conversation. Calculate pub2 from sec2, verify conversation_key from (sec1, pub2), encrypt, verify payload, then calculate pub1 from sec1, verify conversation_key from (sec2, pub1), decrypt, verify plaintext.
- `valid.encrypt_decrypt_long_msg`: same as previous step, but instead of a full plaintext and payload, their checksum is provided.
- `invalid.encrypt_msg_lengths`
- `invalid.get_conversation_key`: calculating conversation_key must throw an error
- `invalid.decrypt`: decrypting message content must throw an error

133
README.md
View File

@@ -1,2 +1,135 @@
Nostr_Login_Lite
===========
## API
Complete configuration showing all available options:
```javascript
await window.NOSTR_LOGIN_LITE.init({
// Theme configuration
theme: 'default', // 'default' | 'dark' | custom theme name
// 🔐 Authentication persistence configuration
persistence: true, // Enable persistent authentication (default: true)
isolateSession: false, // Use sessionStorage for per-tab isolation (default: false = localStorage)
// Relay configuration
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
// Authentication methods
methods: {
extension: true, // Browser extensions (Alby, nos2x, etc.)
local: true, // Manual key entry & generation
readonly: true, // Read-only mode (no signing)
connect: true, // NIP-46 remote signers
otp: false // OTP/DM authentication (not implemented yet)
},
// Floating tab configuration
floatingTab: {
enabled: true, // Show/hide floating login tab
hPosition: 0.95, // 0.0 = left edge, 1.0 = right edge
vPosition: 0.1, // 0.0 = top edge, 1.0 = bottom edge
offset: { x: 0, y: 0 }, // Fine-tune positioning (pixels)
appearance: {
style: 'pill', // 'pill' | 'square' | 'circle'
theme: 'auto', // 'auto' | 'light' | 'dark'
icon: '[LOGIN]', // Text-based icon
text: 'Sign In', // Button text
iconOnly: false // Show icon only (no text)
},
behavior: {
hideWhenAuthenticated: false, // Keep visible after login
showUserInfo: true, // Show user info when authenticated
autoSlide: true, // Slide animation on hover
persistent: false // Persist across page reloads
}
}
});
// Control Methods
NOSTR_LOGIN_LITE.launch(); // Open login modal
NOSTR_LOGIN_LITE.logout(); // Clear authentication state
NOSTR_LOGIN_LITE.switchTheme('dark'); // Change theme
NOSTR_LOGIN_LITE.showFloatingTab(); // Show floating tab
NOSTR_LOGIN_LITE.hideFloatingTab(); // Hide floating tab
NOSTR_LOGIN_LITE.updateFloatingTab(options); // Update floating tab options
NOSTR_LOGIN_LITE.toggleFloatingTab(); // Toggle floating tab visibility
// Get Authentication State (Single Source of Truth)
const authState = NOSTR_LOGIN_LITE.getAuthState();
const isAuthenticated = !!authState;
const userInfo = authState; // Contains { method, pubkey, etc. }
```
**Authentication Persistence:**
Two-tier configuration system:
1. **`persistence: boolean`** - Master switch for authentication persistence
- `true` (default): Save authentication state for automatic restore
- `false`: No persistence - user must login fresh every time
2. **`isolateSession: boolean`** - Storage location when persistence is enabled
- `false` (default): Use localStorage - shared across tabs/windows
- `true`: Use sessionStorage - isolated per tab/window
**Use Cases for Session Isolation (`isolateSession: true`):**
- Multi-tenant applications where different tabs need different users
- Testing environments requiring separate authentication per tab
- Privacy-focused applications that shouldn't share login state across tabs
## Embedded Modal API
Embed login interface directly into page element:
```javascript
// Initialize library first
await NOSTR_LOGIN_LITE.init({
methods: {
extension: true,
local: true,
readonly: true
}
});
// Embed into container
const modal = NOSTR_LOGIN_LITE.embed('#login-container', {
title: 'Login',
showHeader: true,
seamless: false // true = no borders/shadows, blends into page
});
```
Container can be CSS selector or DOM element. Modal renders inline without backdrop overlay.
## Logout API
To log out users and clear authentication state:
```javascript
// Unified logout method - works for all authentication methods
window.NOSTR_LOGIN_LITE.logout();
```
This will:
- Clear persistent authentication data from localStorage
- Dispatch `nlLogout` event for custom cleanup
- Reset the authentication state across all components
### Event Handling
Listen for logout events in your application:
```javascript
window.addEventListener('nlLogout', () => {
console.log('User logged out');
// Clear your application's UI state
// Redirect to login page, etc.
});
```
The logout system works consistently across all authentication methods (extension, local keys, NIP-46, etc.) and all UI components (floating tab, modal, embedded).

View File

@@ -1,50 +0,0 @@
# NOSTR_LOGIN_LITE Examples
This directory contains examples and tests for NOSTR_LOGIN_LITE using the local bundle setup.
## Files
### 🔬 comprehensive-test.html
**The main diagnostic and testing tool**
- Comprehensive test suite with extensive debugging output
- Tests all library functions, dependencies, crypto, storage, etc.
- Results displayed on webpage for easy copying and debugging
- Run this when you need to diagnose issues or verify functionality
### 📱 simple-demo.html
Basic demonstration of NOSTR_LOGIN_LITE integration
- Minimal setup example
- Good starting point for new implementations
### 🎨 modal-login-demo.html
Demonstrates modal-based login flow
- Shows how to trigger and handle the login modal
- Example of auth event handling
### 👤 login-and-profile.html
Login and user profile demonstration
- Shows authentication flow
- Displays user profile information after login
### 🔗 nip46-bunker-demo.html
NIP-46 remote signing demonstration
- Shows how to connect to remote signers/bunkers
- Advanced use case example
## Usage
1. Start a local web server (e.g., `python -m http.server 5501` or Live Server in VS Code)
2. Navigate to any HTML file
3. For comprehensive testing and debugging, use `comprehensive-test.html`
All examples use the local bundle setup with two files:
1. `../lite/nostr.bundle.js` - Official nostr-tools bundle
2. `../lite/nostr-lite.js` - NOSTR_LOGIN_LITE library with embedded NIP-46 extension
## Architecture Update (2025-09-13)
The library has been simplified from a three-file to a two-file architecture:
-**Before:** `nostr.bundle.js` + `nip46-extension.js` + `nostr-lite.js`
-**Now:** `nostr.bundle.js` + `nostr-lite.js` (with embedded NIP-46)
All functionality remains identical - NIP-46 remote signing, all auth methods, and full compatibility are preserved.

134
examples/button.html Normal file
View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedded NOSTR_LOGIN_LITE</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 40px;
background: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 90vh;
}
.container {
max-width: 400px;
width: 100%;
}
#login-button {
background: #0066cc;
color: white;
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
text-align: center;
transition: background 0.2s;
}
#login-button:hover {
opacity: 0.8;
}
</style>
</head>
<body>
<div class="container">
<div id="login-button">Login</div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
let isAuthenticated = false;
let currentUser = null;
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
methods: {
extension: true,
local: true,
readonly: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: false
}
});
// Listen for authentication events
window.addEventListener('nlMethodSelected', handleAuthEvent);
window.addEventListener('nlLogout', handleLogoutEvent);
// Check for existing authentication state
checkAuthState();
// Initialize button
updateButtonState();
});
function handleAuthEvent(event) {
const { pubkey, method } = event.detail;
console.log(`Authenticated with ${method}, pubkey: ${pubkey}`);
isAuthenticated = true;
currentUser = event.detail;
updateButtonState();
}
function handleLogoutEvent() {
console.log('Logout event received');
isAuthenticated = false;
currentUser = null;
updateButtonState();
}
function checkAuthState() {
// Check if user is already authenticated (from persistent storage)
try {
// Try to get public key - this will work if already authenticated
window.nostr.getPublicKey().then(pubkey => {
console.log('Found existing authentication, pubkey:', pubkey);
isAuthenticated = true;
currentUser = { pubkey, method: 'persistent' };
updateButtonState();
}).catch(error => {
console.log('No existing authentication found:', error.message);
// User is not authenticated, button stays in login state
});
} catch (error) {
console.log('No existing authentication found');
// User is not authenticated, button stays in login state
}
}
function updateButtonState() {
const button = document.getElementById('login-button');
if (isAuthenticated) {
button.textContent = 'Logout';
button.onclick = () => window.NOSTR_LOGIN_LITE.logout();
button.style.background = '#dc3545'; // Red for logout
} else {
button.textContent = 'Login';
button.onclick = () => window.NOSTR_LOGIN_LITE.launch('login');
button.style.background = '#0066cc'; // Blue for login
}
}
</script>
</body>
</html>

58
examples/embedded.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedded NOSTR_LOGIN_LITE</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 40px;
background: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
max-width: 400px;
width: 100%;
}
#login-container {
/* No styling - let embedded modal blend seamlessly */
}
</style>
</head>
<body>
<div class="container">
<div id="login-container"></div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme:'default',
methods: {
extension: true,
local: true,
seedphrase: true,
readonly: true,
connect: true,
remote: true,
otp: true
}
});
window.NOSTR_LOGIN_LITE.embed('#login-container', {
seamless: true
});
});
</script>
</body>
</html>

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -9,249 +10,33 @@
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #ffffff;
min-height: 100vh;
color: white;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 30px;
background: linear-gradient(45deg, #fff, #007bff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.status {
padding: 15px;
border-radius: 10px;
margin: 15px 0;
font-weight: 500;
}
.status.logged-out {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.status.logged-in {
background: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.5);
}
.status.loading {
background: rgba(255, 193, 7, 0.2);
border: 1px solid rgba(255, 193, 7, 0.5);
}
.button {
background: linear-gradient(45deg, #007bff, #0056b3);
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
margin: 10px 5px;
text-decoration: none;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.4);
}
.button.secondary {
background: linear-gradient(45deg, #6c757d, #495057);
}
.button.secondary:hover {
box-shadow: 0 4px 15px rgba(108, 117, 125, 0.4);
}
.button.danger {
background: linear-gradient(45deg, #dc3545, #c82333);
}
.button.danger:hover {
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.4);
}
.profile-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin: 20px 0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.profile-header {
text-align: center;
margin-bottom: 20px;
}
.profile-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin: 0 auto 15px;
display: block;
background: rgba(255, 255, 255, 0.2);
}
.profile-name {
font-size: 24px;
font-weight: 700;
margin-bottom: 5px;
}
.profile-about {
font-style: italic;
opacity: 0.8;
margin-bottom: 15px;
}
.profile-pubkey {
font-family: 'Courier New', monospace;
font-size: 14px;
background: rgba(0, 0, 0, 0.2);
padding: 10px;
border-radius: 5px;
word-break: break-all;
}
.console-output {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.console-entry {
margin: 5px 0;
}
.console-timestamp {
color: #ccc;
margin-right: 10px;
}
.info-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
.info-section h3 {
margin-top: 0;
color: #007bff;
}
.methods-list {
list-style: none;
padding: 0;
}
.methods-list li {
background: rgba(255, 255, 255, 0.1);
margin: 10px 0;
padding: 10px 15px;
border-radius: 8px;
border-left: 4px solid #007bff;
color: #000000;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 NOSTR_LOGIN_LITE - All Login Methods Test</h1>
<div id="status" class="status logged-out">
⏳ Initializing NOSTR_LOGIN_LITE...
</div>
<div id="login-section">
<button id="launch-modal" class="button">🚀 Launch Login Modal</button>
<p style="font-size: 14px; opacity: 0.8; margin-top: 10px;">
Click to open the NOSTR_LOGIN_LITE modal and test all available login methods:
</p>
<div class="info-section">
<h3>Available Login Methods</h3>
<ul class="methods-list">
<li><strong>Browser Extension:</strong> Alby, nos2x, etc. (if installed)</li>
<li><strong>Local Key:</strong> Generate new keys or import existing private key/nsec</li>
<li><strong>Read Only:</strong> Access public content without authentication</li>
<li><strong>Nostr Connect (NIP-46):</strong> Connect to remote signing services</li>
<li><strong>DM/OTP:</strong> Secure local accounts with one-time passwords</li>
</ul>
</div>
</div>
<div id="profile-section" style="display: none;">
<div class="profile-card">
<div class="profile-header">
<img id="profile-picture" class="profile-avatar" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iNDAiIGN5PSI0MCIgcj0iNDAiIGZpbGw9IiNmNmY2ZjYiLz4KPHBhdGggZD0iTTQwIDQ1QzQ1IDQ1IDUwIDQyIDUwIDQwUzQ1IDQxIDQwIDQ1WiIgc3Ryb2tlPSIjYmJkIiBzdHJva2Utd2lkdGg9IjIiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgo8Y2lyY2xlIGN4PSIzNSIgY3k9IjM1IiByPSIyIiBmaWxsPSIjYmJkIi8+CjxjaXJjbGUgY3g9IjQ1IiBjeT0iMzUiIHI9IjIiIGZpbGw9IiNiYmQiLz4KPGRlZnM+Cj08L2RlZnM+Cjwvc3ZnPgo=" alt="Profile Picture">
<div id="profile-name" class="profile-name">Loading...</div>
<div id="profile-about" class="profile-about"></div>
</div>
<div>
<strong>Public Key:</strong>
<div id="profile-pubkey" class="profile-pubkey"></div>
</div>
</div>
<button id="refresh-profile" class="button secondary">🔄 Refresh Profile</button>
<button id="logout" class="button danger">🚪 Logout</button>
</div>
<div class="console-output" id="console-display">
<div class="console-entry">
<span class="console-timestamp">[Console]</span> Ready for testing
</div>
</div>
<div id="login-section">
<!-- Login UI if needed -->
</div>
<div id="profile-section">
<img id="profile-picture">
<div id="profile-pubkey"></div>
<div id="profile-name"></div>
<div id="profile-about"></div>
</div>
<!-- Load the official nostr-tools bundle first -->
<script src="../lite/nostr.bundle.js"></script>
<!-- Load NOSTR_LOGIN_LITE main library (now includes NIP-46 extension) -->
<script src="../lite/nostr-lite.js"></script>
<script>
// Console logging helper
function log(level, message) {
const consoleDiv = document.getElementById('console-display');
const entry = document.createElement('div');
entry.className = 'console-entry';
const timestamp = new Date().toLocaleTimeString();
const prefix = level === 'ERROR' ? '[ERROR]' :
level === 'SUCCESS' ? '[SUCCESS]' :
level === 'WARNING' ? '[WARNING]' : '[INFO]';
entry.innerHTML = `<span class="console-timestamp">[${timestamp}] ${prefix}</span> ${message}`;
consoleDiv.appendChild(entry);
consoleDiv.scrollTop = consoleDiv.scrollHeight;
}
// Global variables
let nlLite = null;
@@ -260,155 +45,119 @@
// Initialize NOSTR_LOGIN_LITE
async function initializeApp() {
log('INFO', 'Initializing NOSTR_LOGIN_LITE...');
// console.log('INFO', 'Initializing NOSTR_LOGIN_LITE...');
try {
await window.NOSTR_LOGIN_LITE.init({
theme: 'dark',
theme: 'default',
darkMode: false,
relays: [relayUrl, 'wss://relay.damus.io'],
methods: {
extension: true,
local: true,
readonly: true,
remote: true, // Enables "Nostr Connect" (NIP-46)
seedphrase: true,
connect: true, // Enables "Nostr Connect" (NIP-46)
remote: true, // Also needed for "Nostr Connect" compatibility
otp: true // Enables "DM/OTP"
},
floatingTab: {
enabled: true,
hPosition: .98, // 95% from left
vPosition: 0, // 50% from top (center)
getUserInfo: true, // Fetch user profiles
getUserRelay: ['wss://relay.laantungir.net'], // Custom relays for profiles
appearance: {
style: 'minimal',
theme: 'auto',
icon: '',
text: 'Login',
iconOnly: false
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: false,
persistent: false
},
animation: {
slideDirection: 'right' // Slide to the right when hiding
}
},
debug: true
});
nlLite = window.NOSTR_LOGIN_LITE;
log('SUCCESS', 'NOSTR_LOGIN_LITE initialized successfully');
document.getElementById('status').innerHTML = '✅ Ready - Click "Launch Login Modal" to test all login methods';
document.getElementById('status').className = 'status logged-in';
console.log('SUCCESS', 'NOSTR_LOGIN_LITE initialized successfully');
window.addEventListener('nlMethodSelected', handleAuthEvent);
window.addEventListener('nlLogout', handleLogoutEvent);
} catch (error) {
log('ERROR', `Initialization failed: ${error.message}`);
document.getElementById('status').innerHTML = '❌ Failed to initialize NOSTR_LOGIN_LITE';
document.getElementById('status').className = 'status logged-out';
console.log('ERROR', `Initialization failed: ${error.message}`);
}
}
// Launch the login modal
async function launchLoginModal() {
log('INFO', 'Launching NOSTR_LOGIN_LITE modal...');
document.getElementById('status').innerHTML = '🔄 Opening login modal...';
document.getElementById('status').className = 'status loading';
try {
// Launch the modal
await nlLite.launch('login');
log('SUCCESS', 'Login modal launched successfully');
} catch (error) {
log('ERROR', `Failed to launch modal: ${error.message}`);
document.getElementById('status').innerHTML = '❌ Failed to launch modal';
document.getElementById('status').className = 'status logged-out';
}
}
// Handle authentication events
function handleAuthEvent(event) {
const { type, pubkey, method, error } = event.detail;
log('INFO', `Auth event received: type=${type}, method=${method}`);
if (type === 'login' && pubkey) {
const { pubkey, method, error } = event.detail;
console.log('INFO', `Auth event received: method=${method}`);
if (method && pubkey) {
userPubkey = pubkey;
log('SUCCESS', `Login successful! Method: ${method}, Pubkey: ${pubkey}`);
document.getElementById('status').innerHTML = `✅ Logged in via ${method}!`;
document.getElementById('status').className = 'status logged-in';
// Show profile section
document.getElementById('login-section').style.display = 'none';
document.getElementById('profile-section').style.display = 'block';
// Load profile
console.log('SUCCESS', `Login successful! Method: ${method}, Pubkey: ${pubkey}`);
loadUserProfile();
} else if (type === 'logout') {
log('INFO', 'User logged out');
userPubkey = null;
document.getElementById('status').innerHTML = '✅ Ready - Click "Launch Login Modal" to test all login methods';
document.getElementById('status').className = 'status logged-in';
// Show login section
document.getElementById('login-section').style.display = 'block';
document.getElementById('profile-section').style.display = 'none';
} else if (error) {
log('ERROR', `Authentication error: ${error}`);
document.getElementById('status').innerHTML = '❌ Authentication failed';
document.getElementById('status').className = 'status logged-out';
console.log('ERROR', `Authentication error: ${error}`);
}
}
// Load user profile
function handleLogoutEvent() {
console.log('INFO', 'Logout event received');
// Clear local UI state
userPubkey = null;
document.getElementById('profile-name').textContent = '';
document.getElementById('profile-about').textContent = '';
document.getElementById('profile-pubkey').textContent = '';
document.getElementById('profile-picture').src = '';
}
// Load user profile using nostr-tools pool
async function loadUserProfile() {
if (!userPubkey) return;
log('INFO', `Loading profile for: ${userPubkey}`);
console.log('INFO', `Loading profile for: ${userPubkey}`);
document.getElementById('profile-name').textContent = 'Loading profile...';
document.getElementById('profile-pubkey').textContent = userPubkey;
try {
// Simple WebSocket connection to get profile
const ws = new WebSocket(relayUrl);
// Create a SimplePool instance
const pool = new window.NostrTools.SimplePool();
const relays = [relayUrl, 'wss://relay.laantungir.net'];
ws.onopen = () => {
log('SUCCESS', 'WebSocket connected, requesting profile...');
const req = JSON.stringify([
'REQ',
'profile',
{
kinds: [0],
authors: [userPubkey],
limit: 1
}
]);
ws.send(req);
};
// Get profile event (kind 0) for the user using querySync
const events = await pool.querySync(relays, {
kinds: [0],
authors: [userPubkey],
limit: 1
});
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
const [type, subscriptionId, eventData] = message;
pool.close(relays); // Clean up connections
if (type === 'EVENT' && eventData && eventData.kind === 0) {
log('SUCCESS', 'Profile event received');
const profile = JSON.parse(eventData.content);
displayProfile(profile);
ws.close();
} else if (type === 'EOSE') {
log('INFO', 'End of subscription');
ws.close();
}
} catch (parseError) {
log('ERROR', `Failed to parse WebSocket message: ${parseError.message}`);
}
};
ws.onerror = (error) => {
log('ERROR', `WebSocket error: ${error.message || 'Connection failed'}`);
document.getElementById('profile-name').textContent = 'Error loading profile';
};
// Timeout after 5 seconds
setTimeout(() => {
if (ws.readyState !== WebSocket.CLOSED) {
ws.close();
if (document.getElementById('profile-name').textContent === 'Loading profile...') {
document.getElementById('profile-name').textContent = 'Profile timeout';
document.getElementById('profile-about').textContent = 'Could not load profile from relay.';
log('WARNING', 'Profile request timed out');
}
}
}, 5000);
if (events.length > 0) {
console.log('SUCCESS', 'Profile event received');
const profile = JSON.parse(events[0].content);
displayProfile(profile);
} else {
console.log('INFO', 'No profile found');
document.getElementById('profile-name').textContent = 'No profile found';
document.getElementById('profile-about').textContent = 'User has not set up a profile yet.';
}
} catch (error) {
log('ERROR', `Profile loading failed: ${error.message}`);
console.log('ERROR', `Profile loading failed: ${error.message}`);
document.getElementById('profile-name').textContent = 'Error loading profile';
document.getElementById('profile-about').textContent = error.message;
}
@@ -427,33 +176,26 @@
document.getElementById('profile-picture').src = picture;
}
log('SUCCESS', `Profile displayed: ${name}`);
console.log('SUCCESS', `Profile displayed: ${name}`);
}
// Logout function
async function logout() {
log('INFO', 'Logging out...');
console.log('INFO', 'Logging out...');
try {
await nlLite.logout();
log('SUCCESS', 'Logged out successfully');
window.NOSTR_LOGIN_LITE.logout();
console.log('SUCCESS', 'Logged out successfully');
} catch (error) {
log('ERROR', `Logout failed: ${error.message}`);
console.log('ERROR', `Logout failed: ${error.message}`);
}
}
// Set up event listeners
document.addEventListener('DOMContentLoaded', () => {
// Button event listeners
document.getElementById('launch-modal').addEventListener('click', launchLoginModal);
document.getElementById('refresh-profile').addEventListener('click', loadUserProfile);
document.getElementById('logout').addEventListener('click', logout);
// Listen for authentication events
window.addEventListener('nlAuth', handleAuthEvent);
// Initialize the app
setTimeout(initializeApp, 100);
});
</script>
</body>
</html>

View File

@@ -1,411 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔐 NOSTR_LOGIN_LITE - Full Modal Login Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: white;
}
.container {
max-width: 800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 30px;
background: linear-gradient(45deg, #fff, #007bff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin: 20px 0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.demo-section h2 {
margin-top: 0;
font-size: 24px;
margin-bottom: 15px;
}
.button {
background: linear-gradient(45deg, #007bff, #0056b3);
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
margin: 10px 5px;
text-decoration: none;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.4);
}
.button.secondary {
background: linear-gradient(45deg, #6c757d, #495057);
}
.button.secondary:hover {
box-shadow: 0 4px 15px rgba(108, 117, 125, 0.4);
}
.status {
display: inline-block;
padding: 8px 12px;
border-radius: 4px;
margin: 10px 0;
font-weight: 500;
}
.status.success { background: rgba(76, 175, 80, 0.2); color: #81c784; }
.status.error { background: rgba(244, 67, 54, 0.2); color: #ef5350; }
.status.warning { background: rgba(255, 193, 7, 0.2); color: #ffd54f; }
.status.info { background: rgba(33, 150, 243, 0.2); color: #64b5f6; }
.console-output {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.console-entry {
margin: 5px 0;
}
.console-timestamp {
color: #ccc;
margin-right: 10px;
}
.feature-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin: 20px 0;
}
.feature-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 10px;
text-align: center;
}
.feature-item .icon {
font-size: 24px;
margin-bottom: 10px;
}
.feature-item h3 {
margin: 0 0 5px 0;
font-size: 16px;
}
.feature-item p {
margin: 0;
opacity: 0.8;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 NOSTR_LOGIN_LITE Full Modal Login Demo</h1>
<div class="demo-section">
<h2>📚 Available Login Methods</h2>
<p>This demo showcases all login methods provided by NOSTR_LOGIN_LITE:</p>
<div class="feature-list">
<div class="feature-item">
<div class="icon">📱</div>
<h3>Extension Login</h3>
<p>Use browser extensions like Alby, nos2x, or other Nostr-compatible extensions</p>
</div>
<div class="feature-item">
<div class="icon">💾</div>
<h3>Local Account</h3>
<p>Create and manage local Nostr keypairs stored in browser storage</p>
</div>
<div class="feature-item">
<div class="icon">👁️</div>
<h3>Read-Only Account</h3>
<p>Access public content without authentication (limited functionality)</p>
</div>
<div class="feature-item">
<div class="icon">🔗</div>
<h3>NIP-46 Remote</h3>
<p>Connect to remote signers for secure key management</p>
</div>
<div class="feature-item">
<div class="icon">🔐</div>
<h3>OTP Backup</h3>
<p>Secure local accounts with time-based one-time passwords</p>
</div>
</div>
</div>
<!-- Library Status -->
<div class="demo-section">
<h2>⚙️ Library Status</h2>
<div id="dep-status" class="status info">Loading nostr-tools...</div>
<div id="lib-status" class="status info">Loading NOSTR_LOGIN_LITE...</div>
</div>
<!-- Modal Authentication -->
<div class="demo-section">
<h2>🎯 Launch Full Login Modal</h2>
<p>Click the button below to launch the complete authentication modal with all available login options:</p>
<button id="launch-auth" class="button">🚀 Launch Authentication Modal</button>
<button onclick="location.reload()" class="button secondary">🔄 Reload Page</button>
<div id="auth-status" class="status" style="margin-top: 15px;">Ready to authenticate...</div>
<div style="font-size: 14px; opacity: 0.8; margin-top: 10px;">
The modal will show all available login methods based on your browser setup and library configuration.
</div>
</div>
<!-- User Info Display (shown after login) -->
<div id="user-info" class="demo-section" style="display: none;">
<h2>👤 User Information</h2>
<div id="user-details">
<strong>Public Key:</strong> <span id="user-pubkey">Loading...</span><br>
<strong>Login Method:</strong> <span id="user-method">Loading...</span><br>
<strong>Account Type:</strong> <span id="user-type">Loading...</span>
</div>
</div>
<!-- Console Log -->
<div class="console-output" id="console-display">
<div class="console-entry">
<span class="console-timestamp">[Demo]</span> Modal Login Demo initialized
</div>
</div>
</div>
<!-- Load the official nostr-tools bundle first -->
<script src="../lite/nostr.bundle.js"></script>
<!-- Load NOSTR_LOGIN_LITE main library (now includes NIP-46 extension) -->
<script src="../lite/nostr-lite.js"></script>
<script>
// Console logging helper
function addConsoleEntry(message, type = 'info') {
const consoleDiv = document.getElementById('console-display');
const entry = document.createElement('div');
entry.className = 'console-entry';
const timestamp = new Date().toLocaleTimeString();
const prefix = type === 'error' ? '[ERROR]' :
type === 'success' ? '[SUCCESS]' :
type === 'warning' ? '[WARNING]' : '[INFO]';
entry.innerHTML = `<span class="console-timestamp">[${timestamp}] ${prefix}</span> ${message}`;
consoleDiv.appendChild(entry);
consoleDiv.scrollTop = consoleDiv.scrollHeight;
}
// Global state
let authInitialized = false;
// Event listeners for authentication events
window.addEventListener('nlAuth', (event) => {
addConsoleEntry(`Authentication event: ${event.detail.type}`, 'success');
if (event.detail.pubkey) {
addConsoleEntry(`User authenticated: ${event.detail.pubkey}`, 'success');
displayUserInfo(event.detail);
}
});
window.addEventListener('nlLogout', (event) => {
addConsoleEntry('User logged out', 'warning');
hideUserInfo();
});
window.addEventListener('nlAuthUrl', (event) => {
addConsoleEntry(`Auth URL generated: ${event.detail.url}`, 'info');
});
window.addEventListener('nlError', (event) => {
addConsoleEntry(`Authentication error: ${event.detail.message}`, 'error');
});
// Library load checking with retry
function checkLibraryLoaded() {
let attempts = 0;
const maxAttempts = 50; // 5 seconds
const check = () => {
if (window.NostrTools) {
document.getElementById('dep-status').textContent = '✓ nostr-tools loaded successfully!';
document.getElementById('dep-status').className = 'status success';
}
if (window.NOSTR_LOGIN_LITE) {
document.getElementById('lib-status').textContent = '✓ NOSTR_LOGIN_LITE loaded successfully!';
document.getElementById('lib-status').className = 'status success';
enableModalLaunch();
} else {
attempts++;
if (attempts < maxAttempts) {
setTimeout(check, 100);
} else {
document.getElementById('lib-status').textContent = '✗ Failed to load NOSTR_LOGIN_LITE';
document.getElementById('lib-status').className = 'status error';
addConsoleEntry('Bundle might have JavaScript errors - check browser console', 'error');
}
}
};
check();
}
// Enable the modal launch button
function enableModalLaunch() {
const launchBtn = document.getElementById('launch-auth');
launchBtn.disabled = false;
launchBtn.textContent = '🚀 Launch Authentication Modal';
addConsoleEntry('Full modal authentication ready', 'success');
}
// Launch authentication modal
async function launchAuthModal() {
const launchBtn = document.getElementById('launch-auth');
const status = document.getElementById('auth-status');
try {
status.textContent = '🔄 Initializing authentication...';
status.className = 'status warning';
launchBtn.disabled = true;
// Initialize NOSTR_LOGIN_LITE with all methods enabled
const options = {
theme: 'dark',
darkMode: false,
relays: ['wss://relay.damus.io', 'wss://relay.nostr.band', 'wss://nos.lol'],
methods: {
extension: true,
local: true,
readonly: true,
remote: true,
otp: true
},
debug: true
};
addConsoleEntry('Initializing NOSTR_LOGIN_LITE with full configuration', 'info');
if (!authInitialized) {
await window.NOSTR_LOGIN_LITE.init(options);
authInitialized = true;
}
addConsoleEntry('Launching full authentication modal', 'info');
status.textContent = '🎨 Opening authentication modal...';
// Launch the modal - this will show all available methods
window.NOSTR_LOGIN_LITE.launch('login');
status.textContent = '✅ Authentication modal launched!';
status.className = 'status success';
addConsoleEntry('Modal launched successfully - all login methods available', 'success');
// Re-enable button after a delay
setTimeout(() => {
launchBtn.disabled = false;
launchBtn.textContent = '🔄 Launch Again';
status.textContent = 'Ready to launch modal again...';
status.className = 'status info';
}, 3000);
} catch (error) {
addConsoleEntry(`Modal launch failed: ${error.message}`, 'error');
status.textContent = '❌ Failed to launch modal';
status.className = 'status error';
launchBtn.disabled = false;
launchBtn.textContent = '🚀 Try Again';
}
}
// Display user information after successful authentication
function displayUserInfo(details) {
document.getElementById('user-info').style.display = 'block';
document.getElementById('user-pubkey').textContent = details.pubkey || 'Unknown';
document.getElementById('user-method').textContent = details.method || 'Unknown';
document.getElementById('user-type').textContent = getAccountType(details.method);
const status = document.getElementById('auth-status');
status.textContent = '✅ Successfully authenticated!';
status.className = 'status success';
}
// Hide user info on logout
function hideUserInfo() {
document.getElementById('user-info').style.display = 'none';
const status = document.getElementById('auth-status');
status.textContent = '👋 User logged out';
status.className = 'status warning';
}
// Helper function to get readable account type
function getAccountType(method) {
const types = {
extension: 'Browser Extension',
local: 'Local Account',
readonly: 'Read-Only Account',
remote: 'NIP-46 Remote',
otp: 'OTP Secured Local'
};
return types[method] || 'Unknown';
}
// Initialize everything when DOM loads
document.addEventListener('DOMContentLoaded', () => {
addConsoleEntry('Demo page loaded, initializing libraries...', 'info');
// Check if libraries are loaded
checkLibraryLoaded();
// Set up the modal launch button
document.getElementById('launch-auth').addEventListener('click', launchAuthModal);
});
// Debug logging
console.log('NOSTR_LOGIN_LITE Modal Demo loaded');
console.log('Available login methods will be shown in modal');
</script>
</body>
</html>

75
examples/modal.html Normal file
View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedded NOSTR_LOGIN_LITE</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 40px;
background: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 90vh;
}
.container {
max-width: 400px;
width: 100%;
}
#login-container {
/* No styling - let embedded modal blend seamlessly */
}
</style>
</head>
<body>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,
seedphrase:true,
readonly: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
style: 'square', // 'pill', 'square', 'circle', 'minimal'
// icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
text: 'Login'
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
},
animation: {
slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down'
}
}});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,534 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Isolation Test - NOSTR LOGIN LITE</title>
<style>
body {
font-family: 'Courier New', monospace;
margin: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status-panel {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.info-box {
background: #e7f3ff;
border: 2px solid #0066cc;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.isolated-notice {
background: #f8d7da;
border: 2px solid #dc3545;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
button {
background: white;
color: black;
border: 2px solid black;
border-radius: 8px;
padding: 10px 20px;
margin: 5px;
cursor: pointer;
font-family: 'Courier New', monospace;
font-weight: bold;
}
button:hover {
border-color: red;
}
button:active {
background: red;
color: white;
}
.mode-indicator {
font-weight: bold;
padding: 8px 16px;
border-radius: 4px;
border: 2px solid #dc3545;
background: #f8d7da;
display: inline-block;
margin: 10px 0;
}
pre {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 10px;
overflow-x: auto;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Session Isolation Test</h1>
<div class="info-box">
<h3>📋 Test Instructions</h3>
<ol>
<li><strong>Isolated Session:</strong> Each tab/window has independent authentication</li>
<li>Login in this tab/window - it will persist on refresh</li>
<li>Open new windows/tabs - they will start unauthenticated</li>
<li>Login with different users in different windows simultaneously</li>
<li>Refresh any window - authentication persists within that window only</li>
</ol>
</div>
<div class="mode-indicator">
🔒 ISOLATED MODE (sessionStorage)
</div>
<div class="isolated-notice">
<strong>🚨 Session Isolation Active:</strong>
<p>This tab uses sessionStorage - authentication is isolated to this window only. Refreshing will maintain your login state, but other tabs/windows are independent.</p>
</div>
<div class="status-panel">
<h3>Authentication Status</h3>
<div id="auth-status">Not authenticated</div>
<div id="auth-details"></div>
</div>
<div>
<h3>Actions</h3>
<button onclick="login()">Login</button>
<button onclick="logout()">Logout</button>
<button onclick="checkStatus()">Check Status</button>
<button onclick="testSigning()">Test Signing</button>
<button onclick="openNewWindow()">Open New Window</button>
<button onclick="debugAuthentication()" style="border-color: orange;">Debug Auth State</button>
</div>
<div>
<h3>Storage Inspector</h3>
<button onclick="inspectStorage()">Inspect SessionStorage</button>
<button onclick="clearStorage()">Clear Session Storage</button>
<div id="storage-content"></div>
</div>
<div>
<h3>Test Results</h3>
<div id="results"></div>
</div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
let nostrLiteInstance = null;
// Initialize in isolated mode (always)
initializeIsolatedMode();
async function initializeIsolatedMode() {
try {
console.log('Initializing NOSTR_LOGIN_LITE in ISOLATED mode...');
nostrLiteInstance = await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
persistence: true,
isolateSession: true, // Always isolated - each tab/window independent
methods: {
extension: true,
local: true,
readonly: true,
connect: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 0.95,
vPosition: 0.1,
appearance: {
style: 'pill',
icon: '🔒',
text: 'ISOLATED',
iconOnly: false
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
}
}
});
checkStatus();
console.log('NOSTR_LOGIN_LITE initialized successfully in ISOLATED mode');
console.log('Authentication will persist on refresh within this tab only');
} catch (error) {
console.error('Failed to initialize NOSTR_LOGIN_LITE:', error);
document.getElementById('results').innerHTML =
`<div style="color: red;">Initialization Error: ${error.message}</div>`;
}
}
function login() {
window.NOSTR_LOGIN_LITE.launch('login');
}
function logout() {
window.NOSTR_LOGIN_LITE.logout();
setTimeout(checkStatus, 100);
}
function debugAuthentication() {
console.log('=== AUTHENTICATION DEBUG ===');
// Check global storage-based authentication state (SINGLE SOURCE OF TRUTH)
const authState = window.NOSTR_LOGIN_LITE.getAuthState();
console.log('🔍 GLOBAL getAuthState():', authState);
console.log('🔍 Derived isAuthenticated():', !!authState);
console.log('🔍 Derived getUserInfo():', authState);
// Check window.nostr (should sync with global state)
console.log('window.nostr exists:', !!window.nostr);
console.log('window.nostr constructor:', window.nostr?.constructor?.name);
console.log('window.nostr.authState (getter):', window.nostr?.authState);
// Check NOSTR_LOGIN_LITE instance
const instance = window.NOSTR_LOGIN_LITE?._instance;
console.log('NOSTR_LOGIN_LITE instance exists:', !!instance);
console.log('Instance hasExtension:', instance?.hasExtension);
console.log('Instance facadeInstalled:', instance?.facadeInstalled);
// Check floating tab state (now queries global getAuthState() only)
const floatingTab = instance?.floatingTab;
console.log('FloatingTab exists:', !!floatingTab);
if (floatingTab) {
const tabAuthState = floatingTab._getAuthState();
console.log('FloatingTab _getAuthState():', tabAuthState);
console.log('FloatingTab derived authenticated:', !!tabAuthState);
}
// Check session storage directly
const sessionKeys = [];
const storageKey = 'nostr_login_lite_auth';
const sessionAuthData = sessionStorage.getItem(storageKey);
const localAuthData = localStorage.getItem(storageKey);
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('nl_')) {
const value = sessionStorage.getItem(key);
sessionKeys.push({ key, valueLength: value?.length || 0, hasValue: !!value });
}
}
console.log('SessionStorage nl_ keys:', sessionKeys);
console.log('SessionStorage auth data:', !!sessionAuthData);
console.log('LocalStorage auth data:', !!localAuthData);
// Display debug results
let debugHTML = '<h4>🔍 Storage-Based Authentication Debug</h4>';
debugHTML += '<div style="font-family: monospace; font-size: 12px; background: #f8f9fa; padding: 10px; border-radius: 4px;">';
debugHTML += `<strong>🎯 GLOBAL getAuthState():</strong> ${!!authState} ${authState ? `(${authState.method})` : ''}<br>`;
debugHTML += `<strong>🎯 Derived isAuthenticated():</strong> ${!!authState}<br>`;
debugHTML += `<strong>🎯 Derived getUserInfo():</strong> ${!!authState}<br>`;
debugHTML += `<strong>window.nostr exists:</strong> ${!!window.nostr} (${window.nostr?.constructor?.name})<br>`;
debugHTML += `<strong>window.nostr.authState (getter):</strong> ${!!window.nostr?.authState}<br>`;
debugHTML += `<strong>FloatingTab queries getAuthState():</strong> ${!!floatingTab?._getAuthState()}<br>`;
debugHTML += `<strong>SessionStorage 'nostr_login_lite_auth':</strong> ${!!sessionAuthData}<br>`;
debugHTML += `<strong>LocalStorage 'nostr_login_lite_auth':</strong> ${!!localAuthData}<br>`;
debugHTML += `<strong>Session storage nl_ keys:</strong> ${sessionKeys.length}<br>`;
debugHTML += `<strong>Instance hasExtension:</strong> ${instance?.hasExtension}<br>`;
debugHTML += `<strong>Facade installed:</strong> ${instance?.facadeInstalled}<br>`;
debugHTML += '</div>';
debugHTML += '<p><strong>Check the browser console for detailed debug output.</strong></p>';
debugHTML += '<p><strong>NEW Architecture:</strong> Global functions query localStorage/sessionStorage directly as single source of truth</p>';
// Check for consistency issues
const derivedAuth = !!authState;
const floatingTabAuth = !!floatingTab?._getAuthState();
if (floatingTabAuth !== derivedAuth) {
debugHTML += '<p style="color: red;"><strong>🚨 MISMATCH DETECTED:</strong> FloatingTab and global getAuthState() disagree!</p>';
debugHTML += '<p>Both should query the same storage - check implementation.</p>';
} else if (sessionAuthData && !derivedAuth) {
debugHTML += '<p style="color: orange;"><strong>⚠️ PARSING ISSUE:</strong> Session data exists but getAuthState() returns null!</p>';
debugHTML += '<p>Check getAuthState() function - it may not be parsing the stored data correctly.</p>';
} else if (!sessionAuthData && !localAuthData && derivedAuth) {
debugHTML += '<p style="color: orange;"><strong>⚠️ STORAGE ISSUE:</strong> No storage data but getAuthState() returns data!</p>';
debugHTML += '<p>getAuthState() may be reading from unexpected sources.</p>';
}
document.getElementById('results').innerHTML = debugHTML;
}
async function checkStatus() {
try {
console.log('🔍 Checking authentication status using GLOBAL functions...');
// Use the single global storage-based authentication state function
const authState = window.NOSTR_LOGIN_LITE.getAuthState();
console.log('🔍 GLOBAL getAuthState():', authState);
console.log('🔍 Derived isAuthenticated():', !!authState);
console.log('🔍 Derived getUserInfo():', authState);
console.log('🔍 window.nostr:', !!window.nostr);
console.log('🔍 window.nostr.constructor:', window.nostr?.constructor?.name);
// Check storage directly for debugging
const storageKey = 'nostr_login_lite_auth';
const sessionAuthData = sessionStorage.getItem(storageKey);
const localAuthData = localStorage.getItem(storageKey);
console.log('🔍 sessionStorage auth data:', !!sessionAuthData);
console.log('🔍 localStorage auth data:', !!localAuthData);
if (authState) {
let pubkey = null;
try {
if (window.nostr) {
pubkey = await window.nostr.getPublicKey();
} else if (authState.pubkey) {
pubkey = authState.pubkey;
}
} catch (err) {
console.warn('Could not get pubkey:', err.message);
pubkey = authState.pubkey;
}
const method = authState.method;
console.log('✅ Authentication detected via GLOBAL functions - method:', method, 'pubkey:', pubkey?.slice(0, 8) + '...');
document.getElementById('auth-status').innerHTML =
`<strong style="color: green;">✅ Authenticated (Session Isolated)</strong>`;
document.getElementById('auth-details').innerHTML =
`<strong>Method:</strong> ${method}<br>
<strong>Public Key:</strong> ${pubkey ? `${pubkey.slice(0, 16)}...${pubkey.slice(-8)}` : 'Available in authState'}<br>
<strong>Storage:</strong> ${sessionAuthData ? 'sessionStorage' : 'localStorage'} (${sessionAuthData ? 'isolated to this tab' : 'shared across tabs'})<br>
<strong>Persistence:</strong> Survives refresh${sessionAuthData ? ', isolated from other tabs' : ', shared with other tabs'}<br>
<strong>Debug:</strong> Global getAuthState() returns valid data`;
} else if (sessionAuthData || localAuthData) {
// We have storage data but getAuthState() returns null
console.log('⚠️ Storage data exists but getAuthState() returns null');
document.getElementById('auth-status').innerHTML =
`<strong style="color: orange;">⚠️ Authentication data found but not parsed</strong>`;
document.getElementById('auth-details').innerHTML =
`<strong>Storage:</strong> ${sessionAuthData ? 'sessionStorage' : 'localStorage'} has authentication data<br>
<strong>Issue:</strong> getAuthState() returns null<br>
<strong>Debug:</strong> Storage data: session=${!!sessionAuthData}, local=${!!localAuthData}<br>
<strong>Solution:</strong> Check getAuthState() function implementation`;
} else {
console.log('❌ No authentication detected via getAuthState()');
document.getElementById('auth-status').innerHTML =
`<strong style="color: red;">❌ Not authenticated</strong>`;
document.getElementById('auth-details').innerHTML =
`<strong>Storage:</strong> sessionStorage (isolated to this tab)<br>
<strong>Status:</strong> Ready for login - will persist on refresh<br>
<strong>Debug:</strong> getAuthState() returns no authentication data`;
}
} catch (error) {
console.error('❌ Error checking status:', error);
document.getElementById('auth-status').innerHTML =
`<strong style="color: orange;">⚠️ Error checking status</strong>`;
document.getElementById('auth-details').innerHTML =
`Error: ${error.message}<br>
<strong>Debug:</strong> Check browser console for details`;
}
}
async function testSigning() {
try {
// Use global authentication state to check if authenticated
const authState = window.NOSTR_LOGIN_LITE.getAuthState();
if (!authState) {
throw new Error('Not authenticated (checked via global getAuthState())');
}
if (!window.nostr) {
throw new Error('window.nostr not available for signing');
}
const event = {
kind: 1,
content: `Test message from ISOLATED session - ${new Date().toISOString()}`,
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
const signedEvent = await window.nostr.signEvent(event);
document.getElementById('results').innerHTML =
`<h4>✅ Signing Test Successful (Session Isolated)</h4>
<p>This signature was created using the storage-based authentication system.</p>
<p><strong>Authentication Method:</strong> getAuthState() confirmed authentication before signing</p>
<pre>${JSON.stringify(signedEvent, null, 2)}</pre>`;
} catch (error) {
document.getElementById('results').innerHTML =
`<h4>❌ Signing Test Failed</h4>
<p style="color: red;">${error.message}</p>
<p><strong>Debug Info:</strong></p>
<ul>
<li>getAuthState(): ${!!window.NOSTR_LOGIN_LITE.getAuthState()}</li>
<li>window.nostr exists: ${!!window.nostr}</li>
<li>Auth method: ${JSON.stringify(window.NOSTR_LOGIN_LITE.getAuthState()?.method || null)}</li>
</ul>`;
}
}
function openNewWindow() {
const newWindow = window.open(
window.location.href,
'_blank',
'width=900,height=700,scrollbars=yes,resizable=yes'
);
if (newWindow) {
document.getElementById('results').innerHTML =
`<h4>🪟 New Window Opened - Independent Session</h4>
<p><strong>Session Isolation Test:</strong></p>
<ol>
<li>The new window starts unauthenticated (independent session)</li>
<li>Login in the new window with a different method or user</li>
<li>Both windows maintain separate authentication states</li>
<li>Refresh either window - authentication persists within that window only</li>
<li>Close a window - its authentication is lost (sessionStorage cleared)</li>
</ol>
<p><strong>Expected Behavior:</strong> Each window/tab has completely independent authentication that persists on refresh but doesn't leak to other windows.</p>`;
} else {
document.getElementById('results').innerHTML =
`<h4>❌ Failed to Open Window</h4>
<p>Please allow popups and try again</p>`;
}
}
function inspectStorage() {
const sessionStorage_keys = [];
// Inspect sessionStorage (our isolated storage)
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('nl_')) {
sessionStorage_keys.push({
key,
value: sessionStorage.getItem(key)
});
}
}
let content = '<h4>📊 Session Storage Inspection</h4>';
content += '<p><strong>Note:</strong> This tab uses sessionStorage for isolation - data here is independent of other tabs/windows.</p>';
content += '<h5>sessionStorage (This tab only):</h5>';
if (sessionStorage_keys.length === 0) {
content += '<p style="color: #666;">No authentication data found in this session</p>';
} else {
content += '<p style="color: green;">✅ Authentication data found (persists on refresh)</p>';
content += '<pre>' + JSON.stringify(sessionStorage_keys, null, 2) + '</pre>';
}
// Show what would be in localStorage if we weren't using isolation
const localStorage_keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('nl_')) {
localStorage_keys.push(key);
}
}
content += '<h5>localStorage (Not used in isolated mode):</h5>';
if (localStorage_keys.length === 0) {
content += '<p style="color: #666;">No NOSTR_LOGIN_LITE data (expected in isolated mode)</p>';
} else {
content += '<p style="color: orange;">⚠️ Found some data - might be from non-isolated sessions</p>';
}
document.getElementById('storage-content').innerHTML = content;
}
function clearStorage() {
// Clear only sessionStorage (our isolated storage)
const sessionKeys = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('nl_')) {
sessionKeys.push(key);
}
}
sessionKeys.forEach(key => sessionStorage.removeItem(key));
document.getElementById('storage-content').innerHTML =
`<h4>🧹 Session Storage Cleared</h4>
<p>Removed ${sessionKeys.length} authentication items from this tab's sessionStorage</p>
<p><strong>Result:</strong> This tab is now logged out, but other tabs are unaffected</p>`;
// Update status
setTimeout(checkStatus, 100);
}
// Listen for authentication events
window.addEventListener('nlMethodSelected', (event) => {
console.log('Authentication successful in isolated session:', event.detail);
setTimeout(checkStatus, 100);
document.getElementById('results').innerHTML =
`<h4>✅ Authentication Successful (Session Isolated)</h4>
<p><strong>Method:</strong> ${event.detail.method}</p>
<p><strong>Storage:</strong> sessionStorage (isolated to this tab)</p>
<p><strong>Persistence:</strong> Will survive refresh, won't affect other tabs</p>
<p><strong>Test:</strong> Open a new tab - it should start unauthenticated</p>`;
});
window.addEventListener('nlLogout', (event) => {
console.log('Logout detected in isolated session:', event.detail);
setTimeout(checkStatus, 100);
document.getElementById('results').innerHTML =
`<h4>👋 Logged Out (Session Isolated)</h4>
<p>Authentication cleared from this tab's sessionStorage only</p>
<p><strong>Result:</strong> Other tabs remain unaffected by this logout</p>`;
});
// Check status on page load (should restore from sessionStorage if available)
window.addEventListener('load', () => {
setTimeout(checkStatus, 500);
// Show persistence message if we're restoring authentication
if (sessionStorage.getItem('nl_auth_state') || sessionStorage.getItem('nl_current')) {
setTimeout(() => {
document.getElementById('results').innerHTML =
`<h4>🔄 Session Restored</h4>
<p>Authentication state restored from sessionStorage on page load</p>
<p><strong>Isolation confirmed:</strong> This tab's login state is independent</p>`;
}, 1000);
}
});
</script>
</body>
</html>

184
examples/sign.html Normal file
View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIP-07 Signing Test</title>
</head>
<body>
<div>
<div id="status"></div>
<div id="test-section" style="display:none;">
<button id="sign-button">Sign Event</button>
<button id="encrypt-button">Test NIP-04 Encrypt</button>
<button id="decrypt-button">Test NIP-04 Decrypt</button>
<div id="results"></div>
</div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
let testPubkey = 'npub1damus9dqe7g7jqn45kjcjgsv0vxjqnk8cxjkf8gqjwm8t8qjm7cqm3z7l';
let ciphertext = '';
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,
readonly: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
icon: '', // Clean display without icon placeholders
text: 'Login'
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
},
getUserInfo: true, // Enable profile fetching
getUserRelay: [ // Specific relays for profile fetching
'wss://relay.laantungir.net'
]
}});
// document.getElementById('login-button').addEventListener('click', () => {
// window.NOSTR_LOGIN_LITE.launch('login');
// });
window.addEventListener('nlMethodSelected', (event) => {
document.getElementById('status').textContent = `Authenticated with: ${event.detail.method}`;
document.getElementById('test-section').style.display = 'block';
});
document.getElementById('sign-button').addEventListener('click', testSigning);
document.getElementById('encrypt-button').addEventListener('click', testEncryption);
document.getElementById('decrypt-button').addEventListener('click', testDecryption);
});
async function testSigning() {
try {
console.log('=== DEBUGGING SIGN EVENT START ===');
console.log('testSigning: window.nostr is:', window.nostr);
console.log('testSigning: window.nostr constructor:', window.nostr?.constructor?.name);
console.log('testSigning: window.nostr === our facade?', window.nostr?.constructor?.name === 'WindowNostr');
// Get user public key for comparison
const userPubkey = await window.nostr.getPublicKey();
console.log('User public key:', userPubkey);
// Check auth state if our facade
if (window.nostr?.constructor?.name === 'WindowNostr') {
console.log('WindowNostr authState:', window.nostr.authState);
console.log('WindowNostr authenticatedExtension:', window.nostr.authenticatedExtension);
console.log('WindowNostr existingNostr:', window.nostr.existingNostr);
}
const event = {
kind: 1,
content: 'Hello from NIP-07!',
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
console.log('=== EVENT BEING SENT TO EXTENSION ===');
console.log('Event object:', JSON.stringify(event, null, 2));
console.log('Event keys:', Object.keys(event));
console.log('Event kind type:', typeof event.kind, event.kind);
console.log('Event content type:', typeof event.content, event.content);
console.log('Event tags type:', typeof event.tags, event.tags);
console.log('Event created_at type:', typeof event.created_at, event.created_at);
console.log('Event created_at value:', event.created_at);
// Check if created_at is within reasonable bounds
const now = Math.floor(Date.now() / 1000);
const timeDiff = Math.abs(event.created_at - now);
console.log('Time difference from now (seconds):', timeDiff);
console.log('Event timestamp as Date:', new Date(event.created_at * 1000));
// Additional debugging for user-specific issues
console.log('=== USER-SPECIFIC DEBUG INFO ===');
console.log('User pubkey length:', userPubkey?.length);
console.log('User pubkey format check (hex):', /^[a-fA-F0-9]{64}$/.test(userPubkey));
// Try to get user profile info if available
try {
const profileEvent = {
kinds: [0],
authors: [userPubkey],
limit: 1
};
console.log('Would query profile with filter:', profileEvent);
} catch (profileErr) {
console.log('Profile query setup failed:', profileErr);
}
console.log('=== ABOUT TO CALL EXTENSION SIGN EVENT ===');
const signedEvent = await window.nostr.signEvent(event);
console.log('=== SIGN EVENT SUCCESSFUL ===');
console.log('Signed event:', JSON.stringify(signedEvent, null, 2));
console.log('Signed event keys:', Object.keys(signedEvent));
console.log('Signature present:', !!signedEvent.sig);
console.log('ID present:', !!signedEvent.id);
console.log('Pubkey matches user:', signedEvent.pubkey === userPubkey);
document.getElementById('results').innerHTML = `<h3>Signed Event:</h3><pre>${JSON.stringify(signedEvent, null, 2)}</pre>`;
console.log('=== DEBUGGING SIGN EVENT END ===');
} catch (error) {
console.error('=== SIGN EVENT ERROR ===');
console.error('Error message:', error.message);
console.error('Error stack:', error.stack);
console.error('Error object:', error);
document.getElementById('results').innerHTML = `<h3>Sign Error:</h3><pre>${error.message}</pre><pre>${error.stack}</pre>`;
}
}
async function testEncryption() {
try {
const plaintext = 'Secret message for testing';
const pubkey = await window.nostr.getPublicKey();
ciphertext = await window.nostr.nip04.encrypt(pubkey, plaintext);
document.getElementById('results').innerHTML = `<h3>Encrypted:</h3><pre>${ciphertext}</pre>`;
} catch (error) {
document.getElementById('results').innerHTML = `<h3>Encrypt Error:</h3><pre>${error.message}</pre>`;
}
}
async function testDecryption() {
try {
if (!ciphertext) {
document.getElementById('results').innerHTML = `<h3>Decrypt Error:</h3><pre>No ciphertext available. Run encrypt first.</pre>`;
return;
}
const pubkey = await window.nostr.getPublicKey();
const decrypted = await window.nostr.nip04.decrypt(pubkey, ciphertext);
document.getElementById('results').innerHTML = `<h3>Decrypted:</h3><pre>${decrypted}</pre>`;
} catch (error) {
document.getElementById('results').innerHTML = `<h3>Decrypt Error:</h3><pre>${error.message}</pre>`;
}
}
</script>
</body>
</html>

View File

@@ -205,22 +205,44 @@ The following features are planned but not yet implemented:
## Development
To work on the source files:
⚠️ **CRITICAL: DO NOT EDIT `nostr-lite.js` DIRECTLY!**
The `nostr-lite.js` file is **auto-generated** by the build script. All changes must be made in the build script itself.
### Build Process
```bash
# Edit individual components
lite/core/nip46-client.js
lite/ui/modal.js
lite/nostr-login-lite.js
# The main library source code is in:
lite/build.js # ← Edit this file for library changes
# Run bundler to create distribution
node lite/bundler.js
# To make changes:
1. Edit lite/build.js # Contains all source code
2. cd lite && node build.js # Regenerates nostr-lite.js
3. Test your changes in examples/
# Start dev server (from project root)
# NEVER edit these files directly (they get overwritten):
lite/nostr-lite.js # ← Auto-generated, don't edit!
# Separate components that can be edited:
lite/ui/modal.js # Modal UI component
themes/default/theme.css # Default theme
themes/dark/theme.css # Dark theme
```
### Development Workflow
```bash
# 1. Make changes to source
nano lite/build.js
# 2. Rebuild bundle
cd lite && node build.js
# 3. Start dev server (from project root)
python3 -m http.server 8000
# Open test page
open http://localhost:8000/examples/simple-demo.html
# 4. Test changes
open http://localhost:8000/examples/modal.html
```
### Local Bundle Setup

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

413
login_logic.md Normal file
View File

@@ -0,0 +1,413 @@
# NOSTR_LOGIN_LITE - Login Logic Analysis
This document explains the complete login and authentication flow for the NOSTR_LOGIN_LITE library, including how state is maintained upon page refresh.
## System Architecture Overview
The library uses a **modular authentication architecture** with these key components:
1. **FloatingTab** - UI component for login trigger and status display
2. **Modal** - UI component for authentication method selection
3. **NostrLite** - Main library coordinator and facade manager
4. **WindowNostr** - NIP-07 compliant facade for non-extension methods
5. **AuthManager** - Persistent state management with encryption
6. **Extension Bridge** - Browser extension detection and management
## Authentication Flow Diagrams
### Initial Page Load Flow
```
┌─────────────────────┐
│ Page Loads │
└─────────┬───────────┘
┌─────────────────────┐
│ NOSTR_LOGIN_LITE │
│ .init() called │
└─────────┬───────────┘
┌─────────────────────┐ YES ┌─────────────────────┐
│ Real extension │──────────▶│ Extension-First │
│ detected? │ │ Mode: Don't install │
└─────────┬───────────┘ │ facade │
│ NO └─────────────────────┘
┌─────────────────────┐
│ Install WindowNostr │
│ facade for local/ │
│ NIP-46/readonly │
└─────────┬───────────┘
┌─────────────────────┐ YES ┌─────────────────────┐
│ Persistence │──────────▶│ _attemptAuthRestore │
│ enabled? │ │ called │
└─────────┬───────────┘ └─────────┬───────────┘
│ NO │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Initialization │ │ Check storage for │
│ complete │ │ saved auth state │
└─────────────────────┘ └─────────┬───────────┘
┌─────────────────────┐ YES
│ Valid auth state │────────┐
│ found? │ │
└─────────┬───────────┘ │
│ NO │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Show login UI │ │ Restore auth & │
│ (FloatingTab,etc) │ │ dispatch events │
└─────────────────────┘ └─────────────────────┘
```
### User-Initiated Login Flow
```
┌─────────────────────┐ ┌─────────────────────┐
│ User clicks │ │ User clicks │
│ FloatingTab │ │ Login Button │
└─────────┬───────────┘ └─────────┬───────────┘
│ │
▼ ▼
┌─────────────────────┐ │
│ Extension │ │
│ available? │ │
└─────────┬───────────┘ │
│ YES │
▼ │
┌─────────────────────┐ │
│ Auto-try extension │ │
│ authentication │ │
└─────────┬───────────┘ │
│ SUCCESS │
▼ │
┌─────────────────────┐ │
│ Authentication │ │
│ complete │◀──────────────────┘
└─────────────────────┘ │ FAIL OR ALWAYS
┌─────────────────────┐
│ Open Modal with │
│ method selection: │
│ • Extension │
│ • Local Key │
│ • NIP-46 Connect │
│ • Read-only │
│ • OTP/DM │
└─────────┬───────────┘
┌─────────────────────┐
│ User selects method │
│ and completes auth │
└─────────┬───────────┘
┌─────────────────────┐
│ Authentication │
│ complete │
└─────────────────────┘
```
### Authentication Storage & Persistence Flow
```
┌─────────────────────┐
│ Authentication │
│ successful │
└─────────┬───────────┘
┌─────────────────────┐
│ nlMethodSelected │
│ event dispatched │
└─────────┬───────────┘
┌─────────────────────┐ Extension? ┌─────────────────────┐
│ AuthManager. │─────────────────▶│ Store verification │
│ saveAuthState() │ │ data only (no │
└─────────┬───────────┘ │ secrets) │
│ Local Key? └─────────────────────┘
┌─────────────────────┐
│ Encrypt secret key │
│ with session │
│ password + AES-GCM │
└─────────┬───────────┘
│ NIP-46?
┌─────────────────────┐
│ Store connection │
│ parameters (no │
│ secrets) │
└─────────┬───────────┘
│ Read-only?
┌─────────────────────┐
│ Store method only │
│ (no secrets) │
└─────────┬───────────┘
┌─────────────────────┐ isolateSession? ┌─────────────────────┐
│ Choose storage: │─────────YES─────────▶│ sessionStorage │
│ localStorage vs │ │ (per-window) │
│ sessionStorage │◀────────NO───────────┤ │
└─────────┬───────────┘ └─────────────────────┘
┌─────────────────────┐
│ localStorage │
│ (cross-window) │
└─────────────────────┘
```
## Key Decision Points and Logic
### 1. Extension Detection Logic (Line 994-1046)
**Function:** `NostrLite._isRealExtension(obj)`
```javascript
// Conservative extension detection
if (!obj || typeof obj !== 'object') return false;
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') return false;
// Exclude our own classes
const constructorName = obj.constructor?.name;
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') return false;
if (obj === window.NostrTools) return false;
// Look for extension indicators
const extensionIndicators = [
'_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope',
'_requests', '_pubkey', 'name', 'version', 'description'
];
const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop));
const hasExtensionConstructor = constructorName &&
constructorName !== 'Object' &&
constructorName !== 'Function';
return hasIndicators || hasExtensionConstructor;
```
### 2. Facade Installation Decision (Line 942-972)
**Function:** `NostrLite._setupWindowNostrFacade()`
```
Extension detected? ──YES──▶ DON'T install facade
Store reference for persistence
NO
Install WindowNostr facade ──▶ Handle local/NIP-46/readonly methods
```
### 3. FloatingTab Click Behavior (Line 351-369)
**Current UX Inconsistency Issue:**
```javascript
async _handleClick() {
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
this._showUserMenu(); // Show user options
} else {
// INCONSISTENCY: Auto-tries extension instead of opening modal
if (window.nostr && this._isRealExtension(window.nostr)) {
await this._tryExtensionLogin(window.nostr); // Automatic extension attempt
} else {
if (this.modal) {
this.modal.open({ startScreen: 'login' }); // Fallback to modal
}
}
}
}
```
**Comparison with Login Button behavior:**
- Login Button: **Always** opens modal for user choice
- FloatingTab: **Auto-tries extension first**, only shows modal if denied
### 4. Authentication Restoration on Page Refresh
**Two-Path System:**
#### Path 1: Extension Mode (Line 1115-1173)
```javascript
async _attemptExtensionRestore() {
const authManager = new AuthManager({ isolateSession: this.options?.isolateSession });
const storedAuth = await authManager.restoreAuthState();
if (!storedAuth || storedAuth.method !== 'extension') return null;
// Verify extension still works with same pubkey
if (!window.nostr || !this._isRealExtension(window.nostr)) return null;
const currentPubkey = await window.nostr.getPublicKey();
if (currentPubkey !== storedAuth.pubkey) return null;
// Dispatch nlAuthRestored event for UI updates
window.dispatchEvent(new CustomEvent('nlAuthRestored', { detail: extensionAuth }));
}
```
#### Path 2: Non-Extension Mode (Line 1080-1098)
```javascript
// Uses facade's restoreAuthState method
if (this.facadeInstalled && window.nostr?.restoreAuthState) {
const restoredAuth = await window.nostr.restoreAuthState();
if (restoredAuth) {
// Handle NIP-46 reconnection if needed
if (restoredAuth.requiresReconnection) {
this._showReconnectionPrompt(restoredAuth);
}
}
}
```
### 5. Storage Strategy (Line 1408-1414)
**Storage Type Selection:**
```javascript
if (options.isolateSession) {
this.storage = sessionStorage; // Per-window isolation
} else {
this.storage = localStorage; // Cross-window persistence
}
```
### 6. Event-Driven State Synchronization
**Key Events:**
- `nlMethodSelected` - Dispatched when user completes authentication
- `nlAuthRestored` - Dispatched when authentication is restored from storage
- `nlLogout` - Dispatched when user logs out
- `nlReconnectionRequired` - Dispatched when NIP-46 needs reconnection
**Event Listeners:**
- FloatingTab listens to all auth events for UI updates (Line 271-295)
- WindowNostr listens to nlMethodSelected/nlLogout for state management (Line 823-869)
## State Persistence Security Model
### By Authentication Method:
**Extension:**
- ✅ Store: pubkey, verification metadata
- ❌ Never store: extension object, secrets
- 🔒 Security: Minimal data, 1-hour expiry
**Local Key:**
- ✅ Store: encrypted secret key, pubkey
- 🔒 Security: AES-GCM encryption with session-specific password
- 🔑 Session password stored in sessionStorage (cleared on tab close)
**NIP-46:**
- ✅ Store: connection parameters, pubkey
- ❌ Never store: session secrets
- 🔄 Requires: User reconnection on restore
**Read-only:**
- ✅ Store: method type, pubkey
- ❌ No secrets to store
## Current Issues Identified
### UX Inconsistency (THE MAIN ISSUE)
**Problem:** FloatingTab and Login Button have different click behaviors
- **FloatingTab:** Auto-tries extension → Falls back to modal if denied
- **Login Button:** Always opens modal for user choice
**Impact:**
- Confusing user experience
- Inconsistent interaction patterns
- Users don't get consistent choice of authentication method
**Root Cause:** Line 358-367 in FloatingTab._handleClick() method
### Proposed Solutions:
#### Option 1: Make FloatingTab Consistent (Recommended)
```javascript
async _handleClick() {
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
this._showUserMenu();
} else {
// Always open modal - consistent with login button
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
}
```
#### Option 2: Add Configuration Option
```javascript
floatingTab: {
behavior: {
autoTryExtension: false, // Default to consistent behavior
// ... other options
}
}
```
## ⚠️ IMPLEMENTATION STATUS: READY FOR CODE CHANGES
**User Decision:** FloatingTab should behave exactly like login buttons - always open modal for authentication method selection.
**Required Changes:**
1. **File:** `lite/build.js`
2. **Method:** `FloatingTab._handleClick()` (lines 351-369)
3. **Action:** Remove extension auto-detection, always open modal
**Current Code to Replace (lines 358-367):**
```javascript
// Check if extension is available for direct login
if (window.nostr && this._isRealExtension(window.nostr)) {
console.log('FloatingTab: Extension available, attempting direct extension login');
await this._tryExtensionLogin(window.nostr);
} else {
// Open login modal
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
```
**Replacement Code:**
```javascript
// Always open login modal (consistent with login buttons)
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
```
**Critical Safety Notes:**
-**DO NOT** change `_checkExistingAuth()` method (lines 299-349) - this handles automatic restoration on page refresh
-**ONLY** change the click handler to remove manual extension detection
- ✅ Authentication restoration will continue to work properly via the separate restoration system
- ✅ Extension detection logic remains intact for other purposes (storage, verification, etc.)
**After Implementation:**
- Rebuild the library with `node lite/build.js`
- Test that both floating tab and login buttons behave identically
- Verify that automatic login restoration on page refresh still works properly
## Important Notes
1. **Extension-First Architecture:** The system never interferes with real browser extensions
2. **Dual Storage Support:** Supports both per-window (sessionStorage) and cross-window (localStorage) persistence
3. **Security-First:** Sensitive data is always encrypted or not stored
4. **Event-Driven:** All components communicate via custom events
5. **Automatic Restoration:** Authentication state is automatically restored on page refresh when possible
The login logic is complex due to supporting multiple authentication methods, security requirements, and different storage strategies, but it provides a flexible and secure authentication system for Nostr applications.

View File

@@ -1,139 +0,0 @@
# 🏰 NIP-46 Remote Signer (Bunker) Test Setup
This directory contains a complete NIP-46 remote signing setup for testing NOSTR_LOGIN_LITE.
## 🔧 Setup Overview
**Bunker**: A remote signer daemon that holds your private keys securely
**Client**: Browser client that connects to the bunker to request signatures
**NOSTR_LOGIN_LITE**: Connects to bunker for remote signing capability
## 🔑 Generated Keys
```
Bunker Secret Key: a33767c3bd05bda47880119d6665b79e6f0eecdf8d025966b0b59a9366379d01
Bunker Public Key: 7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90
```
## 🚀 Testing NIP-46 Remote Signing
### Step 1: Start the Bunker
```bash
# Open a new terminal and run:
./nip46-test/start-bunker.sh
```
You'll see output like:
```
🔐 Starting NIP-46 Bunker Remote Signer...
==============================================
Bunker Public Key: 7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90
Secret key is securely held by bunker
🚀 Starting bunker daemon...
{"time":"202X-XX-XXTXX:XX:XX.XXXZ","level":"info","msg":"starting bunker on ws://localhost:8080"}
{"time":"202X-XX-XXTXX:XX:XX.XXXZ","level":"info","msg":"bunker ready to handle NIP-46 requests"}
```
### Step 2: Test with NOSTR_LOGIN_LITE
Navigate to:
```
http://localhost:8000/examples/modal-login-demo.html
```
Click "🚀 Launch Authentication Modal" and select **"NIP-46 Remote"** option.
The browser will connect to the bunker running on `ws://localhost:8080` and request signatures remotely.
## 🔄 How NIP-46 Works
```
Browser (NOSTR_LOGIN_LITE) → WebSocket → Bunker (NAK on localhost:8080)
↓ ↓
Requests signature Holds private key
↓ ↓
Receives signed event Signs & returns result
```
## 📁 Files in this Directory
- `start-bunker.sh` - Script to start the remote signer daemon
- `bunker-config.js` - Configuration for NOSTR_LOGIN_LITE
- `README.md` - This documentation
## 🧪 Testing Scenarios
### ✅ Successful Connection
- Bunker runs on localhost:8080
- Browser connects and requests pubkey
- Bunker responds with `7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90`
### 🔧 Signature Requests
- Browser sends event to sign
- Bunker signs with private key
- Signed event returned to browser
- Browser publishes signed event to relay
### 🐛 Debug Issues
- Check bunker logs for connection errors
- Verify WebSocket connection in browser dev tools
- Look for NIP-46 protocol errors
## 📝 NOSTR_LOGIN_LITE Configuration
In your app, configure remote signing like this:
```javascript
const nip46Config = {
type: "nip46",
bunker: {
pubkey: "7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90",
url: "ws://your-bunker-server:8080" // Production URL
}
};
await window.NOSTR_LOGIN_LITE.init(nip46Config);
```
## ⚠️ Production Notes
- **This setup uses localhost** - replace with real server URL in production
- **Private key is shown for testing** - production bunkers should be secured
- **WebSocket URL should be secure** (wss://) in production
- **Consider authentication** for your bunker to prevent unauthorized access
## 🎯 Common Testing Commands
### Check NAK version
```bash
nak --version
```
### Generate new keys (if needed)
```bash
nak key generate # Secret key
echo "your_secret_key_here" | nak key public # Public key
```
### Manual bunker test
```bash
nak bunker --sec "your_secret_key" --port 8080 --relay "wss://relay.damus.io"
```
## 🐛 Troubleshooting
**Bunker won't start:**
- Check if port 8080 is free
- Verify NAK is installed correctly
**Browser can't connect:**
- Check firewall settings
- Verify bunker is running (`ps aux | grep nak`)
- Check browser console for WebSocket errors
**Signing fails:**
- Verify keys are correct
- Check bunker logs for errors
- Ensure event format is valid

1
nostr-tools Submodule

Submodule nostr-tools added at 23aebbd341

370
themes/README.md Normal file
View File

@@ -0,0 +1,370 @@
# NOSTR_LOGIN_LITE Theme System
A comprehensive theming system supporting CSS custom properties, JSON metadata, and runtime theme switching.
## Architecture Overview
The theme system consists of:
- **CSS Custom Properties**: Dynamic styling variables
- **JSON Metadata**: Theme descriptions and configurations
- **Theme Manager**: Runtime loading and switching
- **Directory Organization**: Structured theme packages
## Directory Structure
```
themes/
├── README.md # This documentation
├── theme-manager.js # Theme management system
├── default/ # Default monospace theme
│ ├── theme.json # Theme metadata
│ ├── theme.css # CSS custom properties
│ └── assets/ # Theme assets (fonts, images)
├── dark/ # Dark cyberpunk theme
│ ├── theme.json
│ ├── theme.css
│ └── assets/
└── community/ # Community contributed themes
└── [theme-name]/
├── theme.json
├── theme.css
└── assets/
```
## CSS Custom Properties
All themes use standardized CSS custom properties with the `--nl-` prefix:
### Colors
- `--nl-primary-color`: Main text/border color
- `--nl-secondary-color`: Background color
- `--nl-accent-color`: Hover/active accent color
### Typography
- `--nl-font-family`: Base font family
- `--nl-font-size-base`: Base font size (14px)
- `--nl-font-size-title`: Title font size (24px)
- `--nl-font-size-heading`: Heading font size (18px)
- `--nl-font-size-button`: Button font size (16px)
- `--nl-font-weight-normal`: Normal weight (400)
- `--nl-font-weight-medium`: Medium weight (500)
- `--nl-font-weight-bold`: Bold weight (600)
### Layout
- `--nl-border-radius`: Border radius (15px)
- `--nl-border-width`: Border thickness (3px)
- `--nl-border-style`: Border style (solid)
- `--nl-padding-button`: Button padding (12px 16px)
- `--nl-padding-container`: Container padding (20px 24px)
### Effects
- `--nl-transition-duration`: Animation duration (0.2s)
- `--nl-transition-easing`: Animation easing (ease)
- `--nl-shadow`: Box shadow effects
- `--nl-backdrop-filter`: Backdrop filter effects
### Component States
- `--nl-button-bg`: Button background
- `--nl-button-color`: Button text color
- `--nl-button-border`: Button border
- `--nl-button-hover-border-color`: Button hover border
- `--nl-button-active-bg`: Button active background
- `--nl-button-active-color`: Button active text
## Theme Metadata (theme.json)
Each theme must include a `theme.json` file with the following structure:
```json
{
"name": "Theme Display Name",
"version": "1.0.0",
"author": "Author Name/Email",
"description": "Theme description",
"preview": "preview.png",
"compatibility": "1.0+",
"license": "MIT",
"variables": {
"--nl-primary-color": "#000000",
"--nl-secondary-color": "#ffffff",
"--nl-accent-color": "#ff0000"
},
"assets": ["fonts/", "images/"],
"tags": ["monospace", "dark", "accessibility"]
}
```
### Required Fields
- `name`: Human-readable theme name
- `version`: Semantic version number
- `variables`: CSS custom property values
### Optional Fields
- `author`: Theme creator information
- `description`: Theme description
- `preview`: Preview image filename
- `compatibility`: Minimum library version
- `license`: License identifier (MIT, GPL, etc.)
- `assets`: Additional asset directories
- `tags`: Theme categorization tags
## Creating a New Theme
### 1. Create Theme Directory
```bash
mkdir themes/my-theme
cd themes/my-theme
```
### 2. Create theme.json
```json
{
"name": "My Custom Theme",
"version": "1.0.0",
"author": "Your Name",
"description": "A custom theme for NOSTR_LOGIN_LITE",
"variables": {
"--nl-primary-color": "#your-color",
"--nl-secondary-color": "#your-bg-color",
"--nl-accent-color": "#your-accent-color",
"--nl-font-family": "\"Your Font\", monospace"
},
"tags": ["custom"]
}
```
### 3. Create theme.css
```css
:root {
/* Colors */
--nl-primary-color: #your-color;
--nl-secondary-color: #your-bg-color;
--nl-accent-color: #your-accent-color;
/* Typography */
--nl-font-family: "Your Font", monospace;
/* Layout - inherit defaults or customize */
--nl-border-radius: 15px;
--nl-border-width: 3px;
/* Add custom variables */
--nl-custom-shadow: 0 0 10px rgba(0,0,0,0.3);
}
/* Optional: Custom component styles */
.nl-button {
/* Theme-specific enhancements */
box-shadow: var(--nl-custom-shadow);
}
```
### 4. Add Assets (Optional)
```
my-theme/
├── theme.json
├── theme.css
└── assets/
├── fonts/
│ └── custom-font.woff2
└── images/
└── pattern.png
```
### 5. Register Theme
Update `theme-manager.js` to include your theme:
```javascript
this.availableThemes.set('my-theme', {
name: 'My Custom Theme',
path: 'my-theme',
description: 'A custom theme for NOSTR_LOGIN_LITE'
});
```
## Using Themes
### Initialization
```javascript
await NOSTR_LOGIN_LITE.init({
theme: 'default',
themePath: './themes/'
});
```
### Runtime Switching
```javascript
// Switch theme
await NOSTR_LOGIN_LITE.switchTheme('dark');
// Get current theme
const current = NOSTR_LOGIN_LITE.getCurrentTheme();
// List available themes
const available = NOSTR_LOGIN_LITE.getAvailableThemes();
```
### Custom Variables
```javascript
// Set custom variable
NOSTR_LOGIN_LITE.setThemeVariable('--nl-accent-color', '#ff00ff');
// Get variable value
const value = NOSTR_LOGIN_LITE.getThemeVariable('--nl-accent-color');
```
### Theme Export
```javascript
// Export current theme configuration
const themeData = NOSTR_LOGIN_LITE.exportTheme();
console.log(JSON.stringify(themeData, null, 2));
```
## Theme Guidelines
### Accessibility
- Ensure sufficient color contrast (4.5:1 minimum)
- Test with screen readers
- Support high contrast mode
- Use semantic color names
### Performance
- Minimize CSS file size
- Optimize asset files
- Use web-safe fonts as fallbacks
- Consider loading performance
### Compatibility
- Test across browsers
- Ensure mobile responsiveness
- Validate CSS custom property support
- Test with different font sizes
### Best Practices
- Use consistent naming conventions
- Provide clear documentation
- Include preview images
- Tag themes appropriately
- Test thoroughly before submission
## Community Contributions
### Submission Process
1. Fork the repository
2. Create theme in `themes/community/your-theme/`
3. Follow all guidelines above
4. Test thoroughly
5. Submit pull request with:
- Theme files
- Preview screenshot
- Documentation updates
### Review Criteria
- Code quality and organization
- Accessibility compliance
- Cross-browser compatibility
- Unique design contribution
- Proper documentation
## Built-in Themes
### Default Theme
- **Colors**: Black/white/red
- **Typography**: Courier New monospace
- **Style**: Clean, minimalist, accessible
- **Use Case**: General purpose, high readability
### Dark Theme
- **Colors**: Green/black/magenta
- **Typography**: Courier New monospace
- **Style**: Cyberpunk, terminal-inspired
- **Use Case**: Low light environments, developer aesthetic
## API Reference
### ThemeManager Class
```javascript
const themeManager = new NostrThemeManager();
// Load theme
await themeManager.loadTheme('theme-name');
// Switch theme
await themeManager.switchTheme('theme-name');
// Get available themes
const themes = themeManager.getAvailableThemes();
// Set/get variables
themeManager.setThemeVariable('--nl-accent-color', '#ff0000');
const value = themeManager.getThemeVariable('--nl-accent-color');
// Export current theme
const exported = themeManager.exportCurrentTheme();
```
### NOSTR_LOGIN_LITE Integration
```javascript
// Initialize with theme
await NOSTR_LOGIN_LITE.init({ theme: 'dark' });
// Theme management
await NOSTR_LOGIN_LITE.switchTheme('theme-name');
const current = NOSTR_LOGIN_LITE.getCurrentTheme();
const available = NOSTR_LOGIN_LITE.getAvailableThemes();
// Variable management
NOSTR_LOGIN_LITE.setThemeVariable('--nl-primary-color', '#000000');
const color = NOSTR_LOGIN_LITE.getThemeVariable('--nl-primary-color');
// Export functionality
const themeData = NOSTR_LOGIN_LITE.exportTheme();
```
## Events
The theme system dispatches events for integration:
```javascript
// Theme change event
window.addEventListener('nlThemeChanged', (event) => {
console.log('New theme:', event.detail.theme);
console.log('Theme data:', event.detail.data);
});
```
## Troubleshooting
### Theme Not Loading
- Check theme.json syntax
- Verify file paths
- Check browser console for errors
- Ensure CSS custom properties are supported
### Variables Not Applying
- Verify CSS custom property names (--nl- prefix)
- Check CSS specificity
- Ensure theme CSS is loaded after base styles
- Validate variable values
### Performance Issues
- Optimize CSS file size
- Compress assets
- Use efficient selectors
- Consider lazy loading for large themes
## License
The theme system is open source under the MIT license. Individual themes may have their own licenses as specified in their theme.json files.

115
themes/dark/theme.css Normal file
View File

@@ -0,0 +1,115 @@
/**
* NOSTR_LOGIN_LITE - Dark Monospace Theme
*/
:root {
/* Core Variables (6) */
--nl-primary-color: #white;
--nl-secondary-color: #black;
--nl-accent-color: #ff0000;
--nl-muted-color: #666666;
--nl-font-family: "Courier New", Courier, monospace;
--nl-border-radius: 15px;
--nl-border-width: 3px;
/* Floating Tab Variables (8) */
--nl-tab-bg-logged-out: #ffffff;
--nl-tab-bg-logged-in: #000000;
--nl-tab-bg-opacity-logged-out: 0.9;
--nl-tab-bg-opacity-logged-in: 0.8;
--nl-tab-color-logged-out: #000000;
--nl-tab-color-logged-in: #ffffff;
--nl-tab-border-logged-out: #000000;
--nl-tab-border-logged-in: #ff0000;
--nl-tab-border-opacity-logged-out: 1.0;
--nl-tab-border-opacity-logged-in: 0.9;
}
/* Base component styles using simplified variables */
.nl-component {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
}
.nl-button {
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
font-family: var(--nl-font-family);
cursor: pointer;
transition: all 0.2s ease;
}
.nl-button:hover {
border-color: var(--nl-accent-color);
}
.nl-button:active {
background: var(--nl-accent-color);
color: var(--nl-secondary-color);
}
.nl-input {
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
font-family: var(--nl-font-family);
box-sizing: border-box;
}
.nl-input:focus {
border-color: var(--nl-accent-color);
outline: none;
}
.nl-container {
background: var(--nl-secondary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
}
.nl-title, .nl-heading {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
margin: 0;
}
.nl-text {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
}
.nl-text--muted {
color: var(--nl-muted-color);
}
.nl-icon {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
}
/* Floating tab styles */
.nl-floating-tab {
font-family: var(--nl-font-family);
border-radius: var(--nl-border-radius);
border: var(--nl-border-width) solid;
transition: all 0.2s ease;
}
.nl-floating-tab--logged-out {
background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out));
color: var(--nl-tab-color-logged-out);
border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out));
}
.nl-floating-tab--logged-in {
background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in));
color: var(--nl-tab-color-logged-in);
border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in));
}
.nl-transition {
transition: all 0.2s ease;
}

117
themes/default/theme.css Normal file
View File

@@ -0,0 +1,117 @@
/**
* NOSTR_LOGIN_LITE - Default Monospace Theme
* Black/white/red color scheme with monospace typography
* Simplified 14-variable system (6 core + 8 floating tab)
*/
:root {
/* Core Variables (6) */
--nl-primary-color: #000000;
--nl-secondary-color: #ffffff;
--nl-accent-color: #ff0000;
--nl-muted-color: #CCCCCC;
--nl-font-family: "Courier New", Courier, monospace;
--nl-border-radius: 15px;
--nl-border-width: 3px;
/* Floating Tab Variables (8) */
--nl-tab-bg-logged-out: #ffffff;
--nl-tab-bg-logged-in: #ffffff;
--nl-tab-bg-opacity-logged-out: 0.9;
--nl-tab-bg-opacity-logged-in: 0.2;
--nl-tab-color-logged-out: #000000;
--nl-tab-color-logged-in: #ffffff;
--nl-tab-border-logged-out: #000000;
--nl-tab-border-logged-in: #ff0000;
--nl-tab-border-opacity-logged-out: 1.0;
--nl-tab-border-opacity-logged-in: 0.1;
}
/* Base component styles using simplified variables */
.nl-component {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
}
.nl-button {
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
font-family: var(--nl-font-family);
cursor: pointer;
transition: all 0.2s ease;
}
.nl-button:hover {
border-color: var(--nl-accent-color);
}
.nl-button:active {
background: var(--nl-accent-color);
color: var(--nl-secondary-color);
}
.nl-input {
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
font-family: var(--nl-font-family);
box-sizing: border-box;
}
.nl-input:focus {
border-color: var(--nl-accent-color);
outline: none;
}
.nl-container {
background: var(--nl-secondary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
}
.nl-title, .nl-heading {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
margin: 0;
}
.nl-text {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
}
.nl-text--muted {
color: var(--nl-muted-color);
}
.nl-icon {
font-family: var(--nl-font-family);
color: var(--nl-primary-color);
}
/* Floating tab styles */
.nl-floating-tab {
font-family: var(--nl-font-family);
border-radius: var(--nl-border-radius);
border: var(--nl-border-width) solid;
transition: all 0.2s ease;
}
.nl-floating-tab--logged-out {
background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out));
color: var(--nl-tab-color-logged-out);
border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out));
}
.nl-floating-tab--logged-in {
background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in));
color: var(--nl-tab-color-logged-in);
border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in));
}
.nl-transition {
transition: all 0.2s ease;
}

35
themes/index.json Normal file
View File

@@ -0,0 +1,35 @@
{
"version": "1.0.0",
"themes": {
"default": {
"name": "Default Monospace",
"path": "default",
"description": "Black/white/red monospace theme with rounded buttons",
"author": "NOSTR_LOGIN_LITE",
"version": "1.0.0",
"preview": "default/preview.png",
"tags": ["monospace", "minimalist", "accessibility", "default"],
"featured": true
},
"dark": {
"name": "Dark Monospace",
"path": "dark",
"description": "Dark mode with green accents and monospace typography",
"author": "NOSTR_LOGIN_LITE",
"version": "1.0.0",
"preview": "dark/preview.png",
"tags": ["dark", "cyberpunk", "monospace", "accessibility"],
"featured": true
}
},
"categories": {
"official": ["default", "dark"],
"community": [],
"experimental": []
},
"metadata": {
"total_themes": 2,
"last_updated": "2025-01-14T11:13:00.000Z",
"schema_version": "1.0.0"
}
}

286
themes/theme-manager.js Normal file
View File

@@ -0,0 +1,286 @@
/**
* NOSTR_LOGIN_LITE Theme Manager
* Handles theme loading, switching, and CSS custom property management
*/
class NostrThemeManager {
constructor() {
this.currentTheme = null;
this.availableThemes = new Map();
this.themeCache = new Map();
this.basePath = './themes/';
// Initialize with default theme
this.init();
}
async init() {
try {
// Load available themes index
await this.loadThemeIndex();
// Set default theme if none is set
if (!this.currentTheme) {
await this.loadTheme('default');
}
console.log('NostrThemeManager: Initialized with themes:', Array.from(this.availableThemes.keys()));
} catch (error) {
console.error('NostrThemeManager: Initialization failed:', error);
this.fallbackToInlineStyles();
}
}
async loadThemeIndex() {
// For now, we'll manually register available themes
// In production, this could fetch from a themes.json index file
this.availableThemes.set('default', {
name: 'Default Monospace',
path: 'default',
description: 'Black/white/red monospace theme'
});
this.availableThemes.set('dark', {
name: 'Dark Monospace',
path: 'dark',
description: 'Dark mode with green accents and monospace typography'
});
// Future themes can be registered here or loaded from an index
// this.availableThemes.set('cyberpunk', { ... });
}
async loadTheme(themeName) {
try {
console.log(`NostrThemeManager: Loading theme "${themeName}"`);
// Check if theme exists
if (!this.availableThemes.has(themeName)) {
throw new Error(`Theme "${themeName}" not found`);
}
// Check cache first
if (this.themeCache.has(themeName)) {
const cachedTheme = this.themeCache.get(themeName);
this.applyTheme(cachedTheme);
this.currentTheme = themeName;
return cachedTheme;
}
// Load theme metadata
const themeInfo = this.availableThemes.get(themeName);
const metadataUrl = `${this.basePath}${themeInfo.path}/theme.json`;
const response = await fetch(metadataUrl);
if (!response.ok) {
throw new Error(`Failed to load theme metadata: ${response.statusText}`);
}
const themeData = await response.json();
// Validate theme data
this.validateThemeData(themeData);
// Load CSS file
await this.loadThemeCSS(themeInfo.path);
// Cache the theme data
this.themeCache.set(themeName, themeData);
// Apply the theme
this.applyTheme(themeData);
this.currentTheme = themeName;
console.log(`NostrThemeManager: Successfully loaded theme "${themeName}"`);
// Dispatch theme change event
this.dispatchThemeChangeEvent(themeName, themeData);
return themeData;
} catch (error) {
console.error(`NostrThemeManager: Failed to load theme "${themeName}":`, error);
throw error;
}
}
async loadThemeCSS(themePath) {
const cssUrl = `${this.basePath}${themePath}/theme.css`;
// Remove existing theme CSS
const existingThemeCSS = document.getElementById('nl-theme-css');
if (existingThemeCSS) {
existingThemeCSS.remove();
}
// Load new theme CSS
const link = document.createElement('link');
link.id = 'nl-theme-css';
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = cssUrl;
// Wait for CSS to load
await new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`));
document.head.appendChild(link);
});
}
applyTheme(themeData) {
if (!themeData.variables) {
console.warn('NostrThemeManager: Theme data has no variables to apply');
return;
}
const root = document.documentElement;
// Apply CSS custom properties
Object.entries(themeData.variables).forEach(([property, value]) => {
root.style.setProperty(property, value);
});
console.log(`NostrThemeManager: Applied ${Object.keys(themeData.variables).length} CSS variables`);
}
validateThemeData(themeData) {
const required = ['name', 'version', 'variables'];
for (const field of required) {
if (!themeData[field]) {
throw new Error(`Theme validation failed: missing required field "${field}"`);
}
}
if (typeof themeData.variables !== 'object') {
throw new Error('Theme validation failed: variables must be an object');
}
}
fallbackToInlineStyles() {
console.log('NostrThemeManager: Falling back to inline styles');
// Apply default theme variables directly
const defaultVariables = {
'--nl-primary-color': '#000000',
'--nl-secondary-color': '#ffffff',
'--nl-accent-color': '#ff0000',
'--nl-font-family': '"Courier New", Courier, monospace',
'--nl-border-radius': '15px',
'--nl-border-width': '3px',
'--nl-border-style': 'solid',
'--nl-padding-button': '12px 16px',
'--nl-padding-container': '20px 24px',
'--nl-font-size-base': '14px',
'--nl-font-size-title': '24px',
'--nl-font-size-button': '16px',
'--nl-transition-duration': '0.2s'
};
const root = document.documentElement;
Object.entries(defaultVariables).forEach(([property, value]) => {
root.style.setProperty(property, value);
});
this.currentTheme = 'fallback';
}
dispatchThemeChangeEvent(themeName, themeData) {
if (typeof window !== 'undefined') {
const event = new CustomEvent('nlThemeChanged', {
detail: {
theme: themeName,
data: themeData,
timestamp: Date.now()
}
});
window.dispatchEvent(event);
}
}
// Public API methods
getCurrentTheme() {
return this.currentTheme;
}
getAvailableThemes() {
return Array.from(this.availableThemes.keys());
}
getThemeInfo(themeName) {
return this.availableThemes.get(themeName);
}
async switchTheme(themeName) {
return await this.loadTheme(themeName);
}
getThemeVariable(variableName) {
if (typeof window === 'undefined') return null;
const root = document.documentElement;
const style = getComputedStyle(root);
return style.getPropertyValue(variableName);
}
setThemeVariable(variableName, value) {
if (typeof window === 'undefined') return;
const root = document.documentElement;
root.style.setProperty(variableName, value);
}
resetTheme() {
const root = document.documentElement;
// Remove all nl- prefixed custom properties
const style = getComputedStyle(root);
for (let i = 0; i < style.length; i++) {
const property = style[i];
if (property.startsWith('--nl-')) {
root.style.removeProperty(property);
}
}
// Remove theme CSS
const themeCSS = document.getElementById('nl-theme-css');
if (themeCSS) {
themeCSS.remove();
}
this.currentTheme = null;
}
// Theme creation utilities (for developers)
exportCurrentTheme() {
const root = document.documentElement;
const style = getComputedStyle(root);
const variables = {};
for (let i = 0; i < style.length; i++) {
const property = style[i];
if (property.startsWith('--nl-')) {
variables[property] = style.getPropertyValue(property);
}
}
return {
name: 'Custom Theme',
version: '1.0.0',
author: 'User',
description: 'Exported theme',
variables,
timestamp: new Date().toISOString()
};
}
}
// Export for use in NOSTR_LOGIN_LITE
if (typeof window !== 'undefined') {
window.NostrThemeManager = NostrThemeManager;
console.log('NostrThemeManager: Class available globally');
} else {
// Node.js environment
module.exports = NostrThemeManager;
}