Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59bf17372 | ||
|
|
3b1eb7f951 | ||
|
|
72b0d9b102 |
42
.gitignore
vendored
42
.gitignore
vendored
@@ -1,32 +1,26 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
nostr-tools/
|
||||
|
||||
packages/auth/dist/
|
||||
packages/components/dist/
|
||||
packages/components/www/
|
||||
packages/components/loader/
|
||||
# IDE and OS files
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
packages/components/*~
|
||||
packages/components/*.sw[mnpcod]
|
||||
packages/components/*.log
|
||||
packages/components/*.lock
|
||||
packages/components/*.tmp
|
||||
packages/components/*.tmp.*
|
||||
packages/components/log.txt
|
||||
packages/components/*.sublime-project
|
||||
packages/components/*.sublime-workspace
|
||||
# Temporary files
|
||||
*.log
|
||||
*.tmp
|
||||
*.tmp.*
|
||||
*~
|
||||
*.sw[mnpcod]
|
||||
*.lock
|
||||
log.txt
|
||||
Trash/
|
||||
|
||||
packages/components/.stencil/
|
||||
packages/components/.idea/
|
||||
packages/components/.vscode/
|
||||
packages/components/.sass-cache/
|
||||
packages/components/.versions/
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
packages/components/$RECYCLE.BIN/
|
||||
|
||||
packages/components/.DS_Store
|
||||
packages/components/Thumbs.db
|
||||
packages/components/UserInterfaceState.xcuserstate
|
||||
packages/components/.env
|
||||
# Aider files
|
||||
.aider.chat.history.md
|
||||
.aider.input.history
|
||||
|
||||
211
17.md
Normal file
211
17.md
Normal file
@@ -0,0 +1,211 @@
|
||||
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
Normal file
297
44.md
Normal file
@@ -0,0 +1,297 @@
|
||||
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
|
||||
@@ -1,318 +0,0 @@
|
||||
# NOSTR_LOGIN_LITE
|
||||
|
||||
Objective
|
||||
- Deliver a minimal, dependency-light replacement for the current auth/UI stack that:
|
||||
- Preserves all login methods: Nostr Connect (nip46), Extension, Local key, Read-only, OTP/DM.
|
||||
- Exposes the same window.nostr surface: [getPublicKey()](lite/nostr-login-lite.js:1), [signEvent(event)](lite/nostr-login-lite.js:1), [nip04.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1), [nip04.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1), [nip44.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1), [nip44.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1).
|
||||
- Dispatches identical nlAuth/nlLaunch/nlLogout/nlDarkMode events and accepts setAuth calls for compatibility.
|
||||
|
||||
Key differences vs current project
|
||||
- UI: replace Stencil/Tailwind component library with a single vanilla-JS modal and minimal CSS.
|
||||
- Transport: remove NDK; implement NIP-46 RPC using nostr-tools SimplePool.
|
||||
- Crypto: rely on nostr-tools (via CDN global window.NostrTools) for keygen/signing (finalizeEvent), nip04, nip19, and SimplePool; embed a small NIP-44 codec if window.NostrTools.nip44 is not available. The current project’s codec is in [packages/auth/src/utils/nip44.ts](packages/auth/src/utils/nip44.ts).
|
||||
|
||||
Nostr Tools via CDN (global window.NostrTools)
|
||||
- Include the bundle once in the page:
|
||||
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
||||
- Access APIs via window.NostrTools inside the lite build:
|
||||
- [NostrTools.generateSecretKey()](lite/nostr-login-lite.js:1) or [NostrTools.generatePrivateKey()](lite/nostr-login-lite.js:1) (depending on CDN version)
|
||||
- [NostrTools.getPublicKey()](lite/nostr-login-lite.js:1)
|
||||
- [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1)
|
||||
- [NostrTools.nip04](lite/nostr-login-lite.js:1) encrypt/decrypt
|
||||
- [NostrTools.nip19](lite/nostr-login-lite.js:1) npub/nsec encode/decode
|
||||
- [NostrTools.SimplePool](lite/nostr-login-lite.js:1) relay connectivity
|
||||
|
||||
Supported login methods and crypto summary
|
||||
- Connect (nip46): secp256k1 Schnorr for event signing/hash; nip44 (ChaCha20/HKDF/HMAC) for RPC payloads, with nip04 fallback. Requires relay connectivity (via [NostrTools.SimplePool](lite/nostr-login-lite.js:1)).
|
||||
- Extension: crypto handled by the extension; we bridge calls.
|
||||
- Local key: secp256k1 Schnorr signing ([NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1)) and nip04/nip44 encrypt/decrypt locally.
|
||||
- Read-only: no client crypto.
|
||||
- OTP/DM: no client crypto beyond state persistence; server sends DM and verifies via HTTP.
|
||||
|
||||
Minimal file layout
|
||||
- Single file drop-in (script tag friendly):
|
||||
- [lite/nostr-login-lite.js](lite/nostr-login-lite.js)
|
||||
- Optional split during development (bundled later):
|
||||
- [lite/core/nostr-lite.js](lite/core/nostr-lite.js)
|
||||
- [lite/core/nip46-client.js](lite/core/nip46-client.js)
|
||||
- [lite/core/extension-bridge.js](lite/core/extension-bridge.js)
|
||||
- [lite/core/store.js](lite/core/store.js)
|
||||
- [lite/ui/modal.js](lite/ui/modal.js)
|
||||
|
||||
External dependencies strategy
|
||||
- Use the CDN bundle to provide [window.NostrTools](lite/nostr-login-lite.js:1) at runtime. No npm installs required.
|
||||
- Prefer built-ins from window.NostrTools:
|
||||
- Keys/sign: [generateSecretKey()](lite/nostr-login-lite.js:1)/[generatePrivateKey()](lite/nostr-login-lite.js:1), [getPublicKey()](lite/nostr-login-lite.js:1), [finalizeEvent()](lite/nostr-login-lite.js:1).
|
||||
- Encoding: [nip19](lite/nostr-login-lite.js:1).
|
||||
- Encryption: [nip04](lite/nostr-login-lite.js:1).
|
||||
- Relays: [SimplePool](lite/nostr-login-lite.js:1).
|
||||
- Nip44 choice:
|
||||
- If window.NostrTools.nip44 is available, use it directly.
|
||||
- Otherwise embed the existing lightweight codec adapted from [packages/auth/src/utils/nip44.ts](packages/auth/src/utils/nip44.ts) as [Nip44](lite/nostr-login-lite.js:1).
|
||||
|
||||
Compatibility requirements
|
||||
- Provide the same exports: [init(opts)](lite/nostr-login-lite.js:1), [launch(startScreen)](lite/nostr-login-lite.js:1), [logout()](lite/nostr-login-lite.js:1), [setDarkMode(dark)](lite/nostr-login-lite.js:1), [setAuth(o)](lite/nostr-login-lite.js:1), [cancelNeedAuth()](lite/nostr-login-lite.js:1).
|
||||
- Dispatch identical events and payload shapes as in [onAuth()](packages/auth/src/modules/AuthNostrService.ts:347).
|
||||
|
||||
Architecture overview
|
||||
- NostrLite: window.nostr facade mirroring current behavior, invokes auth UI when needed.
|
||||
- Auth: manages methods (connect/extension/local/readOnly/otp), state, storage, event dispatch.
|
||||
- NIP46Client: minimal RPC over [NostrTools.SimplePool](lite/nostr-login-lite.js:1) (subscribe, send request, parse/decrypt, dedupe auth_url).
|
||||
- ExtensionBridge: safely handle window.nostr detection, guard against overwrites, switch to extension mode when requested.
|
||||
- Store: localStorage helpers for accounts/current/recents and misc values.
|
||||
- UI: single vanilla modal that lists options and drives flow, with optional inline iframe starter for providers that publish iframe_url.
|
||||
|
||||
Function-level TODO checklist (execution order)
|
||||
|
||||
Bootstrap and API surface
|
||||
- [ ] [Deps.ensureNostrToolsLoaded()](lite/nostr-login-lite.js:1)
|
||||
- Verify window.NostrTools presence; throw with actionable message if missing.
|
||||
|
||||
- [ ] [NostrLite.init(options: NostrLoginOptions)](lite/nostr-login-lite.js:1)
|
||||
- Call [Deps.ensureNostrToolsLoaded()](lite/nostr-login-lite.js:1).
|
||||
- Bind window.nostr to the facade.
|
||||
- Initialize ExtensionBridge checking loop.
|
||||
- Mount modal UI, persist options (theme, perms, bunkers, methods, otp URLs, default relays).
|
||||
- Wire nlLaunch/nlLogout/nlDarkMode/nlSetAuth listeners.
|
||||
|
||||
- [ ] [NostrLite.launch(startScreen?: string)](lite/nostr-login-lite.js:1)
|
||||
- Open modal with selected start screen or switch-account if accounts exist.
|
||||
|
||||
- [ ] [NostrLite.logout()](lite/nostr-login-lite.js:1)
|
||||
- Clear current account; dispatch nlAuth logout; reset signer/client state.
|
||||
|
||||
- [ ] [NostrLite.setDarkMode(dark: boolean)](lite/nostr-login-lite.js:1)
|
||||
- Persist and apply theme to modal.
|
||||
|
||||
- [ ] [NostrLite.setAuth(o: NostrLoginAuthOptions)](lite/nostr-login-lite.js:1)
|
||||
- Validate o.type in {login, signup, logout}; o.method in {connect, extension, local, otp, readOnly}.
|
||||
- Delegate to [Auth.switchAccount()](lite/nostr-login-lite.js:1); dispatch consistent nlAuth.
|
||||
|
||||
- [ ] [NostrLite.cancelNeedAuth()](lite/nostr-login-lite.js:1)
|
||||
- Cancel current connect/listen flow and close modal section if waiting.
|
||||
|
||||
window.nostr facade
|
||||
- [ ] [NostrLite.ensureAuth()](lite/nostr-login-lite.js:1)
|
||||
- If no user, open modal; await until authenticated or rejected.
|
||||
|
||||
- [ ] [NostrLite.getPublicKey()](lite/nostr-login-lite.js:1)
|
||||
- Ensure auth, return current pubkey or throw.
|
||||
|
||||
- [ ] [NostrLite.signEvent(event: NostrEvent)](lite/nostr-login-lite.js:1)
|
||||
- Ensure auth; if local key then sign locally via [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1); else sign via [NIP46Client.sendRequest()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [NostrLite.nip04.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [NostrLite.nip04.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1)
|
||||
- Ensure auth; route to local ([NostrTools.nip04](lite/nostr-login-lite.js:1)) or remote signer via NIP-46 nip04_{encrypt|decrypt}.
|
||||
|
||||
- [ ] [NostrLite.nip44.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [NostrLite.nip44.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1)
|
||||
- Ensure auth; use window.NostrTools.nip44 if present or local [Nip44](lite/nostr-login-lite.js:1); remote via nip44_{encrypt|decrypt}.
|
||||
|
||||
Auth module
|
||||
- [ ] [Auth.startAuthSession()](lite/nostr-login-lite.js:1)
|
||||
- Create a promise used by ensureAuth/waitReady; allows Connect flow to resolve or be cancelled.
|
||||
|
||||
- [ ] [Auth.endAuthSession()](lite/nostr-login-lite.js:1)
|
||||
- Resolve session; if iframeUrl present, bind iframe port to NIP46Client before resolving.
|
||||
|
||||
- [ ] [Auth.resetAuthSession()](lite/nostr-login-lite.js:1)
|
||||
- Cancel current session safely.
|
||||
|
||||
- [ ] [Auth.onAuth(type: "login" | "signup" | "logout", info?: Info)](lite/nostr-login-lite.js:1)
|
||||
- Update state, persist, dispatch nlAuth; fetch profile asynchronously and update name/picture when fetched; preserve nip05 semantics.
|
||||
|
||||
- [ ] [Auth.switchAccount(info: Info, signup = false)](lite/nostr-login-lite.js:1)
|
||||
- Branch to [Auth.setReadOnly()](lite/nostr-login-lite.js:1)/[Auth.setOTP()](lite/nostr-login-lite.js:1)/[Auth.setLocal()](lite/nostr-login-lite.js:1)/[Auth.trySetExtensionForPubkey()](lite/nostr-login-lite.js:1)/[Auth.setConnect()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [Auth.setReadOnly(pubkey: string)](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [Auth.setOTP(pubkey: string, data: string)](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [Auth.localSignup(name: string, sk?: string)](lite/nostr-login-lite.js:1)
|
||||
- Generate sk via [NostrTools.generateSecretKey()](lite/nostr-login-lite.js:1)/[generatePrivateKey()](lite/nostr-login-lite.js:1) if missing; [Auth.setLocal(signup=true)](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [Auth.setLocal(info: Info, signup?: boolean)](lite/nostr-login-lite.js:1)
|
||||
- Instantiate [LocalSigner](lite/nostr-login-lite.js:1); call [Auth.onAuth()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [Auth.trySetExtensionForPubkey(pubkey: string)](lite/nostr-login-lite.js:1)
|
||||
- Use [ExtensionBridge.setExtensionReadPubkey()](lite/nostr-login-lite.js:1) and reconcile.
|
||||
|
||||
- [ ] [Auth.setConnect(info: Info)](lite/nostr-login-lite.js:1)
|
||||
- Initialize NIP46Client with existing token/relays via [NIP46Client.init()](lite/nostr-login-lite.js:1) and [NIP46Client.connect()](lite/nostr-login-lite.js:1); call [Auth.onAuth()](lite/nostr-login-lite.js:1); [Auth.endAuthSession()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [Auth.authNip46(type, { name, bunkerUrl, sk, domain, iframeUrl })](lite/nostr-login-lite.js:1)
|
||||
- Parse bunkerUrl/nip05; [NIP46Client.init()](lite/nostr-login-lite.js:1) + [NIP46Client.connect()](lite/nostr-login-lite.js:1); [Auth.onAuth()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [Auth.nostrConnect(relay?: string, opts?: { domain?: string; link?: string; importConnect?: boolean; iframeUrl?: string })](lite/nostr-login-lite.js:1)
|
||||
- Create local keypair and secret; open provider link or show iframe starter; [NIP46Client.listen()](lite/nostr-login-lite.js:1) to learn signerPubkey, then [NIP46Client.initUserPubkey()](lite/nostr-login-lite.js:1); set bunkerUrl; [Auth.onAuth()](lite/nostr-login-lite.js:1) unless importConnect.
|
||||
|
||||
- [ ] [Auth.createNostrConnect(relay?: string)](lite/nostr-login-lite.js:1)
|
||||
- Build nostrconnect:// URL with meta (icon/url/name/perms).
|
||||
|
||||
- [ ] [Auth.getNostrConnectServices(): Promise<[string, ConnectionString[]]>](lite/nostr-login-lite.js:1)
|
||||
- Return nostrconnect URL and preconfigured providers; query .well-known/nostr.json for relays and iframe_url.
|
||||
|
||||
- [ ] [Auth.importAndConnect(cs: ConnectionString)](lite/nostr-login-lite.js:1)
|
||||
- nostrConnect(..., importConnect=true) → logout keep signer → onAuth(login, connect info).
|
||||
|
||||
- [ ] [Auth.createAccount(nip05: string)](lite/nostr-login-lite.js:1) (optional parity)
|
||||
- Use bunker provider to create account; return bunkerUrl and sk.
|
||||
|
||||
NIP46Client (transport over NostrTools.SimplePool)
|
||||
- [ ] [NIP46Client.init(localSk: string, remotePubkey?: string, relays: string[], iframeOrigin?: string)](lite/nostr-login-lite.js:1)
|
||||
- Create [this.pool = new NostrTools.SimplePool()](lite/nostr-login-lite.js:1).
|
||||
- Cache relays; initialize local signer state (seckey, pubkey via [NostrTools.getPublicKey()](lite/nostr-login-lite.js:1)).
|
||||
- If remotePubkey provided, set it; cache iframeOrigin.
|
||||
|
||||
- [ ] [NIP46Client.setUseNip44(use: boolean)](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [NIP46Client.subscribeReplies()](lite/nostr-login-lite.js:1)
|
||||
- sub = pool.sub(relays, [{ kinds:[24133], '#p':[localPubkey] }])
|
||||
- sub.on('event', (ev) => [NIP46Client.onEvent(ev)](lite/nostr-login-lite.js:1)); sub.on('eose', () => {/* keep alive */})
|
||||
|
||||
- [ ] [NIP46Client.onEvent(ev: NostrEvent)](lite/nostr-login-lite.js:1)
|
||||
- Parse/decrypt via [NIP46Client.parseEvent()](lite/nostr-login-lite.js:1); route to response handlers.
|
||||
|
||||
- [ ] [NIP46Client.listen(nostrConnectSecret: string): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
- Await unsolicited reply to local pubkey; accept "ack" or exact secret; return signer’s pubkey; unsubscribe this one-shot subscription.
|
||||
|
||||
- [ ] [NIP46Client.connect(token?: string, perms?: string)](lite/nostr-login-lite.js:1)
|
||||
- [NIP46Client.sendRequest()](lite/nostr-login-lite.js:1) with method "connect" and params [userPubkey, token||'', perms||'']; resolve on ack.
|
||||
|
||||
- [ ] [NIP46Client.initUserPubkey(hint?: string): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
- If hint present, set and return; else call "get_public_key" RPC and store result.
|
||||
|
||||
- [ ] [NIP46Client.sendRequest(remotePubkey: string, method: string, params: string[], kind = 24133, cb?: (res) => void)](lite/nostr-login-lite.js:1)
|
||||
- id = random string
|
||||
- ev = [NIP46Client.createRequestEvent(id, remotePubkey, method, params, kind)](lite/nostr-login-lite.js:1)
|
||||
- pool.publish(relays, ev)
|
||||
- [NIP46Client.setResponseHandler(id, cb)](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [NIP46Client.createRequestEvent(id, remotePubkey, method, params, kind)](lite/nostr-login-lite.js:1)
|
||||
- content = JSON.stringify({ id, method, params })
|
||||
- encrypt content using nip44 unless method==='create_account', otherwise nip04 fallback
|
||||
- tags: [ ['p', remotePubkey] ]
|
||||
- sign via [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1)
|
||||
- return event object
|
||||
|
||||
- [ ] [NIP46Client.parseEvent(event)](lite/nostr-login-lite.js:1)
|
||||
- Detect nip04 vs nip44 by ciphertext shape; decrypt using local seckey and remote pubkey (nip04 or [Nip44](lite/nostr-login-lite.js:1)/window.NostrTools.nip44).
|
||||
- Return { id, method, params, event } or { id, result, error, event }.
|
||||
|
||||
- [ ] [NIP46Client.setResponseHandler(id, cb)](lite/nostr-login-lite.js:1)
|
||||
- Deduplicate 'auth_url' emissions; ensure only one resolution per id; log elapsed; cleanup map entries.
|
||||
|
||||
- [ ] [NIP46Client.setWorkerIframePort(port: MessagePort)](lite/nostr-login-lite.js:1)
|
||||
- When iframe is present, forward signed events via postMessage and map request id↔event.id; send keepalive pings.
|
||||
|
||||
- [ ] [NIP46Client.teardown()](lite/nostr-login-lite.js:1)
|
||||
- Unsubscribe; clear timers/ports; pool.close(relays).
|
||||
|
||||
Iframe handshake
|
||||
- [ ] [IframeReadyListener.start(messages: string[], origin: string)](lite/nostr-login-lite.js:1)
|
||||
- Listen for ["workerReady","workerError"] and ["starterDone","starterError"]; origin-check by host or subdomain.
|
||||
|
||||
- [ ] [IframeReadyListener.wait(): Promise<any>](lite/nostr-login-lite.js:1)
|
||||
|
||||
ExtensionBridge
|
||||
- [ ] [ExtensionBridge.startChecking(nostrLite: NostrLite)](lite/nostr-login-lite.js:1)
|
||||
- Poll window.nostr until found; then call [ExtensionBridge.initExtension()](lite/nostr-login-lite.js:1) once; retry after a delay to capture last extension.
|
||||
|
||||
- [ ] [ExtensionBridge.initExtension(nostrLite: NostrLite, lastTry?: boolean)](lite/nostr-login-lite.js:1)
|
||||
- Cache extension, reassign window.nostr to nostrLite; if currently authed as extension, reconcile; schedule a final late check.
|
||||
|
||||
- [ ] [ExtensionBridge.setExtensionReadPubkey(expectedPubkey?: string)](lite/nostr-login-lite.js:1)
|
||||
- Temporarily set window.nostr = extension; read pubkey; compare to expected; emit extensionLogin/extensionLogout.
|
||||
|
||||
- [ ] [ExtensionBridge.trySetForPubkey(expectedPubkey: string)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [ExtensionBridge.setExtension()](lite/nostr-login-lite.js:1)
|
||||
- [ ] [ExtensionBridge.unset(nostrLite: NostrLite)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [ExtensionBridge.hasExtension(): boolean](lite/nostr-login-lite.js:1)
|
||||
|
||||
Local signer (wrapping window.NostrTools)
|
||||
- [ ] [LocalSigner.constructor(sk: string)](lite/nostr-login-lite.js:1)
|
||||
- Cache public key on construct via [NostrTools.getPublicKey()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [LocalSigner.pubkey(): string](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [LocalSigner.sign(event: NostrEvent): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
- Use [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1), set id/sig/pubkey.
|
||||
|
||||
- [ ] [LocalSigner.encrypt04(pubkey: string, plaintext: string): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
- Use [NostrTools.nip04.encrypt()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [LocalSigner.decrypt04(pubkey: string, ciphertext: string): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
- Use [NostrTools.nip04.decrypt()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [LocalSigner.encrypt44(pubkey: string, plaintext: string): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
- Use window.NostrTools.nip44 if available, else [Nip44.encrypt()](lite/nostr-login-lite.js:1).
|
||||
|
||||
- [ ] [LocalSigner.decrypt44(pubkey: string, ciphertext: string): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
- Use window.NostrTools.nip44 if available, else [Nip44.decrypt()](lite/nostr-login-lite.js:1).
|
||||
|
||||
Store (localStorage helpers)
|
||||
- [ ] [Store.addAccount(info: Info)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Store.removeCurrentAccount()](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Store.getCurrent(): Info | null](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Store.getAccounts(): Info[]](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Store.getRecents(): Info[]](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Store.setItem(key: string, value: string)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Store.getIcon(): Promise<string>](lite/nostr-login-lite.js:1)
|
||||
|
||||
UI (single modal)
|
||||
- [ ] [Modal.init(options)](lite/nostr-login-lite.js:1)
|
||||
- Create modal container; inject minimal CSS; set theme and RTL if required.
|
||||
|
||||
- [ ] [Modal.open(opts: { startScreen?: string })](lite/nostr-login-lite.js:1)
|
||||
- Render options: Connect (with provider list/QR), Extension, Local (Create/Import), Read-only, OTP (if server URLs configured), Switch Account if prior accounts.
|
||||
|
||||
- [ ] [Modal.close()](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [Modal.showAuthUrl(url: string)](lite/nostr-login-lite.js:1)
|
||||
- For connect OAuth-like prompt; present link/QR; if iframeUrl is used, show embedded iframe.
|
||||
|
||||
- [ ] [Modal.showIframeUrl(url: string)](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [Modal.onSwitchAccount(info: Info)](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [Modal.onLogoutConfirm()](lite/nostr-login-lite.js:1)
|
||||
|
||||
- [ ] [Modal.onImportConnectionString(cs: ConnectionString)](lite/nostr-login-lite.js:1)
|
||||
- Import-and-connect flow: calls [Auth.importAndConnect()](lite/nostr-login-lite.js:1).
|
||||
|
||||
Event bus
|
||||
- [ ] [Bus.on(event: string, handler: Function)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Bus.off(event: string, handler: Function)](lite/nostr-login-lite.js:1)
|
||||
- [ ] [Bus.emit(event: string, payload?: any)](lite/nostr-login-lite.js:1)
|
||||
|
||||
Relay configuration helpers
|
||||
- [ ] [Relays.getDefaultRelays(options): string[]](lite/nostr-login-lite.js:1)
|
||||
- From options or sensible defaults.
|
||||
|
||||
- [ ] [Relays.normalize(relays: string[]): string[]](lite/nostr-login-lite.js:1)
|
||||
- Ensure proper wss:// and deduping.
|
||||
|
||||
Parity acceptance criteria
|
||||
- Same external API names and events exported as current initializer: [init](packages/auth/src/index.ts:333), [launch](packages/auth/src/index.ts:333), [logout](packages/auth/src/index.ts:333), [setDarkMode](packages/auth/src/index.ts:333), [setAuth](packages/auth/src/index.ts:333), [cancelNeedAuth](packages/auth/src/index.ts:333).
|
||||
- Same nlAuth payload shape as [onAuth()](packages/auth/src/modules/AuthNostrService.ts:347-418).
|
||||
- window.nostr behavior matches [class Nostr](packages/auth/src/modules/Nostr.ts:23).
|
||||
|
||||
Notes and implementation tips
|
||||
- Load order: [Deps.ensureNostrToolsLoaded()](lite/nostr-login-lite.js:1) must guard all usages of window.NostrTools.
|
||||
- Request de-duplication: mirror [setResponseHandler()](packages/auth/src/modules/Nip46.ts:150-173) behavior to avoid auth_url flooding.
|
||||
- NIP-44 selection: use nip44 for all methods except "create_account".
|
||||
- Iframe origin checks: follow [ReadyListener](packages/auth/src/modules/Nip46.ts:291-338) host/subdomain verification.
|
||||
- Secret handling in nostrconnect: accept 'ack' or exact secret; see [listen](packages/auth/src/modules/Nip46.ts:71-107).
|
||||
- Profile fetch is optional; keep it async and non-blocking, update UI/state when complete.
|
||||
|
||||
Out of scope for initial lite version
|
||||
- Complex banners/popups and multi-window flows.
|
||||
- Full NDK feature parity beyond the minimum for NIP-46 RPC.
|
||||
- Advanced error telemetry; keep console logs minimal.
|
||||
|
||||
Deliverables
|
||||
- Single distributable [lite/nostr-login-lite.js](lite/nostr-login-lite.js) usable via:
|
||||
- <script src="..."></script> with window.nostr present after [init()](lite/nostr-login-lite.js:1).
|
||||
- Optional ESM import with same API.
|
||||
- Minimal HTML example showing each auth method path is functional.
|
||||
203
README.md
203
README.md
@@ -1,167 +1,62 @@
|
||||
Nostr_Login_Lite
|
||||
===========
|
||||
|
||||
This library is a powerful `window.nostr` provider.
|
||||
## Floating Tab API
|
||||
|
||||
```
|
||||
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js'></script>
|
||||
```
|
||||
|
||||
Just add the above script to your HTML and
|
||||
get a nice UI for users to login with Nostr Connect (nip46), with an extension, read-only login,
|
||||
account switching, OAuth-like sign up, etc. Your app just talks to the `window.nostr`, the rest is handled by `nostr-login`.
|
||||
|
||||
See it in action on [nostr.band](https://nostr.band).
|
||||
|
||||
## Options
|
||||
|
||||
You can set these attributes to the `script` tag to customize the behavior:
|
||||
- `data-dark-mode` - `true`/`false`, default will use the browser's color theme
|
||||
- `data-bunkers` - the comma-separated list of domain names of Nostr Connect (nip46) providers for sign up, i.e. `nsec.app,highlighter.com`
|
||||
- `data-perms` - the comma-separated list of [permissions](https://github.com/nostr-protocol/nips/blob/master/46.md#requested-permissions) requested by the app over Nostr Connect, i.e. `sign_event:1,nip04_encrypt`
|
||||
- `data-theme` - color themes, one of `default`, `ocean`, `lemonade`, `purple`
|
||||
- `data-no-banner` - if `true`, do not show the `nostr-login` banner, will need to launch the modals using event dispatch, see below
|
||||
- `data-methods` - comma-separated list of allowed auth methods, method names: `connect`, `extension`, `readOnly`, `local`, all allowed by default.
|
||||
- `data-otp-request-url` - URL for requesting OTP code
|
||||
- `data-otp-reply-url` - URL for replying with OTP code
|
||||
- `data-title` - title for the welcome screen
|
||||
- `data-description` - description for the welcome screen
|
||||
- `data-start-screen` - screen shown by default (banner click, window.nostr.* call), options: `welcome`, `welcome-login`, `welcome-signup`, `signup`, `local-signup`, `login`, `otp`, `connect`, `login-bunker-url`, `login-read-only`, `connection-string`, `switch-account`, `import`
|
||||
- `data-signup-relays` - comma-separated list of relays where nip65 event will be published on local signup
|
||||
- `data-outbox-relays` - comma-separated list of relays that will be added to nip65 event on local signup
|
||||
- `data-signup-nstart` - "true" to use start.njump.me instead of local signup
|
||||
- `data-follow-npubs` - comma-separated list of npubs to follow if njump.me signup is used
|
||||
|
||||
Example:
|
||||
```
|
||||
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js' data-perms="sign_event:1,sign_event:0" data-theme="ocean"></script>
|
||||
```
|
||||
|
||||
## Updating the UI
|
||||
|
||||
Whenever user performs an auth-related action using `nostr-login`, a `nlAuth` event will be dispatched on the `document`, which you can listen
|
||||
to in order to update your UI (show user profile, etc):
|
||||
|
||||
```
|
||||
document.addEventListener('nlAuth', (e) => {
|
||||
// type is login, signup or logout
|
||||
if (e.detail.type === 'login' || e.detail.type === 'signup') {
|
||||
onLogin(); // get pubkey with window.nostr and show user profile
|
||||
} else {
|
||||
onLogout() // clear local user data, hide profile info
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Launching, logout, etc
|
||||
|
||||
The `nostr-login` auth modals will be automatically launched whenever you
|
||||
make a call to `window.nostr` if user isn't authed yet. However, you can also launch the auth flow by dispatching a custom `nlLaunch` event:
|
||||
|
||||
```
|
||||
document.dispatchEvent(new CustomEvent('nlLaunch', { detail: 'welcome' }));
|
||||
```
|
||||
|
||||
The `detail` event payload can be empty, or can be one of `welcome`, `signup`, `login`, `login-bunker-url`, `login-read-only`, `switch-account`.
|
||||
|
||||
To trigger logout in the `nostr-login`, you can dispatch a `nlLogout` event:
|
||||
|
||||
```
|
||||
document.dispatchEvent(new Event("nlLogout"));
|
||||
```
|
||||
|
||||
To change dark mode in the `nostr-login`, you can dispatch a `nlDarkMode` event, with detail as `darkMode` boolean:
|
||||
|
||||
```
|
||||
document.dispatchEvent(new CustomEvent("nlDarkMode", { detail: true }));
|
||||
```
|
||||
|
||||
## Use as a package
|
||||
|
||||
Install `nostr-login` package with `npm` and then:
|
||||
|
||||
```
|
||||
import { init as initNostrLogin } from "nostr-login"
|
||||
|
||||
// make sure this is called before any
|
||||
// window.nostr calls are made
|
||||
initNostrLogin({/*options*/})
|
||||
|
||||
```
|
||||
|
||||
Now the `window.nostr` will be initialized and on your first call
|
||||
to it the auth flow will be launched if user isn't authed yet.
|
||||
|
||||
You can also launch the auth flow yourself:
|
||||
|
||||
```
|
||||
import { launch as launchNostrLoginDialog } from "nostr-login"
|
||||
|
||||
// make sure init() was called
|
||||
|
||||
// on your signup button click
|
||||
function onSignupClick() {
|
||||
// launch signup screen
|
||||
launchNostrLoginDialog({
|
||||
startScreen: 'signup'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js Fix for Server Side Rendering (SSR)
|
||||
|
||||
`nostr-login` calls `document` which is unavailable for server-side rendering. You will have build errors. To fix this, you can import `nostr-login` on the client side in your component with a `useEffect` like this:
|
||||
Configure persistent floating tab for login/logout:
|
||||
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
import('nostr-login')
|
||||
.then(async ({ init }) => {
|
||||
init({
|
||||
// options
|
||||
})
|
||||
})
|
||||
.catch((error) => console.log('Failed to load nostr-login', error));
|
||||
}, []);
|
||||
await NOSTR_LOGIN_LITE.init({
|
||||
floatingTab: {
|
||||
enabled: true,
|
||||
hPosition: 0.95, // 0.0-1.0 or '95%' from left
|
||||
vPosition: 0.5, // 0.0-1.0 or '50%' from top
|
||||
appearance: {
|
||||
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
||||
theme: 'auto', // 'auto', 'light', 'dark'
|
||||
icon: '🔐',
|
||||
text: 'Login'
|
||||
},
|
||||
behavior: {
|
||||
hideWhenAuthenticated: false,
|
||||
showUserInfo: true,
|
||||
autoSlide: true
|
||||
},
|
||||
animation: {
|
||||
slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down'
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
Note: even if your component has `"use client"` in the first line, this fix still may be necessary.
|
||||
|
||||
---
|
||||
Control methods:
|
||||
```javascript
|
||||
NOSTR_LOGIN_LITE.showFloatingTab();
|
||||
NOSTR_LOGIN_LITE.hideFloatingTab();
|
||||
NOSTR_LOGIN_LITE.updateFloatingTab(options);
|
||||
NOSTR_LOGIN_LITE.destroyFloatingTab();
|
||||
```
|
||||
|
||||
API:
|
||||
- `init(opts)` - set mapping of window.nostr to nostr-login
|
||||
- `launch(startScreen)` - launch nostr-login UI
|
||||
- `logout()` - drop the current nip46 connection
|
||||
## Embedded Modal API
|
||||
|
||||
Options:
|
||||
- `theme` - same as `data-theme` above
|
||||
- `startScreen` - same as `startScreen` for `nlLaunch` event above
|
||||
- `bunkers` - same as `data-bunkers` above
|
||||
- `devOverrideBunkerOrigin` - for testing, overrides the bunker origin for local setup
|
||||
- `onAuth: (npub: string, options: NostrLoginAuthOptions)` - a callback to provide instead of listening to `nlAuth` event
|
||||
- `perms` - same as `data-perms` above
|
||||
- `darkMode` - same as `data-dark-mode` above
|
||||
- `noBanner` - same as `data-no-banner` above
|
||||
- `isSignInWithExtension` - `true` to bring the *Sign in with exception* button into main list of options, `false` to hide to the *Advanced*, default will behave as `true` if extension is detected.
|
||||
Embed login interface directly into page element:
|
||||
|
||||
## OTP login
|
||||
```javascript
|
||||
// Initialize library first
|
||||
await NOSTR_LOGIN_LITE.init({
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: true
|
||||
}
|
||||
});
|
||||
|
||||
If you supply both `data-otp-request-url` and `data-otp-reply-url` then "Login with DM" button will appear on the welcome screen.
|
||||
// Embed into container
|
||||
const modal = NOSTR_LOGIN_LITE.embed('#login-container', {
|
||||
title: 'Login',
|
||||
showHeader: true,
|
||||
seamless: false // true = no borders/shadows, blends into page
|
||||
});
|
||||
```
|
||||
|
||||
When user enters their nip05 or npub, a GET request is made to `<data-otp-request-url>[?&]pubkey=<user-pubkey>`. Server should send
|
||||
a DM with one-time code to that pubkey and should return 200.
|
||||
|
||||
After user enters the code, a GET request is made to `<data-otp-reply-url>[?&]pubkey=<user-pubkey>&code=<code>`. Server should check that code matches the pubkey and hasn't expired, and should return 200 status and an optional payload. Nostr-login will deliver the payload as `otpData` field in `nlAuth` event, and will save the payload in localstore and will deliver it again as `nlAuth` on page reload.
|
||||
|
||||
The reply payload may be used to supply the session token. If token is sent by the server as a cookie then payload might be empty, otherwise the payload should be used by the app to extract the token and use it in future API calls to the server.
|
||||
|
||||
## Examples
|
||||
|
||||
* [Basic HTML Example](./examples/usage.html)
|
||||
|
||||
## TODO
|
||||
|
||||
- fetch bunker list using NIP-89
|
||||
- Amber support
|
||||
- allow use without the UIs
|
||||
- add timeout handling
|
||||
- more at [issues](https://github.com/nostrband/nostr-login/issues)
|
||||
Container can be CSS selector or DOM element. Modal renders inline without backdrop overlay.
|
||||
|
||||
50
examples/README.md
Normal file
50
examples/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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.
|
||||
1134
examples/comprehensive-test.html
Normal file
1134
examples/comprehensive-test.html
Normal file
File diff suppressed because it is too large
Load Diff
56
examples/embedded.html
Normal file
56
examples/embedded.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!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({
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: true,
|
||||
connect: true,
|
||||
remote: true,
|
||||
otp: true
|
||||
}
|
||||
});
|
||||
|
||||
window.NOSTR_LOGIN_LITE.embed('#login-container', {
|
||||
seamless: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
481
examples/login-and-profile.html
Normal file
481
examples/login-and-profile.html
Normal file
@@ -0,0 +1,481 @@
|
||||
<!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 - All Login Methods Test</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: 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;
|
||||
}
|
||||
</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>
|
||||
|
||||
<!-- 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;
|
||||
let userPubkey = null;
|
||||
let relayUrl = 'wss://relay.laantungir.net';
|
||||
|
||||
// Initialize NOSTR_LOGIN_LITE
|
||||
async function initializeApp() {
|
||||
log('INFO', 'Initializing NOSTR_LOGIN_LITE...');
|
||||
|
||||
try {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
theme: 'dark',
|
||||
darkMode: false,
|
||||
relays: [relayUrl, 'wss://relay.damus.io'],
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: 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: 0.80, // 95% from left
|
||||
vPosition: 0.01, // 50% from top (center)
|
||||
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';
|
||||
|
||||
} 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';
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// Load user profile
|
||||
async function loadUserProfile() {
|
||||
if (!userPubkey) return;
|
||||
|
||||
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);
|
||||
|
||||
ws.onopen = () => {
|
||||
log('SUCCESS', 'WebSocket connected, requesting profile...');
|
||||
const req = JSON.stringify([
|
||||
'REQ',
|
||||
'profile',
|
||||
{
|
||||
kinds: [0],
|
||||
authors: [userPubkey],
|
||||
limit: 1
|
||||
}
|
||||
]);
|
||||
ws.send(req);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
const [type, subscriptionId, eventData] = message;
|
||||
|
||||
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);
|
||||
|
||||
} catch (error) {
|
||||
log('ERROR', `Profile loading failed: ${error.message}`);
|
||||
document.getElementById('profile-name').textContent = 'Error loading profile';
|
||||
document.getElementById('profile-about').textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Display profile data
|
||||
function displayProfile(profile) {
|
||||
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
|
||||
const about = profile.about || '';
|
||||
const picture = profile.picture || '';
|
||||
|
||||
document.getElementById('profile-name').textContent = name;
|
||||
document.getElementById('profile-about').textContent = about;
|
||||
|
||||
if (picture) {
|
||||
document.getElementById('profile-picture').src = picture;
|
||||
}
|
||||
|
||||
log('SUCCESS', `Profile displayed: ${name}`);
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
log('INFO', 'Logging out...');
|
||||
try {
|
||||
await nlLite.logout();
|
||||
log('SUCCESS', 'Logged out successfully');
|
||||
} catch (error) {
|
||||
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>
|
||||
@@ -1,121 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Minimal Nostr Login Test (Remote)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="user-info" style="display: none;">
|
||||
<div id="user-name"></div>
|
||||
<img id="user-picture" style="display: none;">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load nostr-login script with configuration from your website
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://laantungir.net/nostr-login/unpkg.js';
|
||||
|
||||
// Configure with Laan theme and Welcome Login start screen
|
||||
script.setAttribute('data-theme', 'laan');
|
||||
script.setAttribute('data-start-screen', 'welcome-login');
|
||||
script.setAttribute('data-methods', 'connect,extension,local,readOnly');
|
||||
script.setAttribute('data-no-banner', 'false');
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Wait for script to load, then set up event listeners
|
||||
script.onload = () => {
|
||||
setupEventListeners();
|
||||
};
|
||||
|
||||
function setupEventListeners() {
|
||||
// Listen for authentication events
|
||||
document.addEventListener('nlAuth', (e) => {
|
||||
const { type, pubkey, name } = e.detail;
|
||||
|
||||
if (type === 'login' || type === 'signup') {
|
||||
console.log('User logged in:', e.detail);
|
||||
showUserInfo();
|
||||
// Check for profile updates periodically
|
||||
startProfilePolling();
|
||||
} else if (type === 'logout') {
|
||||
console.log('User logged out');
|
||||
hideUserInfo();
|
||||
stopProfilePolling();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let profileInterval;
|
||||
|
||||
function startProfilePolling() {
|
||||
// Check immediately
|
||||
updateUserProfile();
|
||||
|
||||
// Then check every 2 seconds for profile updates
|
||||
profileInterval = setInterval(updateUserProfile, 2000);
|
||||
|
||||
// Stop checking after 15 seconds (profile should be loaded by then)
|
||||
setTimeout(stopProfilePolling, 15000);
|
||||
}
|
||||
|
||||
function stopProfilePolling() {
|
||||
if (profileInterval) {
|
||||
clearInterval(profileInterval);
|
||||
profileInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserProfile() {
|
||||
try {
|
||||
// Try to get user info from localStorage (current user)
|
||||
const userInfo = JSON.parse(localStorage.getItem('__nostrlogin_nip46') || 'null');
|
||||
if (userInfo) {
|
||||
updateUserDisplay(userInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle errors to avoid interference
|
||||
}
|
||||
}
|
||||
|
||||
function showUserInfo() {
|
||||
const userInfoDiv = document.getElementById('user-info');
|
||||
userInfoDiv.style.display = 'block';
|
||||
updateUserProfile();
|
||||
}
|
||||
|
||||
function updateUserDisplay(userInfo) {
|
||||
const userNameDiv = document.getElementById('user-name');
|
||||
const userPicture = document.getElementById('user-picture');
|
||||
|
||||
// Display name with proper fallback hierarchy
|
||||
const displayName = userInfo.name ||
|
||||
(userInfo.nip05 && userInfo.nip05.split('@')[0]) ||
|
||||
(userInfo.pubkey && userInfo.pubkey.substring(0, 16) + '...') ||
|
||||
'Unknown';
|
||||
|
||||
userNameDiv.textContent = displayName;
|
||||
|
||||
// Update profile picture if available
|
||||
if (userInfo.picture) {
|
||||
userPicture.src = userInfo.picture;
|
||||
userPicture.style.display = 'block';
|
||||
userPicture.style.width = '200px';
|
||||
// userPicture.style.height = '200px';
|
||||
}
|
||||
}
|
||||
|
||||
function hideUserInfo() {
|
||||
const userInfoDiv = document.getElementById('user-info');
|
||||
const userNameDiv = document.getElementById('user-name');
|
||||
const userPicture = document.getElementById('user-picture');
|
||||
|
||||
userInfoDiv.style.display = 'none';
|
||||
userNameDiv.textContent = '';
|
||||
userPicture.src = '';
|
||||
userPicture.style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,121 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Minimal Nostr Login Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="user-info" style="display: none;">
|
||||
<div id="user-name"></div>
|
||||
<img id="user-picture" style="display: none;">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load nostr-login script with configuration
|
||||
const script = document.createElement('script');
|
||||
script.src = '../packages/auth/dist/unpkg.js';
|
||||
|
||||
// Configure with Laan theme and Welcome Login start screen
|
||||
script.setAttribute('data-theme', 'laan');
|
||||
script.setAttribute('data-start-screen', 'welcome-login');
|
||||
script.setAttribute('data-methods', 'connect,extension,local,readOnly');
|
||||
script.setAttribute('data-no-banner', 'false');
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Wait for script to load, then set up event listeners
|
||||
script.onload = () => {
|
||||
setupEventListeners();
|
||||
};
|
||||
|
||||
function setupEventListeners() {
|
||||
// Listen for authentication events
|
||||
document.addEventListener('nlAuth', (e) => {
|
||||
const { type, pubkey, name } = e.detail;
|
||||
|
||||
if (type === 'login' || type === 'signup') {
|
||||
console.log('User logged in:', e.detail);
|
||||
showUserInfo();
|
||||
// Check for profile updates periodically
|
||||
startProfilePolling();
|
||||
} else if (type === 'logout') {
|
||||
console.log('User logged out');
|
||||
hideUserInfo();
|
||||
stopProfilePolling();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let profileInterval;
|
||||
|
||||
function startProfilePolling() {
|
||||
// Check immediately
|
||||
updateUserProfile();
|
||||
|
||||
// Then check every 2 seconds for profile updates
|
||||
profileInterval = setInterval(updateUserProfile, 2000);
|
||||
|
||||
// Stop checking after 15 seconds (profile should be loaded by then)
|
||||
setTimeout(stopProfilePolling, 15000);
|
||||
}
|
||||
|
||||
function stopProfilePolling() {
|
||||
if (profileInterval) {
|
||||
clearInterval(profileInterval);
|
||||
profileInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserProfile() {
|
||||
try {
|
||||
// Try to get user info from localStorage (current user)
|
||||
const userInfo = JSON.parse(localStorage.getItem('__nostrlogin_nip46') || 'null');
|
||||
if (userInfo) {
|
||||
updateUserDisplay(userInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle errors to avoid interference
|
||||
}
|
||||
}
|
||||
|
||||
function showUserInfo() {
|
||||
const userInfoDiv = document.getElementById('user-info');
|
||||
userInfoDiv.style.display = 'block';
|
||||
updateUserProfile();
|
||||
}
|
||||
|
||||
function updateUserDisplay(userInfo) {
|
||||
const userNameDiv = document.getElementById('user-name');
|
||||
const userPicture = document.getElementById('user-picture');
|
||||
|
||||
// Display name with proper fallback hierarchy
|
||||
const displayName = userInfo.name ||
|
||||
(userInfo.nip05 && userInfo.nip05.split('@')[0]) ||
|
||||
(userInfo.pubkey && userInfo.pubkey.substring(0, 16) + '...') ||
|
||||
'Unknown';
|
||||
|
||||
userNameDiv.textContent = displayName;
|
||||
|
||||
// Update profile picture if available
|
||||
if (userInfo.picture) {
|
||||
userPicture.src = userInfo.picture;
|
||||
userPicture.style.display = 'block';
|
||||
userPicture.style.width = '200px';
|
||||
// userPicture.style.height = '200px';
|
||||
}
|
||||
}
|
||||
|
||||
function hideUserInfo() {
|
||||
const userInfoDiv = document.getElementById('user-info');
|
||||
const userNameDiv = document.getElementById('user-name');
|
||||
const userPicture = document.getElementById('user-picture');
|
||||
|
||||
userInfoDiv.style.display = 'none';
|
||||
userNameDiv.textContent = '';
|
||||
userPicture.src = '';
|
||||
userPicture.style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
411
examples/modal-login-demo.html
Normal file
411
examples/modal-login-demo.html
Normal file
@@ -0,0 +1,411 @@
|
||||
<!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>
|
||||
@@ -1,838 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="" class="has-dark-text" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>من الجميل في نوستر هي قدرة…</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://blossom.npubpro.com/9d9f5663fcc8ec87f256d3959fcd763ae234774d7cfcaf85a5ad10e3adea63e3.css"
|
||||
/>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* The script for calculating the color contrast was taken from
|
||||
https://gomakethings.com/dynamically-changing-the-text-color-based-on-background-color-contrast-with-vanilla-js/ */
|
||||
var accentColor = getComputedStyle(
|
||||
document.documentElement,
|
||||
).getPropertyValue('--background-color')
|
||||
accentColor = accentColor.trim().slice(1)
|
||||
var r = parseInt(accentColor.substr(0, 2), 16)
|
||||
var g = parseInt(accentColor.substr(2, 2), 16)
|
||||
var b = parseInt(accentColor.substr(4, 2), 16)
|
||||
var yiq = (r * 299 + g * 587 + b * 114) / 1000
|
||||
var textColor = yiq >= 128 ? 'dark' : 'light'
|
||||
|
||||
document.documentElement.className = `has-${textColor}-text`
|
||||
</script>
|
||||
|
||||
<meta property="og:title" content="من الجميل في نوستر هي قدرة…" />
|
||||
<meta name="twitter:title" content="من الجميل في نوستر هي قدرة…" />
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="من الجميل في نوستر هي قدرة التطبيقات على التخاطر.
|
||||
|
||||
إذا بتفضلوا استخدام مساحة مثل الDiscord، ضيفوا صفحتنا على flotilla.social للتواصل بصيغة التشات.
|
||||
|
||||
اضغط إضافة مساحة Add Space،
|
||||
ثم الدخول على مساحة Join a space،
|
||||
وضيفوا ريلاي relay.nostrarabia.com
|
||||
|
||||
نراكم. 🫡
|
||||
|
||||
#nostrarabia…"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="من الجميل في نوستر هي قدرة التطبيقات على التخاطر.
|
||||
|
||||
إذا بتفضلوا استخدام مساحة مثل الDiscord، ضيفوا صفحتنا على flotilla.social للتواصل بصيغة التشات.
|
||||
|
||||
اضغط إضافة مساحة Add Space،
|
||||
ثم الدخول على مساحة Join a space،
|
||||
وضيفوا ريلاي relay.nostrarabia.com
|
||||
|
||||
نراكم. 🫡
|
||||
|
||||
#nostrarabia…"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="من الجميل في نوستر هي قدرة التطبيقات على التخاطر.
|
||||
|
||||
إذا بتفضلوا استخدام مساحة مثل الDiscord، ضيفوا صفحتنا على flotilla.social للتواصل بصيغة التشات.
|
||||
|
||||
اضغط إضافة مساحة Add Space،
|
||||
ثم الدخول على مساحة Join a space،
|
||||
وضيفوا ريلاي relay.nostrarabia.com
|
||||
|
||||
نراكم. 🫡
|
||||
|
||||
#nostrarabia…"
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://image.nostr.build/4afea744d3f72f4f7af1f7d632a8594159ef65f49b48cb00890c3904c57e1822.jpg"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://image.nostr.build/4afea744d3f72f4f7af1f7d632a8594159ef65f49b48cb00890c3904c57e1822.jpg"
|
||||
/>
|
||||
<meta name="twitter:image:alt" content="من الجميل في نوستر هي قدرة…" />
|
||||
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://nostrarabia.npub.pro/post/note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn/"
|
||||
/>
|
||||
<link
|
||||
rel="og:url"
|
||||
href="https://nostrarabia.npub.pro/post/note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn/"
|
||||
/>
|
||||
<meta property="og:site_name" content="نوستر" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@nostrprotocol" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<!--
|
||||
***********************
|
||||
Powered by npub.pro
|
||||
***********************
|
||||
-->
|
||||
|
||||
<meta
|
||||
name="nostr:site"
|
||||
content="naddr1qqxkummnw3shywnzxg6kyv34qythwumn8ghj7un9d3shjtnwwp6kyurjduhxxmmdqgsgzt8ltta9m8pmgzptstrsxew06tf8nsn64yfrzwu0e07kt3q2a6crqsqqqaesldfhew"
|
||||
/>
|
||||
<meta name="author" content="نوستر بالعربي" />
|
||||
<meta
|
||||
name="nostr:author"
|
||||
content="npub1syk07kh6tkwrksyzhqk8qdjul5kj08p842gjxyacljlavhzq4m4slmdu3p"
|
||||
/>
|
||||
<meta
|
||||
name="nostr:id"
|
||||
content="note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn"
|
||||
/>
|
||||
<meta
|
||||
name="nostr:event_id"
|
||||
content="note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn"
|
||||
/>
|
||||
<link
|
||||
rel="manifest"
|
||||
href="https://nostrarabia.npub.pro/manifest.webmanifest"
|
||||
/>
|
||||
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.5.1.min.js"
|
||||
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
as="style"
|
||||
href="https://cdn.jsdelivr.net/npm/venobox@2.1.8/dist/venobox.min.css"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
href="https://image.nostr.build/4afea744d3f72f4f7af1f7d632a8594159ef65f49b48cb00890c3904c57e1822.jpg"
|
||||
type="image/jpeg"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="https://image.nostr.build/4afea744d3f72f4f7af1f7d632a8594159ef65f49b48cb00890c3904c57e1822.jpg"
|
||||
/>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<script>
|
||||
document.documentElement.setAttribute('dir', 'rtl')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.np-oembed-video-link {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.np-oembed-video-link img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.np-oembed-video-link:after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAJeUlEQVR4nO1daYwUVRD+9oBlV9kFJESjixxq8GAl3lEU1EXiwQ+RQwUPvIi3xgMVD7wxCgYTDwQFlxANakw0Jho1iqJoBEQMIuqqYHA9dleRBXZhZExpTeyU1T09M/1ed8/0l7xkd2be1a/7Vb2qr6qBBAkSJEiQoHjRA8AwABMA3AFgMYDlANYAaAbQDqCLSzt/Rt99xL+9g+sO47YS5IhKAIcDmAbgLQDbAaQDKjsBrAAwE0AjgKpkdXSU8wVqArAlwAXIVqiv5wCczGMoefTnu/VHi4uQdik0hgeBf8ZUctgfwDMAdvi8WM0AXgXwMIBLAZwA4BAAAwH0BtCNS2/+jL4bwb+lOq9xG376Ilk0H8B+KAEMAvA8gFSWi7IRwAIA5wHYJ8D+qa3zASz08VSmWCmgBS460N17LYAOjwuwmWUIyZIyS+Mi5WEOgFaPcW0DMKOYFIBTAHztMeG1fNdWhzjGah7DWo9xruebJdbqK91Zf7lMcDVfhApEB2UAxgD4xGXMu/iJ6o6YYYDHpDYBmIjo4xweqzYHOpjui5jgVAC/uxzKZgPoifigFsCjPHY5H7IKjEbEcb6LKksy5DDEF4cD+EaZF811MiKKa1zkxcsAeiH+6MkquyZXbkLEcJ/LFnU5ig9Xumxh9yIiuEoZXCeAsShejAGwVZn3DWEPbJKyTZFAH47ix9EAflO2rwvD1KZ2KJpHA0oHDTxnKeita19kEW1TTAyl8GRoT4o0CbXzWcyaXeojxRB3JkoXZyiC/hNbJ/rZijArRm0qCOWGTP9GMYoFl7PTF0x3GiO8qAh58kYaAZmfvxIdfsPmhQT/HR7XK1YKI6b7O5WDX5DmkNMAfAngAwBnIb44UnHA3WbC07dNdEKyJChUKAbJd5m6E0fMEXPZGrTWJW04mwK22vZ1MXOnADwNoB/ihToALWIui4Jq/ADlEQzan+G2IGkufwC4MWaOoUnKFj84iIafFQ2vMuDz3iPLgqQdSsR4xAcrxfjpaS/4RC7NIyaMhn1EH9sVjc5Z3mKqT9QxXqEY1RfS4EzR4FpDDL/eiumhG4DrFFuRcwt4gre7qIKu1Tox7gcKaUxyl8gjaAK9RD+/O77ryxfejcvVzgtHCxhFXCjGuyHfm3qUwpuqgTmtJC2EuMQQAK97bGNfR1S+VPN8nGM9KZ+GmkQj82AOtcriu6ExC3/qbQBDES0sEGOk/3NCd4WFTnxaU+gp+vrTx/huUu68TNnBhzNSFqKAk5QbLqct9niFa2uS3rm76G+Lz3r9WJV0I+O1sg+cSHtholzheB2XSwMzRGU6i5jEbqK/jhzrHwjgDY9tjFTo0xEupAi4PZfKS0VlYqGbRI1i+8mXfNCc5fxyMMLBFDGWd3LRCjpF5SBDAtz6TDsKGTLzBcUR3uoRiUWHs1khcMX6K4dfXzGPw0RFuuNMo4cy2EKxF2+1bvLlVwBTLZO9vxdj8EUImSgqUeSSaVSJPjsDbJt8Nu97bGPrmEFjA/IcNS4fR5RxvzCrsWmxrQQNki/feSzMa0FZY3PgI0z3U2mxqHQJzKObco4wJaum8TnH6/xClgMTmCr6I80rKz4UlSh40jQqFcOhSezNDiNJ2MiUnwFcbMCQeqLoh9zVWfGFqGRDTawQfaZgB0cAWOaxja0K2ELRINr/3E+lH0QlG5FC5aLPv2APZWyU/CGLfCFeQaEYKNolmZYVMiKVvHk2LkraUWgrsQ2yFtyjkDmcqvjYgD2jRNbOii5RyZYfOy1KWOjP5D9tUcimZ129D2tBdol+bcWqawfjpR5nFusLEsaWBeVEXQ67IM/kkx6eyZ+Y6W59ywpDqIe5IJUALlOCb+TZpDYsoR6G2gvlzqyw0OdIVj29rMMHha32LgvhYAiFbtTNYF8DFKa6s6xnrjGicDAMw3Ria0Gq2XTiZprvMJxoJi/TSRjGRRva3RjF/O0899DF2RMRNC6GYX6H4hSrCqhdog+96bE9UQ7GYxFh8/uhITiooJyQqwNQMR/3UGNbmMRm87wjn9Chfr138m4tiI/qE1tFn/mS8kg7u0KJEs6ULt6GbUd+7auYYnzvAu9ZopAGvSAjfKixYZEcLsqX5EC4q1CmXR7oEH2SsS8X/0aTh38jCmEMi/IR6BkMF5V/tLDX/in69BOhVc03j5Z3JM1t3hyBQJ9yNr04x5aTItFduUCmD4ibRX/Z9vhxHj6MXZwgmZgnUcDJhVJJwRNyNkK5bKOwIEOysBRX5krTtICFYoyUszhnNFoMR4BCnK5TgnrmuOSqSrOV+tqIJdUEXzO525ANLdIBO1DConvz5xWcusMtt+4OZiKaYopEJmAHnAvdRkgblNC1PkwwWO2xPb3BW1icQtruL6TBesXGZCrLQptCLEh7qLFkm4o6JohxdwbBk54vGv3MkArc5rEAmbKVrbFxeDFLmRIW/VQQDQ9WBOnZCB6tHgtBauySmL1K4jwxh1SQb1yQPpKfDNiCfo2ANTYo1DH7MWffRy6+YGmNpYzPQWKFaP8Xdo7F8c03j4m5dJh4uqeLTnZyxucg3ZstfB6ZHePky0cpJv9bTHRUpSTnCjqBWVkEAjQLQS8lpO5Lk7a0RsWiSsI2wb94SVFG6Mk3ikcUwUuhx6WOa5Tr8pCNjiuVGJJUkacWz4YxytHgY5v5V+qVc8M2TjhQajhG8cdYTaScAaXTTlKN4382uC5O3BPaa4G0ZPzHl3Ay/gvCHtiVijDrjHmqVz8yQwvsuR4RwT3K4HZy+u1i1KZSynzvRsRwtUvWhFcczqY4o9YlsiqSrzzKYLLLS8G+5YhXxNgc0qzMiwT4uYg4Rrv4NlKGg/JNoI4NhdoW1RqmNpUPbXK5izm9hZMMhxVD6Adl7M+Q2akz5UNL9Fqrr15dE8FXr5azBvVpllevRjUDqm+SmFdS5HXMzKgJcYw1nGBMEhKk1da4odD267vdIpcyppclfIdWWHoaiDI71yMZjdOPXzSv73ZiAJOO3chuaYeLuImfnP4By7YpPAbJtdXOUU0x8+MXRJyYp1CM0i7le44+msUhzCM5onUQ87e6c+nDnzXwb6Zyndc9wtk0VXZuQLlNYod6Jo5t9Hmx0gbLBh6L6fySsUA5JxteqJCuTZbNnJdxZEzJFFZQweSJaRzxtD3ABdjJ7JaZ7I4OO24klqhimTCemS+LOLnBajZntPG+38V/N/N3y1goT+dYkoZi1ZQSJEiQIEECwt/UKk4KqlBwIwAAAABJRU5ErkJggg==');
|
||||
background-size: cover;
|
||||
content: '';
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
:root {
|
||||
--ghost-accent-color: #139ad4;
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.npubpro.com/maptalks.css" />
|
||||
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="نوستر"
|
||||
href="https://nostrarabia.npub.pro/rss/"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300..800&display=swap"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300..800&display=swap"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="script"
|
||||
href="https://blossom.npubpro.com/64f7c3c5de348a7d1a5c7d1519abfa33fec8c5442c583fda441d25cd7b5990cf.js"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="style"
|
||||
href="https://blossom.npubpro.com/8c397e35e20c7e4c6e163f0d17511294a2fba1c1a13823b91e2c220c3c38e83b.css"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="style"
|
||||
href="https://blossom.npubpro.com/9d9f5663fcc8ec87f256d3959fcd763ae234774d7cfcaf85a5ad10e3adea63e3.css"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="script"
|
||||
href="https://blossom.npubpro.com/b8292724b60caed6133f097c3f0427163e93e87724da84861192e1322d4146f5.js"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="script"
|
||||
href="https://blossom.npubpro.com/b866f2b384af7668032c0c2fd523717520537bf812de9870cf1a960b11087333.js"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="style"
|
||||
href="https://blossom.npubpro.com/eb267981d379bed63595b7ebd1dd6cb775c912a6f9b0f16c2faee5050afbf381.css"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="post-template tag-nostrarabia is-head-left-logo has-parallax-feed is-dropdown-loaded"
|
||||
style="overflow: initial"
|
||||
>
|
||||
<div class="gh-site">
|
||||
<header id="gh-head" class="gh-head gh-outer">
|
||||
<div class="gh-head-inner gh-inner">
|
||||
<div class="gh-head-brand">
|
||||
<div class="gh-head-brand-wrapper">
|
||||
<a
|
||||
class="gh-head-logo"
|
||||
href="https://nostrarabia.npub.pro"
|
||||
>
|
||||
<img
|
||||
src="https://image.nostr.build/4afea744d3f72f4f7af1f7d632a8594159ef65f49b48cb00890c3904c57e1822.jpg"
|
||||
alt="نوستر"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
class="gh-search gh-icon-btn"
|
||||
aria-label="Search this site"
|
||||
data-ghost-search=""
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.5 17.5L12.5 12.5L17.5 17.5ZM14.1667 8.33333C14.1667 9.09938 14.0158 9.85792 13.7226 10.5657C13.4295 11.2734 12.9998 11.9164 12.4581 12.4581C11.9164 12.9998 11.2734 13.4295 10.5657 13.7226C9.85792 14.0158 9.09938 14.1667 8.33333 14.1667C7.56729 14.1667 6.80875 14.0158 6.10101 13.7226C5.39328 13.4295 4.75022 12.9998 4.20854 12.4581C3.66687 11.9164 3.23719 11.2734 2.94404 10.5657C2.65088 9.85792 2.5 9.09938 2.5 8.33333C2.5 6.78624 3.11458 5.30251 4.20854 4.20854C5.30251 3.11458 6.78624 2.5 8.33333 2.5C9.88043 2.5 11.3642 3.11458 12.4581 4.20854C13.5521 5.30251 14.1667 6.78624 14.1667 8.33333Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="gh-burger"></button>
|
||||
</div>
|
||||
|
||||
<nav class="gh-head-menu">
|
||||
<ul class="nav">
|
||||
<li class="nav-alreysyh">
|
||||
<a href="https://nostrarabia.npub.pro/"
|
||||
>الرئيسية</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-nwstr-arby">
|
||||
<a
|
||||
href="https://nostrarabia.npub.pro/tag/nostrarabia/"
|
||||
>نوستر عربي</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-talm-albytkwyn">
|
||||
<a
|
||||
href="https://nostrarabia.npub.pro/tag/learnbitcoin/"
|
||||
>تعلّم البيتكوين</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-talm-alaqtsad-alnmsawy">
|
||||
<a
|
||||
href="https://nostrarabia.npub.pro/tag/Learnaustrianeconomics/"
|
||||
>تعلّم الاقتصاد النمساوي</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-alaadhaa">
|
||||
<a
|
||||
href="https://nostrarabia.npub.pro/tag/community/"
|
||||
>الأعضاء</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-akhbar">
|
||||
<a href="https://nostrarabia.npub.pro/tag/news/"
|
||||
>أخبار</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<button
|
||||
class="nav-more-toggle gh-icon-btn"
|
||||
aria-label="More"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M21.333 16c0-1.473 1.194-2.667 2.667-2.667v0c1.473 0 2.667 1.194 2.667 2.667v0c0 1.473-1.194 2.667-2.667 2.667v0c-1.473 0-2.667-1.194-2.667-2.667v0zM13.333 16c0-1.473 1.194-2.667 2.667-2.667v0c1.473 0 2.667 1.194 2.667 2.667v0c0 1.473-1.194 2.667-2.667 2.667v0c-1.473 0-2.667-1.194-2.667-2.667v0zM5.333 16c0-1.473 1.194-2.667 2.667-2.667v0c1.473 0 2.667 1.194 2.667 2.667v0c0 1.473-1.194 2.667-2.667 2.667v0c-1.473 0-2.667-1.194-2.667-2.667v0z"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="gh-dropdown">
|
||||
<li class="nav-mymz">
|
||||
<a
|
||||
href="https://nostrarabia.npub.pro/tag/memes/"
|
||||
>ميمز</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-adwat">
|
||||
<a
|
||||
href="https://nostrarabia.npub.pro/tag/tools/"
|
||||
>أدوات</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-idamna">
|
||||
<a
|
||||
href="https://nostrarabia.npub.pro/tag/supportus/"
|
||||
>إدعمنا</a
|
||||
>
|
||||
</li>
|
||||
</div>
|
||||
</button>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="gh-head-actions">
|
||||
<button
|
||||
class="gh-search gh-icon-btn"
|
||||
aria-label="Search this site"
|
||||
data-ghost-search=""
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.5 17.5L12.5 12.5L17.5 17.5ZM14.1667 8.33333C14.1667 9.09938 14.0158 9.85792 13.7226 10.5657C13.4295 11.2734 12.9998 11.9164 12.4581 12.4581C11.9164 12.9998 11.2734 13.4295 10.5657 13.7226C9.85792 14.0158 9.09938 14.1667 8.33333 14.1667C7.56729 14.1667 6.80875 14.0158 6.10101 13.7226C5.39328 13.4295 4.75022 12.9998 4.20854 12.4581C3.66687 11.9164 3.23719 11.2734 2.94404 10.5657C2.65088 9.85792 2.5 9.09938 2.5 8.33333C2.5 6.78624 3.11458 5.30251 4.20854 4.20854C5.30251 3.11458 6.78624 2.5 8.33333 2.5C9.88043 2.5 11.3642 3.11458 12.4581 4.20854C13.5521 5.30251 14.1667 6.78624 14.1667 8.33333Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="gh-main gh-outer">
|
||||
<div class="gh-inner">
|
||||
<article class="gh-article post tag-nostrarabia no-image">
|
||||
<section class="gh-content gh-canvas">
|
||||
<np-content
|
||||
event="note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn"
|
||||
><p>
|
||||
من الجميل في نوستر هي قدرة التطبيقات على
|
||||
التخاطر.<br /><br />إذا بتفضلوا استخدام
|
||||
مساحة مثل الDiscord، ضيفوا صفحتنا على
|
||||
flotilla.social للتواصل بصيغة التشات.
|
||||
<br /><br />اضغط إضافة مساحة Add Space،<br />ثم
|
||||
الدخول على مساحة Join a space،<br />وضيفوا
|
||||
ريلاي relay.nostrarabia.com<br /><br />نراكم.
|
||||
🫡<br /><br /><a href="/tag/nostrarabia/"
|
||||
>#nostrarabia</a
|
||||
>
|
||||
</p>
|
||||
</np-content>
|
||||
<style>
|
||||
np-map {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
np-content p {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
np-content h1 {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
np-content h2 {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
np-content h3 {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
np-content h4 {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
np-content h5 {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
np-content h6 {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
<np-content-cta
|
||||
data-cta-list="zap,like,share"
|
||||
data-cta-main="zap"
|
||||
data-button-color="#139ad4"
|
||||
data-text-button-color="#fff"
|
||||
data-event-addr="note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn"
|
||||
data-event-id="note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn"
|
||||
data-author-npub="npub1syk07kh6tkwrksyzhqk8qdjul5kj08p842gjxyacljlavhzq4m4slmdu3p"
|
||||
id="np-content-cta"
|
||||
></np-content-cta>
|
||||
<div
|
||||
style="display: none"
|
||||
id="zap-button"
|
||||
data-anon=""
|
||||
data-npub="npub1syk07kh6tkwrksyzhqk8qdjul5kj08p842gjxyacljlavhzq4m4slmdu3p"
|
||||
data-note-id="note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn"
|
||||
data-relays="wss://purplepag.es/,wss://user.kindpag.es/,wss://relay.nos.social/,wss://relay.primal.net/,wss://relay.damus.io/,wss://relay.nostrarabia.com/"
|
||||
data-button-color="#139ad4"
|
||||
data-amount=""
|
||||
></div>
|
||||
<np-content-comments
|
||||
data-id="note1dplcmt2885l9tsy6c4xenm3w7hrg92af42sz76p0ypd7ft0muvpqg80sqn"
|
||||
data-relays="wss://purplepag.es/,wss://user.kindpag.es/,wss://relay.nos.social/,wss://relay.primal.net/,wss://relay.damus.io/,wss://relay.nostrarabia.com/"
|
||||
data-client="nostrarabia.npub.pro"
|
||||
></np-content-comments
|
||||
><np-content-dm
|
||||
data-peer-npub="npub1syk07kh6tkwrksyzhqk8qdjul5kj08p842gjxyacljlavhzq4m4slmdu3p"
|
||||
data-relays="wss://purplepag.es/,wss://user.kindpag.es/,wss://relay.nos.social/,wss://relay.primal.net/,wss://relay.damus.io/,wss://relay.nostrarabia.com/"
|
||||
></np-content-dm>
|
||||
<header class="gh-article-header">
|
||||
<h1 class="gh-article-title">
|
||||
من الجميل في نوستر هي قدرة…
|
||||
</h1>
|
||||
</header>
|
||||
<aside class="gh-article-meta">
|
||||
<div class="gh-article-meta-inner">
|
||||
<figure class="gh-author-image">
|
||||
<img
|
||||
src="https://mir-s3-cdn-cf.behance.net/projects/404/01ffa529646977.55fca90cee60d.jpg"
|
||||
alt="نوستر بالعربي"
|
||||
/>
|
||||
</figure>
|
||||
<div class="gh-article-meta-wrapper">
|
||||
<h4 class="gh-author-name">
|
||||
<a
|
||||
href="/author/npub1syk07kh6tkwrksyzhqk8qdjul5kj08p842gjxyacljlavhzq4m4slmdu3p/"
|
||||
>نوستر بالعربي</a
|
||||
>
|
||||
</h4>
|
||||
<time
|
||||
class="gh-article-date"
|
||||
datetime="2024-12-01"
|
||||
>Dec 01, 2024</time
|
||||
>
|
||||
</div>
|
||||
<!-- <a
|
||||
class="gh-article-tag"
|
||||
href="/tag/nostrarabia/"
|
||||
style="--tag-color:"
|
||||
>nostrarabia</a
|
||||
> -->
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<footer class="gh-article-footer gh-canvas">
|
||||
<nav class="gh-navigation">
|
||||
<div class="gh-navigation-previous">
|
||||
<a
|
||||
class="gh-navigation-link"
|
||||
href="/post/1733010811046/"
|
||||
>← Previous</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="gh-navigation-middle"></div>
|
||||
|
||||
<div class="gh-navigation-next">
|
||||
<a
|
||||
class="gh-navigation-link"
|
||||
href="/post/1733278250434/"
|
||||
>Next →</a
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="gh-foot gh-outer">
|
||||
<div class="gh-foot-inner gh-inner">
|
||||
<nav class="gh-foot-menu"></nav>
|
||||
|
||||
<div class="gh-copyright">
|
||||
نوستر © 2024. Powered by
|
||||
<a
|
||||
href="https://npub.pro/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Npub.pro</a
|
||||
>. Theme by
|
||||
<a
|
||||
href="https://ghost.org/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Ghost</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="pswp__bg"></div>
|
||||
|
||||
<div class="pswp__scroll-wrap">
|
||||
<div class="pswp__container">
|
||||
<div class="pswp__item"></div>
|
||||
<div class="pswp__item"></div>
|
||||
<div class="pswp__item"></div>
|
||||
</div>
|
||||
|
||||
<div class="pswp__ui pswp__ui--hidden">
|
||||
<div class="pswp__top-bar">
|
||||
<div class="pswp__counter"></div>
|
||||
|
||||
<button
|
||||
class="pswp__button pswp__button--close"
|
||||
title="Close (Esc)"
|
||||
></button>
|
||||
<button
|
||||
class="pswp__button pswp__button--share"
|
||||
title="Share"
|
||||
></button>
|
||||
<button
|
||||
class="pswp__button pswp__button--fs"
|
||||
title="Toggle fullscreen"
|
||||
></button>
|
||||
<button
|
||||
class="pswp__button pswp__button--zoom"
|
||||
title="Zoom in/out"
|
||||
></button>
|
||||
|
||||
<div class="pswp__preloader">
|
||||
<div class="pswp__preloader__icn">
|
||||
<div class="pswp__preloader__cut">
|
||||
<div class="pswp__preloader__donut"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap"
|
||||
>
|
||||
<div class="pswp__share-tooltip"></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="pswp__button pswp__button--arrow--left"
|
||||
title="Previous (arrow left)"
|
||||
></button>
|
||||
<button
|
||||
class="pswp__button pswp__button--arrow--right"
|
||||
title="Next (arrow right)"
|
||||
></button>
|
||||
|
||||
<div class="pswp__caption">
|
||||
<div class="pswp__caption__center"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://blossom.npubpro.com/64f7c3c5de348a7d1a5c7d1519abfa33fec8c5442c583fda441d25cd7b5990cf.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/venobox@2.1.8/dist/venobox.min.css"
|
||||
type="text/css"
|
||||
media="screen"
|
||||
/>
|
||||
<script>
|
||||
;(() => {
|
||||
const script = document.createElement('script')
|
||||
script.async = true
|
||||
script.type = 'text/javascript'
|
||||
script.src =
|
||||
'https://cdn.jsdelivr.net/npm/venobox@2.1.8/dist/venobox.min.js'
|
||||
script.onload = () => {
|
||||
new VenoBox({
|
||||
selector: '.vbx-media',
|
||||
spinColor: '#139ad4',
|
||||
overlayColor: '#139ad4',
|
||||
})
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
})()
|
||||
</script>
|
||||
<script
|
||||
async=""
|
||||
type="text/javascript"
|
||||
src="https://cdn.jsdelivr.net/npm/venobox@2.1.8/dist/venobox.min.js"
|
||||
></script>
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
async=""
|
||||
src="https://cdn.npubpro.com/zapthreads.iife.0.6.0.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
async=""
|
||||
src="https://cdn.npubpro.com/nostr-site-zapthreads.1.0.2.js"
|
||||
></script>
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
async=""
|
||||
src="https://cdn.npubpro.com/content-cta.iife.1.0.22.js"
|
||||
></script>
|
||||
|
||||
<script
|
||||
async=""
|
||||
src="../packages/auth/dist/unpkg.js"
|
||||
data-perms="sign_event:1,sign_event:7,sign_event:3,sign_event:9734,sign_event:10003,sign_event:9802,nip04_encrypt,nip04_decrypt"
|
||||
data-start-screen="local-signup"
|
||||
data-signup-relays="wss://relay.primal.net/,wss://relay.damus.io/,wss://relay.nostrarabia.com/"
|
||||
></script>
|
||||
<script>
|
||||
;(async () => {
|
||||
if (!window.nostrSite)
|
||||
await new Promise((ok) =>
|
||||
document.addEventListener('npLoad', ok),
|
||||
)
|
||||
const ep = window.nostrSite.plugins.register('nostr-login')
|
||||
document.addEventListener('nlAuth', async (e) => {
|
||||
console.log('nlAuth', e)
|
||||
ep.dispatch('auth', {
|
||||
type: e.detail.type,
|
||||
pubkey: e.detail.pubkey,
|
||||
})
|
||||
|
||||
if (
|
||||
e.detail.type === 'login' ||
|
||||
e.detail.type === 'signup'
|
||||
) {
|
||||
window.__nlAuthed = true
|
||||
} else {
|
||||
window.__nlAuthed = false
|
||||
}
|
||||
|
||||
const npub = window.nostrSite.nostrTools.nip19.npubEncode(
|
||||
await window.nostr.getPublicKey(),
|
||||
)
|
||||
const zapButton = document.querySelector('#zap-button')
|
||||
if (zapButton) {
|
||||
if (window.__nlAuthed)
|
||||
zapButton.setAttribute('data-anon', '')
|
||||
else zapButton.setAttribute('data-anon', 'true')
|
||||
}
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.npubpro.com/nostr-zap.0.22.2.js"></script>
|
||||
<div></div>
|
||||
<np-content-cta-selection></np-content-cta-selection>
|
||||
<script>
|
||||
;(async () => {
|
||||
if (!window.nostrSite)
|
||||
await new Promise((ok) =>
|
||||
document.addEventListener('npLoad', ok),
|
||||
)
|
||||
const ep = window.nostrSite.plugins.register('nostr-zap')
|
||||
console.log('nostr-zap ep', ep)
|
||||
ep.subscribe('action-zap', (amount) => {
|
||||
const button = document.querySelector('#zap-button')
|
||||
button.setAttribute('data-amount', amount || '')
|
||||
button.dispatchEvent(new Event('click'))
|
||||
})
|
||||
document.addEventListener('nostr-zap-published', (e) => {
|
||||
console.log('nostr-zap on zap published', e)
|
||||
ep.dispatch('event-published', e.detail)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
|
||||
<script
|
||||
async=""
|
||||
src="https://unpkg.com/nostr-site-search@1.0.12/dist/index.js"
|
||||
></script>
|
||||
<script>
|
||||
document.addEventListener('np-search-goto', (e) => {
|
||||
console.log('np-search-goto', e)
|
||||
window.location.href = e.detail
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
async=""
|
||||
src="https://cdn.npubpro.com/embeds.iife.1.0.4.js"
|
||||
></script>
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.npubpro.com/maptalks.min.js"
|
||||
></script>
|
||||
<script>
|
||||
const container = document.querySelector('np-map')
|
||||
console.log('map', container)
|
||||
if (container) {
|
||||
const coords = container
|
||||
.getAttribute('coords')
|
||||
.split(',')
|
||||
.map((c) => Number(c))
|
||||
console.log('coords', coords)
|
||||
const div = document.createElement('div')
|
||||
div.style.width = '100%'
|
||||
div.style.height = '300px'
|
||||
container.append(div)
|
||||
const map = new maptalks.Map(div, {
|
||||
center: coords,
|
||||
zoom: 15,
|
||||
zoomControl: true, // add zoom control
|
||||
scaleControl: true, // add scale control
|
||||
baseLayer: new maptalks.TileLayer('base', {
|
||||
// urlTemplate: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
|
||||
urlTemplate:
|
||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
subdomains: ['a', 'b', 'c'], // "d"
|
||||
attribution:
|
||||
'© <a href="http://osm.org">OpenStreetMap</a>',
|
||||
}),
|
||||
})
|
||||
|
||||
const point = new maptalks.Marker(coords, {
|
||||
visible: true,
|
||||
editable: false,
|
||||
cursor: 'pointer',
|
||||
draggable: false,
|
||||
// symbol : {
|
||||
// 'textFaceName' : 'sans-serif',
|
||||
// 'textName' : 'MapTalks',
|
||||
// 'textFill' : '#34495e',
|
||||
// 'textHorizontalAlignment' : 'right',
|
||||
// 'textSize' : 40
|
||||
// }
|
||||
})
|
||||
point.on('click touchend', (e) => console.log(e))
|
||||
|
||||
new maptalks.VectorLayer('vector', point).addTo(map)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.npubpro.com/index.js"
|
||||
onload="window.nostrSite.startTab();"
|
||||
></script>
|
||||
|
||||
<style>
|
||||
#pwa-toast {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 16px;
|
||||
padding: 12px;
|
||||
border: 1px solid #8885;
|
||||
border-radius: 4px;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
box-shadow: 3px 4px 5px 0 #8885;
|
||||
display: grid;
|
||||
background-color: #fff;
|
||||
}
|
||||
#pwa-toast .message {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#pwa-toast .buttons {
|
||||
display: flex;
|
||||
}
|
||||
#pwa-toast button {
|
||||
border: 1px solid #8885;
|
||||
outline: none;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
#pwa-toast.show {
|
||||
visibility: visible;
|
||||
}
|
||||
button#pwa-refresh {
|
||||
display: none;
|
||||
}
|
||||
#pwa-toast.show.refresh button#pwa-refresh {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<iframe
|
||||
width="0"
|
||||
height="0"
|
||||
border="0"
|
||||
id="__nostr-login-worker-iframe-3-use-nsec-app"
|
||||
style="display: none"
|
||||
src="https://3.use.nsec.app/iframe"
|
||||
></iframe
|
||||
><deepl-input-controller></deepl-input-controller>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,343 +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 Interactive Test</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.controls { background: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.control-group { margin-bottom: 15px; }
|
||||
.control-group label { display: inline-block; width: 150px; font-weight: bold; }
|
||||
.control-group input, .control-group select { margin-left: 10px; padding: 5px; }
|
||||
.checkbox-group { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.checkbox-group label { width: auto; margin-right: 15px; }
|
||||
.current-config { background: #e8f4fd; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
|
||||
.login-area { background: #fff; border: 2px solid #ddd; padding: 20px; border-radius: 8px; }
|
||||
button { padding: 10px 15px; margin: 5px; cursor: pointer; }
|
||||
.apply-btn { background: #007cba; color: white; border: none; border-radius: 5px; }
|
||||
.reset-btn { background: #999; color: white; border: none; border-radius: 5px; }
|
||||
.note { background: #fff3cd; padding: 10px; border-radius: 5px; margin-top: 10px; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Nostr Login Interactive Test</h1>
|
||||
|
||||
<!-- Controls Section -->
|
||||
<div class="controls">
|
||||
<h3>Configuration Options</h3>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Theme:</label>
|
||||
<select id="themeSelect">
|
||||
<option value="default">Default</option>
|
||||
<option value="ocean">Ocean</option>
|
||||
<option value="lemonade">Lemonade</option>
|
||||
<option value="purple">Purple</option>
|
||||
<option value="laan">Laan</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Dark Mode:</label>
|
||||
<input type="checkbox" id="darkModeCheck">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Start Screen:</label>
|
||||
<select id="startScreenSelect">
|
||||
<option value="">Default</option>
|
||||
<option value="welcome">Welcome</option>
|
||||
<option value="welcome-login">Welcome Login</option>
|
||||
<option value="welcome-signup">Welcome Signup</option>
|
||||
<option value="signup">Signup</option>
|
||||
<option value="local-signup">Local Signup</option>
|
||||
<option value="login">Login</option>
|
||||
<option value="connect">Connect</option>
|
||||
<option value="login-bunker-url">Login Bunker URL</option>
|
||||
<option value="login-read-only">Login Read Only</option>
|
||||
<option value="switch-account">Switch Account</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Auth Methods:</label>
|
||||
<div class="checkbox-group">
|
||||
<label><input type="checkbox" value="connect" checked> Connect (NIP-46)</label>
|
||||
<label><input type="checkbox" value="extension" checked> Extension</label>
|
||||
<label><input type="checkbox" value="readOnly" checked> Read Only</label>
|
||||
<label><input type="checkbox" value="local" checked> Local</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>No Banner:</label>
|
||||
<input type="checkbox" id="noBannerCheck">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Bunkers:</label>
|
||||
<input type="text" id="bunkersInput" placeholder="e.g., nsec.app,highlighter.com" style="width: 300px;">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Permissions:</label>
|
||||
<input type="text" id="permsInput" placeholder="e.g., sign_event:1,nip04_encrypt" style="width: 300px;">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Title:</label>
|
||||
<input type="text" id="titleInput" placeholder="Custom welcome title" style="width: 300px;">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Description:</label>
|
||||
<input type="text" id="descriptionInput" placeholder="Custom welcome description" style="width: 300px;">
|
||||
</div>
|
||||
|
||||
<button class="apply-btn" onclick="applyConfig()">Apply Configuration</button>
|
||||
<button class="reset-btn" onclick="resetConfig()">Reset to Defaults</button>
|
||||
|
||||
<div class="note">
|
||||
<strong>Note:</strong> Configuration changes will be applied by reloading the page with new settings.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Configuration Display -->
|
||||
<div class="current-config">
|
||||
<h4>Current Configuration:</h4>
|
||||
<pre id="configDisplay"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Login/User Section -->
|
||||
<div class="login-area">
|
||||
<!-- Login Section -->
|
||||
<div id="loginSection">
|
||||
<h3>Test Login</h3>
|
||||
<button id="loginBtn">Login with Nostr</button>
|
||||
<button onclick="launchSpecificScreen()">Launch with Start Screen</button>
|
||||
</div>
|
||||
|
||||
<!-- User Info Section (hidden initially) -->
|
||||
<div id="userSection" style="display: none;">
|
||||
<h3>Welcome!</h3>
|
||||
<p><strong>Your Public Key (hex):</strong></p>
|
||||
<div id="pubkeyHex" style="word-break: break-all; background: #f0f0f0; padding: 10px; border-radius: 4px;"></div>
|
||||
<br>
|
||||
<button id="logoutBtn">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic script loading with configuration -->
|
||||
<script>
|
||||
let currentConfig = {};
|
||||
let scriptLoaded = false;
|
||||
|
||||
// Load nostr-login with current configuration
|
||||
function loadNostrLogin() {
|
||||
if (scriptLoaded) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = '../packages/auth/dist/unpkg.js';
|
||||
|
||||
// Apply configuration as data attributes
|
||||
const config = getStoredConfig();
|
||||
if (config.theme) script.setAttribute('data-theme', config.theme);
|
||||
if (config.darkMode !== undefined) script.setAttribute('data-dark-mode', config.darkMode);
|
||||
if (config.startScreen) script.setAttribute('data-start-screen', config.startScreen);
|
||||
if (config.noBanner !== undefined) script.setAttribute('data-no-banner', config.noBanner);
|
||||
if (config.bunkers) script.setAttribute('data-bunkers', config.bunkers);
|
||||
if (config.perms) script.setAttribute('data-perms', config.perms);
|
||||
if (config.title) script.setAttribute('data-title', config.title);
|
||||
if (config.description) script.setAttribute('data-description', config.description);
|
||||
if (config.methods && config.methods.length > 0) {
|
||||
script.setAttribute('data-methods', config.methods.join(','));
|
||||
}
|
||||
|
||||
document.head.appendChild(script);
|
||||
scriptLoaded = true;
|
||||
|
||||
// Wait for script to load, then set up event listeners
|
||||
script.onload = () => {
|
||||
setupEventListeners();
|
||||
};
|
||||
}
|
||||
|
||||
// Get stored configuration from localStorage
|
||||
function getStoredConfig() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('nostrLoginConfig') || '{}');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Store configuration in localStorage
|
||||
function storeConfig(config) {
|
||||
localStorage.setItem('nostrLoginConfig', JSON.stringify(config));
|
||||
}
|
||||
|
||||
// Listen for nostr-login auth events
|
||||
function setupEventListeners() {
|
||||
document.addEventListener('nlAuth', async (e) => {
|
||||
console.log('nlAuth event:', e.detail);
|
||||
if (e.detail.type === 'login' || e.detail.type === 'signup') {
|
||||
await showUserInfo();
|
||||
} else if (e.detail.type === 'logout') {
|
||||
showLoginSection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get and display user info
|
||||
async function showUserInfo() {
|
||||
try {
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
console.log('Got pubkey:', pubkey);
|
||||
|
||||
// Display pubkey
|
||||
document.getElementById('pubkeyHex').textContent = pubkey;
|
||||
|
||||
// Hide login section, show user section
|
||||
document.getElementById('loginSection').style.display = 'none';
|
||||
document.getElementById('userSection').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to get pubkey:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Show login section
|
||||
function showLoginSection() {
|
||||
document.getElementById('loginSection').style.display = 'block';
|
||||
document.getElementById('userSection').style.display = 'none';
|
||||
document.getElementById('pubkeyHex').textContent = '';
|
||||
}
|
||||
|
||||
// Apply configuration
|
||||
function applyConfig() {
|
||||
// Collect all settings
|
||||
const config = {
|
||||
theme: document.getElementById('themeSelect').value,
|
||||
darkMode: document.getElementById('darkModeCheck').checked,
|
||||
startScreen: document.getElementById('startScreenSelect').value,
|
||||
noBanner: document.getElementById('noBannerCheck').checked,
|
||||
bunkers: document.getElementById('bunkersInput').value,
|
||||
perms: document.getElementById('permsInput').value,
|
||||
title: document.getElementById('titleInput').value,
|
||||
description: document.getElementById('descriptionInput').value,
|
||||
methods: Array.from(document.querySelectorAll('input[type="checkbox"][value]'))
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.value)
|
||||
};
|
||||
|
||||
// Remove empty values
|
||||
Object.keys(config).forEach(key => {
|
||||
if (config[key] === '' || (Array.isArray(config[key]) && config[key].length === 0)) {
|
||||
delete config[key];
|
||||
}
|
||||
});
|
||||
|
||||
currentConfig = config;
|
||||
storeConfig(config);
|
||||
updateConfigDisplay();
|
||||
|
||||
console.log('Applied config:', config);
|
||||
|
||||
// Reload page to apply new configuration
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Reset configuration
|
||||
function resetConfig() {
|
||||
document.getElementById('themeSelect').value = 'default';
|
||||
document.getElementById('darkModeCheck').checked = false;
|
||||
document.getElementById('startScreenSelect').value = '';
|
||||
document.getElementById('noBannerCheck').checked = false;
|
||||
document.getElementById('bunkersInput').value = '';
|
||||
document.getElementById('permsInput').value = '';
|
||||
document.getElementById('titleInput').value = '';
|
||||
document.getElementById('descriptionInput').value = '';
|
||||
|
||||
// Reset auth methods to all checked
|
||||
document.querySelectorAll('input[type="checkbox"][value]').forEach(cb => cb.checked = true);
|
||||
|
||||
currentConfig = {};
|
||||
localStorage.removeItem('nostrLoginConfig');
|
||||
updateConfigDisplay();
|
||||
}
|
||||
|
||||
// Update config display
|
||||
function updateConfigDisplay() {
|
||||
document.getElementById('configDisplay').textContent = JSON.stringify(currentConfig, null, 2);
|
||||
}
|
||||
|
||||
// Launch with specific screen
|
||||
function launchSpecificScreen() {
|
||||
const startScreen = document.getElementById('startScreenSelect').value || 'welcome';
|
||||
document.dispatchEvent(new CustomEvent('nlLaunch', { detail: startScreen }));
|
||||
}
|
||||
|
||||
// Load configuration from storage and update UI
|
||||
function loadConfigIntoUI() {
|
||||
const config = getStoredConfig();
|
||||
currentConfig = config;
|
||||
|
||||
// Set default to laan theme for testing
|
||||
if (config.theme) document.getElementById('themeSelect').value = config.theme;
|
||||
else document.getElementById('themeSelect').value = 'laan';
|
||||
|
||||
if (config.darkMode !== undefined) document.getElementById('darkModeCheck').checked = config.darkMode;
|
||||
if (config.startScreen) document.getElementById('startScreenSelect').value = config.startScreen;
|
||||
if (config.noBanner !== undefined) document.getElementById('noBannerCheck').checked = config.noBanner;
|
||||
if (config.bunkers) document.getElementById('bunkersInput').value = config.bunkers;
|
||||
if (config.perms) document.getElementById('permsInput').value = config.perms;
|
||||
if (config.title) document.getElementById('titleInput').value = config.title;
|
||||
if (config.description) document.getElementById('descriptionInput').value = config.description;
|
||||
|
||||
// Load auth methods
|
||||
document.querySelectorAll('input[type="checkbox"][value]').forEach(cb => {
|
||||
cb.checked = !config.methods || config.methods.includes(cb.value);
|
||||
});
|
||||
|
||||
updateConfigDisplay();
|
||||
}
|
||||
|
||||
// Handle login button click
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Load configuration into UI
|
||||
loadConfigIntoUI();
|
||||
|
||||
// Load nostr-login with current config
|
||||
loadNostrLogin();
|
||||
|
||||
document.getElementById('loginBtn').addEventListener('click', () => {
|
||||
// Trigger nostr-login modal
|
||||
document.dispatchEvent(new CustomEvent('nlLaunch', { detail: 'welcome' }));
|
||||
});
|
||||
|
||||
// Handle logout button click
|
||||
document.getElementById('logoutBtn').addEventListener('click', () => {
|
||||
// Trigger logout
|
||||
document.dispatchEvent(new Event('nlLogout'));
|
||||
});
|
||||
|
||||
// Update config display when inputs change
|
||||
document.querySelectorAll('input, select').forEach(input => {
|
||||
input.addEventListener('change', () => {
|
||||
// Auto-apply dark mode for immediate feedback
|
||||
if (input.id === 'darkModeCheck') {
|
||||
document.dispatchEvent(new CustomEvent('nlDarkMode', { detail: input.checked }));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,135 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Input and Output Fields</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
}
|
||||
input, button, textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px; /* Adds space between the buttons */
|
||||
}
|
||||
.button-container button {
|
||||
flex: 1 1 calc(50% - 10px); /* Makes each button take up half the width minus the gap */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Try Nostr-Login</h1>
|
||||
<textarea id="inputField" rows="4" placeholder="Paste your json formatted event here"></textarea>
|
||||
<label>Output:</label>
|
||||
<textarea id="outputField" rows="4" placeholder="Output will be shown here" readonly></textarea>
|
||||
<button onclick="fillExampleData()">Fill with example event</button>
|
||||
<button onclick="SignEventFn()">Sign Event</button>
|
||||
<div class="button-container">
|
||||
<button onclick="cryptWithNIP04('encrypt')">Encrypt with NIP-04</button>
|
||||
<button onclick="cryptWithNIP04('decrypt')">Decrypt with NIP-04</button>
|
||||
<button onclick="cryptWithNIP44('encrypt')">Encrypt with NIP-44</button>
|
||||
<button onclick="cryptWithNIP44('decrypt')">Decrypt with NIP-44</button>
|
||||
</div>
|
||||
<button onclick="switchOutputInput()">Switch Output Input</button>
|
||||
</div>
|
||||
|
||||
<form id="testpw" action="/usage.html" method="POST">
|
||||
<input type="text" name="name" value="npub">
|
||||
<input type="password" name="password" value="a;h123UIFBEKSDBF,SA">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<script>
|
||||
const form = document.querySelector("#testpw");
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
// Example Nostr event as json string
|
||||
var ExampleData = `{ "content": "hello world", "created_at": 1731313613, "id": "", "kind": 1, "pubkey": "568ad8bf00ed530eb44614e4b363271f36f6b645700630470c51f98e7e58fbf0", "tags": [] }`
|
||||
|
||||
// Function to get public key
|
||||
async function getPublicKey() {
|
||||
return await window.nostr.getPublicKey();
|
||||
}
|
||||
|
||||
// Function to encrypt with NIP-04
|
||||
async function cryptWithNIP04(encryptType) {
|
||||
try {
|
||||
var publicKey = await getPublicKey();
|
||||
var data = document.getElementById('inputField').value;
|
||||
var result;
|
||||
if (encryptType === "encrypt") {
|
||||
result = await window.nostr.nip04.encrypt(publicKey, data);
|
||||
} else if (encryptType === "decrypt") {
|
||||
result = await window.nostr.nip04.decrypt(publicKey, data);
|
||||
}
|
||||
document.getElementById('outputField').value = result;
|
||||
} catch (error) {
|
||||
console.error("Error in cryptWithNIP04:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to encrypt with NIP-44
|
||||
async function cryptWithNIP44(encryptType) {
|
||||
try {
|
||||
var publicKey = await getPublicKey();
|
||||
var data = document.getElementById('inputField').value;
|
||||
var result;
|
||||
if (encryptType === "encrypt") {
|
||||
result = await window.nostr.nip44.encrypt(publicKey, data);
|
||||
} else if (encryptType === "decrypt") {
|
||||
result = await window.nostr.nip44.decrypt(publicKey, data);
|
||||
}
|
||||
document.getElementById('outputField').value = result;
|
||||
} catch (error) {
|
||||
console.error("Error in cryptWithNIP44:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sign nostr event
|
||||
async function SignEventFn() {
|
||||
try {
|
||||
var input = document.getElementById('inputField').value;
|
||||
// Convert the text from input to json object
|
||||
var json_data = JSON.parse(input);
|
||||
// Sign the event object using the plugin signEvent method
|
||||
var signedEvent = await window.nostr.signEvent(json_data);
|
||||
document.getElementById('outputField').value = JSON.stringify(signedEvent, null, 2);
|
||||
} catch (error) {
|
||||
console.error("Error in SignEventFn:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill example data for easy testing
|
||||
function fillExampleData() {
|
||||
document.getElementById('outputField').value = '';
|
||||
document.getElementById('inputField').value = ExampleData;
|
||||
}
|
||||
|
||||
// Switch input and output fields
|
||||
function switchOutputInput() {
|
||||
var tempOutput = document.getElementById('outputField').value;
|
||||
document.getElementById('inputField').value = tempOutput;
|
||||
document.getElementById('outputField').value = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "1.0.1",
|
||||
"npmClient": "npm"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
1346
lite/build.js
Normal file
1346
lite/build.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Clean bundler for NOSTR_LOGIN_LITE
|
||||
* Removes problematic files and recreates bundle
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function createCleanBundle() {
|
||||
// First, remove the old bundle if it exists
|
||||
const outputPath = path.join(__dirname, 'nostr-login-lite.bundle.js');
|
||||
try {
|
||||
if (fs.existsSync(outputPath)) {
|
||||
fs.unlinkSync(outputPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No old bundle to remove');
|
||||
}
|
||||
|
||||
const mainFile = path.join(__dirname, 'nostr-login-lite.js');
|
||||
const nip46File = path.join(__dirname, 'core/nip46-client.js');
|
||||
const modalFile = path.join(__dirname, 'ui/modal.js');
|
||||
|
||||
// Start with a clean header
|
||||
let bundle = `/**
|
||||
* NOSTR_LOGIN_LITE
|
||||
* Single-file Nostr authentication library
|
||||
* Generated on: ${new Date().toISOString()}
|
||||
*/
|
||||
|
||||
`;
|
||||
|
||||
// Add section markers and combine files
|
||||
const files = [
|
||||
{ path: modalFile, name: 'modal.js' },
|
||||
{ path: nip46File, name: 'nip46-client.js' },
|
||||
{ path: mainFile, name: 'nostr-login-lite.js' }
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
if (fs.existsSync(file.path)) {
|
||||
const content = fs.readFileSync(file.path, 'utf8');
|
||||
|
||||
bundle += `\n// ======================================\n`;
|
||||
bundle += `// ${file.name}\n`;
|
||||
bundle += `// ======================================\n\n`;
|
||||
|
||||
// Clean the content by removing initial header comments
|
||||
let lines = content.split('\n');
|
||||
let contentStartIndex = 0;
|
||||
|
||||
// Skip the first 10 lines if they contain file headers
|
||||
for (let i = 0; i < Math.min(10, lines.length); i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('/**') || line.startsWith('*') || line.startsWith('/*') || line.startsWith('//') ||
|
||||
line.includes('Copyright') || line.includes('@license') || line.includes('Licensed') || line.includes('©')) {
|
||||
contentStartIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentStartIndex > 0) {
|
||||
lines = lines.slice(contentStartIndex);
|
||||
}
|
||||
|
||||
bundle += lines.join('\n');
|
||||
bundle += '\n\n';
|
||||
|
||||
console.log(`Added ${file.name}`);
|
||||
} else {
|
||||
console.warn(`File not found: ${file.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the bundled file
|
||||
fs.writeFileSync(outputPath, bundle, 'utf8');
|
||||
|
||||
const sizeKB = (bundle.length / 1024).toFixed(2);
|
||||
console.log(`\n✅ Clean bundle created: ${outputPath}`);
|
||||
console.log(`📏 Bundle size: ${sizeKB} KB`);
|
||||
console.log(`📄 Total lines: ${bundle.split('\n').length}`);
|
||||
|
||||
// Verify the bundle starts correctly
|
||||
const firstLines = bundle.split('\n').slice(0, 20).join('\n');
|
||||
console.log('\n📋 First 20 lines:');
|
||||
console.log(firstLines);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { createCleanBundle };
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (typeof require !== 'undefined' && require.main === module) {
|
||||
createCleanBundle().catch(console.error);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Simple bundler for NOSTR_LOGIN_LITE
|
||||
* Combines all files into a single distributable script
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function bundleLite() {
|
||||
const mainFile = path.join(__dirname, 'nostr-login-lite.js');
|
||||
const nip46File = path.join(__dirname, 'core/nip46-client.js');
|
||||
const modalFile = path.join(__dirname, 'ui/modal.js');
|
||||
|
||||
let bundle = `/**
|
||||
* NOSTR_LOGIN_LITE
|
||||
* Single-file Nostr authentication library
|
||||
* Generated on: ${new Date().toISOString()}
|
||||
*/
|
||||
|
||||
// ======================================
|
||||
// Core Classes and Components
|
||||
// ======================================
|
||||
`;
|
||||
|
||||
// Read and combine files
|
||||
const files = [modalFile, nip46File, mainFile];
|
||||
|
||||
for (const file of files) {
|
||||
if (fs.existsSync(file)) {
|
||||
let content = fs.readFileSync(file, 'utf8');
|
||||
|
||||
// Skip the initial comment and license if present
|
||||
let lines = content.split('\n');
|
||||
|
||||
// Find and skip complete JSDoc blocks at the beginning
|
||||
let skipUntil = 0;
|
||||
let inJSDocBlock = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('/**')) {
|
||||
inJSDocBlock = true;
|
||||
skipUntil = i;
|
||||
} else if (inJSDocBlock && line.startsWith('*/')) {
|
||||
skipUntil = i;
|
||||
break;
|
||||
} else if (i < 10 && (line.startsWith('const') || line.startsWith('class') || line.startsWith('function'))) {
|
||||
// Hit actual code before finding end of JSDoc block
|
||||
inJSDocBlock = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (inJSDocBlock) {
|
||||
lines = lines.slice(skipUntil + 1); // Skip the entire JSDoc block
|
||||
} else {
|
||||
// Fallback to old filtering (skip comment-like lines in first 10)
|
||||
lines = lines.filter((line, index) => {
|
||||
return index >= 10 || !line.trim().startsWith('*') && !line.trim().startsWith('//');
|
||||
});
|
||||
}
|
||||
|
||||
bundle += '\n// ======================================\n';
|
||||
bundle += `// ${path.basename(file)}\n`;
|
||||
bundle += '// ======================================\n\n';
|
||||
bundle += lines.join('\n');
|
||||
bundle += '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Write the bundled file
|
||||
const outputPath = path.join(__dirname, 'nostr-login-lite.bundle.js');
|
||||
fs.writeFileSync(outputPath, bundle);
|
||||
|
||||
console.log('Bundle created:', outputPath);
|
||||
console.log('Bundle size:', (bundle.length / 1024).toFixed(2), 'KB');
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { bundleLite };
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (typeof require !== 'undefined' && require.main === module) {
|
||||
bundleLite().catch(console.error);
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
/**
|
||||
* NOSTR NIP-46 Client Implementation
|
||||
* Minimal RPC over NostrTools.SimplePool for NOSTR_LOGIN_LITE
|
||||
*/
|
||||
|
||||
class NIP46Client {
|
||||
constructor() {
|
||||
this.pool = null;
|
||||
this.localSk = null;
|
||||
this.localPk = null;
|
||||
this.remotePk = null;
|
||||
this.relays = [];
|
||||
this.sub = null;
|
||||
this.pendingRequests = {};
|
||||
this.useNip44 = false;
|
||||
this.iframeOrigin = null;
|
||||
this.iframePort = null;
|
||||
}
|
||||
|
||||
init(localSk, remotePk, relays, iframeOrigin) {
|
||||
// Create SimplePool
|
||||
this.pool = new window.NostrTools.SimplePool();
|
||||
|
||||
// Setup keys
|
||||
this.localSk = localSk;
|
||||
if (this.localSk) {
|
||||
this.localPk = window.NostrTools.getPublicKey(this.localSk);
|
||||
}
|
||||
|
||||
this.remotePk = remotePk;
|
||||
this.relays = [...relays];
|
||||
|
||||
// Store iframe origin for future use
|
||||
this.iframeOrigin = iframeOrigin;
|
||||
|
||||
console.log('NIP46Client initialized for', this.remotePk ? 'remote signer' : 'listening mode');
|
||||
}
|
||||
|
||||
setUseNip44(use) {
|
||||
this.useNip44 = use;
|
||||
}
|
||||
|
||||
subscribeReplies() {
|
||||
if (!this.pool || !this.localPk) return;
|
||||
|
||||
// Subscribe to replies to our pubkey on kind 24133 (NIP-46 methods)
|
||||
this.sub = this.pool.sub(this.relays, [{
|
||||
kinds: [24133],
|
||||
'#p': [this.localPk]
|
||||
}]);
|
||||
|
||||
this.sub.on('event', (event) => this.onEvent(event));
|
||||
this.sub.on('eose', () => {
|
||||
console.log('NIP-46 subscription caught up');
|
||||
});
|
||||
|
||||
console.log('Subscribed to NIP-46 replies on relays:', this.relays);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
if (this.sub) {
|
||||
this.sub.unsub();
|
||||
this.sub = null;
|
||||
}
|
||||
}
|
||||
|
||||
async onEvent(event) {
|
||||
console.log('NIP-46 event received:', event);
|
||||
|
||||
try {
|
||||
const parsed = await this.parseEvent(event);
|
||||
if (parsed) {
|
||||
if (parsed.id && this.pendingRequests[parsed.id]) {
|
||||
// Handle response
|
||||
const handler = this.pendingRequests[parsed.id];
|
||||
delete this.pendingRequests[parsed.id];
|
||||
|
||||
if (parsed.result !== undefined) {
|
||||
handler.resolve(parsed.result);
|
||||
} else if (parsed.error) {
|
||||
handler.reject(new Error(parsed.error));
|
||||
} else {
|
||||
handler.reject(new Error('Invalid response format'));
|
||||
}
|
||||
} else if (parsed.method === 'auth_url') {
|
||||
// Handle auth_url emissions (deduplication required)
|
||||
this.emitAuthUrlIfNeeded(parsed.params[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing NIP-46 event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
emitAuthUrlIfNeeded(url) {
|
||||
// Deduplicate auth_url emissions - only emit if not recently shown
|
||||
const lastUrl = sessionStorage.getItem('nl-last-auth-url');
|
||||
if (lastUrl === url) {
|
||||
console.log('Auth URL already shown, skipping duplicate:', url);
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('nl-last-auth-url', url);
|
||||
console.log('New auth URL:', url);
|
||||
|
||||
// Emit event for UI
|
||||
window.dispatchEvent(new CustomEvent('nlAuthUrl', { detail: { url } }));
|
||||
}
|
||||
|
||||
async parseEvent(event) {
|
||||
try {
|
||||
let content = event.content;
|
||||
|
||||
// Determine encryption method based on content structure
|
||||
if (content.length > 44) {
|
||||
// Likely NIP-44 (encrypted)
|
||||
if (this.localSk && event.pubkey) {
|
||||
try {
|
||||
content = window.NostrTools.nip44?.decrypt(this.localSk, event.pubkey, content);
|
||||
} catch (e) {
|
||||
console.warn('NIP-44 decryption failed, trying NIP-04...');
|
||||
content = await window.NostrTools.nip04.decrypt(this.localSk, event.pubkey, content);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Likely NIP-04
|
||||
if (this.localSk && event.pubkey) {
|
||||
content = await window.NostrTools.nip04.decrypt(this.localSk, event.pubkey, content);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = JSON.parse(content);
|
||||
console.log('Decrypted NIP-46 payload:', payload);
|
||||
|
||||
return {
|
||||
id: payload.id,
|
||||
method: payload.method,
|
||||
params: payload.params,
|
||||
result: payload.result,
|
||||
error: payload.error,
|
||||
event: event
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error('Failed to parse event:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async listen(nostrConnectSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.localPk) {
|
||||
reject(new Error('No local pubkey available for listening'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to unsolicited events to our pubkey
|
||||
let foundSecretOrAck = false;
|
||||
|
||||
const listenSub = this.pool.sub(this.relays, [{
|
||||
kinds: [24133],
|
||||
'#p': [this.localPk]
|
||||
}]);
|
||||
|
||||
listenSub.on('event', async (event) => {
|
||||
try {
|
||||
const parsed = await this.parseEvent(event);
|
||||
if (parsed && parsed.method === 'connect') {
|
||||
// Accept if it's an ack or matches our secret
|
||||
const [userPubkey, token] = parsed.params || [];
|
||||
|
||||
if (token === '' && parsed.result === 'ack') {
|
||||
// Ack received
|
||||
foundSecretOrAck = true;
|
||||
listenSub.unsub();
|
||||
resolve(event.pubkey);
|
||||
} else if (token === nostrConnectSecret) {
|
||||
// Secret match
|
||||
foundSecretOrAck = true;
|
||||
listenSub.unsub();
|
||||
resolve(event.pubkey);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in listen mode:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout after 5 minutes
|
||||
setTimeout(() => {
|
||||
if (!foundSecretOrAck) {
|
||||
listenSub.unsub();
|
||||
reject(new Error('Listen timeout - no signer connected'));
|
||||
}
|
||||
}, 300000);
|
||||
});
|
||||
}
|
||||
|
||||
async connect(token, perms) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const result = await this.sendRequest(
|
||||
this.remotePk,
|
||||
'connect',
|
||||
[this.localPk, token || '', perms || ''],
|
||||
24133,
|
||||
(response) => {
|
||||
if (response === 'ack') {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('Connection not acknowledged'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Set 30 second timeout
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 30000);
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async initUserPubkey(hint) {
|
||||
if (hint) {
|
||||
this.remotePk = hint;
|
||||
return hint;
|
||||
}
|
||||
|
||||
if (!this.remotePk) {
|
||||
// Request get_public_key
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const pubkey = await this.sendRequest(
|
||||
this.remotePk,
|
||||
'get_public_key',
|
||||
[],
|
||||
24133
|
||||
);
|
||||
this.remotePk = pubkey;
|
||||
resolve(pubkey);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.remotePk;
|
||||
}
|
||||
|
||||
async sendRequest(remotePubkey, method, params, kind = 24133, cb) {
|
||||
if (!this.pool || !this.localSk || !this.localPk) {
|
||||
throw new Error('NIP46Client not properly initialized');
|
||||
}
|
||||
|
||||
if (!remotePubkey) {
|
||||
throw new Error('No remote pubkey specified');
|
||||
}
|
||||
|
||||
const id = this._generateId();
|
||||
|
||||
// Create request event
|
||||
const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
|
||||
|
||||
console.log('Sending NIP-46 request:', { id, method, params });
|
||||
|
||||
// Publish to relays
|
||||
const pubs = await this.pool.publish(this.relays, event);
|
||||
console.log('Published to relays, waiting for response...');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Set timeout
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('NIP-46 request timeout for id:', id);
|
||||
delete this.pendingRequests[id];
|
||||
reject(new Error(`Request timeout for ${method}`));
|
||||
}, 60000); // 1 minute timeout
|
||||
|
||||
// Store handler
|
||||
this.pendingRequests[id] = {
|
||||
resolve: (result) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(result);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// If callback provided, override resolve
|
||||
if (cb) {
|
||||
const originalResolve = this.pendingRequests[id].resolve;
|
||||
this.pendingRequests[id].resolve = (result) => {
|
||||
cb(result);
|
||||
originalResolve(result);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createRequestEvent(id, remotePubkey, method, params, kind = 24133) {
|
||||
let content = JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params
|
||||
});
|
||||
|
||||
// Choose encryption method
|
||||
let encrypted = content;
|
||||
if (method !== 'create_account') {
|
||||
// Use NIP-44 for non-account creation methods if available
|
||||
if (this.useNip44 && window.NostrTools.nip44) {
|
||||
encrypted = window.NostrTools.nip44.encrypt(this.localSk, remotePubkey, content);
|
||||
} else {
|
||||
// Fallback to NIP-04
|
||||
encrypted = await window.NostrTools.nip04.encrypt(this.localSk, remotePubkey, content);
|
||||
}
|
||||
}
|
||||
|
||||
// Create event structure
|
||||
const event = {
|
||||
kind: kind,
|
||||
content: encrypted,
|
||||
tags: [
|
||||
['p', remotePubkey]
|
||||
],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: this.localPk,
|
||||
id: '', // Will be set by finalizeEvent
|
||||
sig: '' // Will be set by finalizeEvent
|
||||
};
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = window.NostrTools.finalizeEvent(event, this.localSk);
|
||||
|
||||
return signedEvent;
|
||||
}
|
||||
|
||||
_generateId() {
|
||||
return 'nl-' + Date.now() + '-' + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
setWorkerIframePort(port) {
|
||||
this.iframePort = port;
|
||||
|
||||
// Set up postMessage routing if needed
|
||||
if (this.iframePort && this.iframeOrigin) {
|
||||
this.iframePort.onmessage = (event) => {
|
||||
if (event.origin !== this.iframeOrigin) {
|
||||
console.warn('Ignoring message from unknown origin:', event.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Received iframe message:', event.data);
|
||||
// Handle iframe messages
|
||||
};
|
||||
|
||||
// Send keepalive
|
||||
setInterval(() => {
|
||||
if (this.iframePort) {
|
||||
try {
|
||||
this.iframePort.postMessage({ type: 'ping' });
|
||||
} catch (e) {
|
||||
console.warn('Iframe port closed');
|
||||
this.iframePort = null;
|
||||
}
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
}
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.unsubscribe();
|
||||
|
||||
if (this.iframePort) {
|
||||
try {
|
||||
this.iframePort.close();
|
||||
} catch (e) {
|
||||
console.warn('Error closing iframe port:', e);
|
||||
}
|
||||
this.iframePort = null;
|
||||
}
|
||||
|
||||
if (this.pool) {
|
||||
this.pool.close(this.relays);
|
||||
this.pool = null;
|
||||
}
|
||||
|
||||
// Clear all pending requests
|
||||
for (const id in this.pendingRequests) {
|
||||
this.pendingRequests[id].reject(new Error('Client teardown'));
|
||||
}
|
||||
this.pendingRequests = {};
|
||||
}
|
||||
}
|
||||
2619
lite/nostr-lite.js
Normal file
2619
lite/nostr-lite.js
Normal file
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
224
lite/ui/modal.js
224
lite/ui/modal.js
@@ -4,11 +4,13 @@
|
||||
*/
|
||||
|
||||
class Modal {
|
||||
constructor(options) {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.container = null;
|
||||
this.isVisible = false;
|
||||
this.currentScreen = null;
|
||||
this.isEmbedded = !!options.embedded;
|
||||
this.embeddedContainer = options.embedded;
|
||||
|
||||
// Initialize modal container and styles
|
||||
this._initModal();
|
||||
@@ -17,7 +19,18 @@ class Modal {
|
||||
_initModal() {
|
||||
// Create modal container
|
||||
this.container = document.createElement('div');
|
||||
this.container.id = 'nl-modal';
|
||||
this.container.id = this.isEmbedded ? 'nl-modal-embedded' : 'nl-modal';
|
||||
|
||||
if (this.isEmbedded) {
|
||||
// Embedded mode: inline positioning, no overlay
|
||||
this.container.style.cssText = `
|
||||
position: relative;
|
||||
display: none;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
width: 100%;
|
||||
`;
|
||||
} else {
|
||||
// Modal mode: fixed overlay
|
||||
this.container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -27,22 +40,38 @@ class Modal {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
display: none;
|
||||
z-index: 10000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
}
|
||||
|
||||
// Create modal content
|
||||
const modalContent = document.createElement('div');
|
||||
if (this.isEmbedded) {
|
||||
// Embedded content: no centering margin, full width
|
||||
modalContent.style.cssText = `
|
||||
position: relative;
|
||||
background: white;
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-primary-color);
|
||||
width: 100%;
|
||||
border-radius: var(--nl-border-radius, 15px);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
overflow: hidden;
|
||||
`;
|
||||
} else {
|
||||
// Modal content: centered with margin
|
||||
modalContent.style.cssText = `
|
||||
position: relative;
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-primary-color);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
border-radius: var(--nl-border-radius, 15px);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
}
|
||||
|
||||
// Header
|
||||
const modalHeader = document.createElement('div');
|
||||
@@ -51,6 +80,8 @@ class Modal {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
`;
|
||||
|
||||
const modalTitle = document.createElement('h2');
|
||||
@@ -59,17 +90,24 @@ class Modal {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
modalHeader.appendChild(modalTitle);
|
||||
|
||||
// Only add close button for non-embedded modals
|
||||
// Embedded modals shouldn't have a close button because there's no way to reopen them
|
||||
if (!this.isEmbedded) {
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.innerHTML = '×';
|
||||
closeButton.onclick = () => this.close();
|
||||
closeButton.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
background: var(--nl-secondary-color);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 28px;
|
||||
color: #6b7280;
|
||||
color: var(--nl-primary-color);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
@@ -77,13 +115,19 @@ class Modal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
closeButton.onmouseover = () => closeButton.style.background = '#f3f4f6';
|
||||
closeButton.onmouseout = () => closeButton.style.background = 'none';
|
||||
closeButton.onmouseover = () => {
|
||||
closeButton.style.borderColor = 'var(--nl-accent-color)';
|
||||
closeButton.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
closeButton.onmouseout = () => {
|
||||
closeButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
closeButton.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
|
||||
modalHeader.appendChild(modalTitle);
|
||||
modalHeader.appendChild(closeButton);
|
||||
}
|
||||
|
||||
// Body
|
||||
this.modalBody = document.createElement('div');
|
||||
@@ -91,38 +135,52 @@ class Modal {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
background: transparent;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
modalContent.appendChild(modalHeader);
|
||||
modalContent.appendChild(this.modalBody);
|
||||
this.container.appendChild(modalContent);
|
||||
|
||||
// Add to body
|
||||
// Add to appropriate parent
|
||||
if (this.isEmbedded && this.embeddedContainer) {
|
||||
// Append to specified container for embedding
|
||||
if (typeof this.embeddedContainer === 'string') {
|
||||
const targetElement = document.querySelector(this.embeddedContainer);
|
||||
if (targetElement) {
|
||||
targetElement.appendChild(this.container);
|
||||
} else {
|
||||
console.error('NOSTR_LOGIN_LITE: Embedded container not found:', this.embeddedContainer);
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
} else if (this.embeddedContainer instanceof HTMLElement) {
|
||||
this.embeddedContainer.appendChild(this.container);
|
||||
} else {
|
||||
console.error('NOSTR_LOGIN_LITE: Invalid embedded container');
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
} else {
|
||||
// Add to body for modal mode
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
// Click outside to close
|
||||
// Click outside to close (only for modal mode)
|
||||
if (!this.isEmbedded) {
|
||||
this.container.onclick = (e) => {
|
||||
if (e.target === this.container) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Update theme
|
||||
this.updateTheme();
|
||||
}
|
||||
|
||||
updateTheme() {
|
||||
const isDark = this.options?.darkMode;
|
||||
const modalContent = this.container.querySelector(':nth-child(1)');
|
||||
const title = this.container.querySelector('h2');
|
||||
|
||||
if (isDark) {
|
||||
modalContent.style.background = '#1f2937';
|
||||
title.style.color = 'white';
|
||||
} else {
|
||||
modalContent.style.background = 'white';
|
||||
title.style.color = '#1f2937';
|
||||
}
|
||||
// The theme will automatically update through CSS custom properties
|
||||
// No manual styling needed - the CSS variables handle everything
|
||||
}
|
||||
|
||||
open(opts = {}) {
|
||||
@@ -165,8 +223,8 @@ class Modal {
|
||||
});
|
||||
}
|
||||
|
||||
// Nostr Connect option
|
||||
if (this.options?.methods?.connect !== false) {
|
||||
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
|
||||
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
||||
options.push({
|
||||
type: 'connect',
|
||||
title: 'Nostr Connect',
|
||||
@@ -186,7 +244,7 @@ class Modal {
|
||||
}
|
||||
|
||||
// OTP/DM option
|
||||
if (this.options?.methods?.otp !== false && this.options?.otp) {
|
||||
if (this.options?.methods?.otp !== false) {
|
||||
options.push({
|
||||
type: 'otp',
|
||||
title: 'DM/OTP',
|
||||
@@ -205,26 +263,41 @@ class Modal {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background: ${this.options?.darkMode ? '#374151' : 'white'};
|
||||
border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'};
|
||||
border-radius: 8px;
|
||||
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);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
button.onmouseover = () => {
|
||||
button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
|
||||
button.style.borderColor = 'var(--nl-accent-color)';
|
||||
button.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
button.onmouseout = () => {
|
||||
button.style.boxShadow = 'none';
|
||||
button.style.borderColor = 'var(--nl-primary-color)';
|
||||
button.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
iconDiv.textContent = option.icon;
|
||||
// Replace emoji icons with text-based ones
|
||||
const iconMap = {
|
||||
'🔌': '[EXT]',
|
||||
'🔑': '[KEY]',
|
||||
'🌐': '[NET]',
|
||||
'👁️': '[VIEW]',
|
||||
'📱': '[SMS]'
|
||||
};
|
||||
iconDiv.textContent = iconMap[option.icon] || option.icon;
|
||||
iconDiv.style.cssText = `
|
||||
font-size: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-right: 16px;
|
||||
width: 24px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
@@ -235,14 +308,16 @@ class Modal {
|
||||
titleDiv.style.cssText = `
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: ${this.options?.darkMode ? 'white' : '#1f2937'};
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
const descDiv = document.createElement('div');
|
||||
descDiv.textContent = option.description;
|
||||
descDiv.style.cssText = `
|
||||
font-size: 14px;
|
||||
color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'};
|
||||
color: #666666;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
contentDiv.appendChild(titleDiv);
|
||||
@@ -446,11 +521,22 @@ class Modal {
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Choose Browser Extension';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
title.style.cssText = `
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = `Found ${extensions.length} Nostr extensions. Choose which one to use:`;
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
description.style.cssText = `
|
||||
margin-bottom: 20px;
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
@@ -465,21 +551,23 @@ class Modal {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background: ${this.options?.darkMode ? '#374151' : 'white'};
|
||||
border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'};
|
||||
border-radius: 8px;
|
||||
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);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
button.onmouseover = () => {
|
||||
button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
|
||||
button.style.transform = 'translateY(-1px)';
|
||||
button.style.borderColor = 'var(--nl-accent-color)';
|
||||
button.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
button.onmouseout = () => {
|
||||
button.style.boxShadow = 'none';
|
||||
button.style.transform = 'none';
|
||||
button.style.borderColor = 'var(--nl-primary-color)';
|
||||
button.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
@@ -499,15 +587,16 @@ class Modal {
|
||||
nameDiv.style.cssText = `
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: ${this.options?.darkMode ? 'white' : '#1f2937'};
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
const pathDiv = document.createElement('div');
|
||||
pathDiv.textContent = ext.name;
|
||||
pathDiv.style.cssText = `
|
||||
font-size: 12px;
|
||||
color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'};
|
||||
font-family: monospace;
|
||||
color: #666666;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
contentDiv.appendChild(nameDiv);
|
||||
@@ -858,11 +947,7 @@ class Modal {
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Pre-fill with our bunker config if available
|
||||
if (window.NIP46_BUNKER_CONFIG) {
|
||||
pubkeyInput.value = window.NIP46_BUNKER_CONFIG.remoteSigner.pubkey;
|
||||
urlInput.value = window.NIP46_BUNKER_CONFIG.remoteSigner.url;
|
||||
}
|
||||
// Users will enter the bunker URL manually from their bunker setup
|
||||
|
||||
const connectButton = document.createElement('button');
|
||||
connectButton.textContent = 'Connect to Bunker';
|
||||
@@ -956,9 +1041,9 @@ class Modal {
|
||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||
console.log('Generated local client keypair for NIP-46 session');
|
||||
|
||||
// Use nostr-tools BunkerSigner.fromBunker() for bunker:// connections
|
||||
// Use nostr-tools BunkerSigner constructor
|
||||
console.log('Creating nip46 BunkerSigner...');
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
const signer = new window.NostrTools.nip46.BunkerSigner(localSecretKey, bunkerPointer, {
|
||||
onauth: (url) => {
|
||||
console.log('Received auth URL from bunker:', url);
|
||||
// Open auth URL in popup or redirect
|
||||
@@ -968,12 +1053,8 @@ class Modal {
|
||||
|
||||
console.log('NIP-46 BunkerSigner created successfully');
|
||||
|
||||
// Attempt initial ping to verify connection
|
||||
console.log('Testing bunker connection with ping...');
|
||||
await signer.ping();
|
||||
console.log('NIP-46 ping successful - bunker is reachable');
|
||||
|
||||
// Try to connect (this may trigger auth flow)
|
||||
// Skip ping test - NIP-46 works through relays, not direct connection
|
||||
// Try to connect directly (this may trigger auth flow)
|
||||
console.log('Attempting NIP-46 connect...');
|
||||
await signer.connect();
|
||||
console.log('NIP-46 connect successful');
|
||||
@@ -1049,23 +1130,24 @@ class Modal {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
if (type === 'primary') {
|
||||
return baseStyle + `
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-primary-color);
|
||||
`;
|
||||
} else {
|
||||
return baseStyle + `
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
background: #cccccc;
|
||||
color: var(--nl-primary-color);
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
139
nip46-test/README.md
Normal file
139
nip46-test/README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 🏰 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
|
||||
42
nip46-test/start-bunker.sh
Executable file
42
nip46-test/start-bunker.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NIP-46 Remote Signer Bunker Setup
|
||||
# Self-contained script to run a test bunker for NOSTR_LOGIN_LITE
|
||||
|
||||
echo "🔐 Starting NIP-46 Bunker Remote Signer..."
|
||||
echo "=============================================="
|
||||
|
||||
# NIP-46 Keys (Generated with NAK for testing)
|
||||
|
||||
BUNKER_SECRET_KEY="a33767c3bd05bda47880119d6665b79e6f0eecdf8d025966b0b59a9366379d01"
|
||||
BUNKER_PUB_KEY="7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90"
|
||||
|
||||
|
||||
|
||||
RELAY_URL="wss://relay.laantungir.net"
|
||||
# RELAY_URL="wss://nostr.mom"
|
||||
|
||||
echo "Bunker Configuration:"
|
||||
echo " Public Key: $BUNKER_PUB_KEY"
|
||||
echo " Relay URL: $RELAY_URL"
|
||||
echo " Secret key securely held by bunker process"
|
||||
echo ""
|
||||
|
||||
# Check if nak is installed
|
||||
if ! command -v nak &> /dev/null; then
|
||||
echo "❌ Error: 'nak' command not found"
|
||||
echo "Please install nak from: https://github.com/fiatjaf/nak"
|
||||
echo "Or run: go install github.com/fiatjaf/nak@latest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 Starting bunker daemon..."
|
||||
echo "The bunker will display a QR code with the connection URL"
|
||||
echo "Copy the bunker:// URL and paste it into the NOSTR_LOGIN_LITE modal"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop the bunker"
|
||||
echo "=============================================="
|
||||
|
||||
# Start the NAK bunker daemon
|
||||
# This listens for NIP-46 requests via the relay and handles signing operations
|
||||
nak bunker --sec "$BUNKER_SECRET_KEY" --qrcode --verbose "$RELAY_URL"
|
||||
10007
package-lock.json
generated
10007
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@
|
||||
"publish": "lerna publish --no-private"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "^2.4.0"
|
||||
"@nostr-dev-kit/ndk": "^2.4.0",
|
||||
"nostr-tools": "^2.16.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lerna": "^8.0.2"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "consistent",
|
||||
"printWidth": 180,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
Nostr-Login
|
||||
===========
|
||||
|
||||
This library is a powerful `window.nostr` provider.
|
||||
|
||||
```
|
||||
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js'></script>
|
||||
```
|
||||
|
||||
Just add the above script to your HTML and
|
||||
get a nice UI for users to login with Nostr Connect (nip46), with an extension, read-only login,
|
||||
account switching, OAuth-like sign up, etc. Your app just talks to the `window.nostr`, the rest is handled by `nostr-login`.
|
||||
|
||||
See it in action on [nostr.band](https://nostr.band).
|
||||
|
||||
## Options
|
||||
|
||||
You can set these attributes to the `script` tag to customize the behavior:
|
||||
- `data-dark-mode` - `true`/`false`, default will use the browser's color theme
|
||||
- `data-bunkers` - the comma-separated list of domain names of Nostr Connect (nip46) providers for sign up, i.e. `nsec.app,highlighter.com`
|
||||
- `data-perms` - the comma-separated list of [permissions](https://github.com/nostr-protocol/nips/blob/master/46.md#requested-permissions) requested by the app over Nostr Connect, i.e. `sign_event:1,nip04_encrypt`
|
||||
- `data-theme` - color themes, one of `default`, `ocean`, `lemonade`, `purple`
|
||||
- `data-no-banner` - if `true`, do not show the `nostr-login` banner, will need to launch the modals using event dispatch, see below
|
||||
- `data-methods` - comma-separated list of allowed auth methods, method names: `connect`, `extension`, `readOnly`, `local`, all allowed by default.
|
||||
- `data-otp-request-url` - URL for requesting OTP code
|
||||
- `data-otp-reply-url` - URL for replying with OTP code
|
||||
- `data-title` - title for the welcome screen
|
||||
- `data-description` - description for the welcome screen
|
||||
- `data-start-screen` - screen shown by default (banner click, window.nostr.* call), options: `welcome`, `welcome-login`, `welcome-signup`, `signup`, `local-signup`, `login`, `otp`, `connect`, `login-bunker-url`, `login-read-only`, `connection-string`, `switch-account`, `import`
|
||||
- `data-signup-relays` - comma-separated list of relays where nip65 event will be published on local signup
|
||||
- `data-outbox-relays` - comma-separated list of relays that will be added to nip65 event on local signup
|
||||
- `data-signup-nstart` - "true" to use start.njump.me instead of local signup
|
||||
- `data-follow-npubs` - comma-separated list of npubs to follow if njump.me signup is used
|
||||
|
||||
Example:
|
||||
```
|
||||
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js' data-perms="sign_event:1,sign_event:0" data-theme="ocean"></script>
|
||||
```
|
||||
|
||||
## Updating the UI
|
||||
|
||||
Whenever user performs an auth-related action using `nostr-login`, a `nlAuth` event will be dispatched on the `document`, which you can listen
|
||||
to in order to update your UI (show user profile, etc):
|
||||
|
||||
```
|
||||
document.addEventListener('nlAuth', (e) => {
|
||||
// type is login, signup or logout
|
||||
if (e.detail.type === 'login' || e.detail.type === 'signup') {
|
||||
onLogin(); // get pubkey with window.nostr and show user profile
|
||||
} else {
|
||||
onLogout() // clear local user data, hide profile info
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Launching, logout, etc
|
||||
|
||||
The `nostr-login` auth modals will be automatically launched whenever you
|
||||
make a call to `window.nostr` if user isn't authed yet. However, you can also launch the auth flow by dispatching a custom `nlLaunch` event:
|
||||
|
||||
```
|
||||
document.dispatchEvent(new CustomEvent('nlLaunch', { detail: 'welcome' }));
|
||||
```
|
||||
|
||||
The `detail` event payload can be empty, or can be one of `welcome`, `signup`, `login`, `login-bunker-url`, `login-read-only`, `switch-account`.
|
||||
|
||||
To trigger logout in the `nostr-login`, you can dispatch a `nlLogout` event:
|
||||
|
||||
```
|
||||
document.dispatchEvent(new Event("nlLogout"));
|
||||
```
|
||||
|
||||
To change dark mode in the `nostr-login`, you can dispatch a `nlDarkMode` event, with detail as `darkMode` boolean:
|
||||
|
||||
```
|
||||
document.dispatchEvent(new CustomEvent("nlDarkMode", { detail: true }));
|
||||
```
|
||||
|
||||
## Use as a package
|
||||
|
||||
Install `nostr-login` package with `npm` and then:
|
||||
|
||||
```
|
||||
import { init as initNostrLogin } from "nostr-login"
|
||||
|
||||
// make sure this is called before any
|
||||
// window.nostr calls are made
|
||||
initNostrLogin({/*options*/})
|
||||
|
||||
```
|
||||
|
||||
Now the `window.nostr` will be initialized and on your first call
|
||||
to it the auth flow will be launched if user isn't authed yet.
|
||||
|
||||
You can also launch the auth flow yourself:
|
||||
|
||||
```
|
||||
import { launch as launchNostrLoginDialog } from "nostr-login"
|
||||
|
||||
// make sure init() was called
|
||||
|
||||
// on your signup button click
|
||||
function onSignupClick() {
|
||||
// launch signup screen
|
||||
launchNostrLoginDialog({
|
||||
startScreen: 'signup'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js Fix for Server Side Rendering (SSR)
|
||||
|
||||
`nostr-login` calls `document` which is unavailable for server-side rendering. You will have build errors. To fix this, you can import `nostr-login` on the client side in your component with a `useEffect` like this:
|
||||
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
import('nostr-login')
|
||||
.then(async ({ init }) => {
|
||||
init({
|
||||
// options
|
||||
})
|
||||
})
|
||||
.catch((error) => console.log('Failed to load nostr-login', error));
|
||||
}, []);
|
||||
```
|
||||
Note: even if your component has `"use client"` in the first line, this fix still may be necessary.
|
||||
|
||||
---
|
||||
|
||||
API:
|
||||
- `init(opts)` - set mapping of window.nostr to nostr-login
|
||||
- `launch(startScreen)` - launch nostr-login UI
|
||||
- `logout()` - drop the current nip46 connection
|
||||
|
||||
Options:
|
||||
- `theme` - same as `data-theme` above
|
||||
- `startScreen` - same as `startScreen` for `nlLaunch` event above
|
||||
- `bunkers` - same as `data-bunkers` above
|
||||
- `devOverrideBunkerOrigin` - for testing, overrides the bunker origin for local setup
|
||||
- `onAuth: (npub: string, options: NostrLoginAuthOptions)` - a callback to provide instead of listening to `nlAuth` event
|
||||
- `perms` - same as `data-perms` above
|
||||
- `darkMode` - same as `data-dark-mode` above
|
||||
- `noBanner` - same as `data-no-banner` above
|
||||
- `isSignInWithExtension` - `true` to bring the *Sign in with exception* button into main list of options, `false` to hide to the *Advanced*, default will behave as `true` if extension is detected.
|
||||
|
||||
## OTP login
|
||||
|
||||
If you supply both `data-otp-request-url` and `data-otp-reply-url` then "Login with DM" button will appear on the welcome screen.
|
||||
|
||||
When user enters their nip05 or npub, a GET request is made to `<data-otp-request-url>[?&]pubkey=<user-pubkey>`. Server should send
|
||||
a DM with one-time code to that pubkey and should return 200.
|
||||
|
||||
After user enters the code, a GET request is made to `<data-otp-reply-url>[?&]pubkey=<user-pubkey>&code=<code>`. Server should check that code matches the pubkey and hasn't expired, and should return 200 status and an optional payload. Nostr-login will deliver the payload as `otpData` field in `nlAuth` event, and will save the payload in localstore and will deliver it again as `nlAuth` on page reload.
|
||||
|
||||
The reply payload may be used to supply the session token. If token is sent by the server as a cookie then payload might be empty, otherwise the payload should be used by the app to extract the token and use it in future API calls to the server.
|
||||
|
||||
## Examples
|
||||
|
||||
* [Basic HTML Example](./examples/usage.html)
|
||||
|
||||
## TODO
|
||||
|
||||
- fetch bunker list using NIP-89
|
||||
- Amber support
|
||||
- allow use without the UIs
|
||||
- add timeout handling
|
||||
- more at [issues](https://github.com/nostrband/nostr-login/issues)
|
||||
@@ -1,30 +0,0 @@
|
||||
<!doctype html>
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
|
||||
<title>Modal Auth Demo</title>
|
||||
|
||||
<script type="module">
|
||||
// import { init } from './dist/index.esm.js';
|
||||
//
|
||||
// const test = async () => {
|
||||
// const bunkerUrl = await launch({
|
||||
// theme: 'purple',
|
||||
// startScreen: 'signup',
|
||||
// });
|
||||
//
|
||||
// console.log(bunkerUrl);
|
||||
// };
|
||||
</script>
|
||||
|
||||
<script src="./dist/unpkg.js" data-start-screen="local-signup"
|
||||
data-signup-relays="wss://relay.nostr.band/,wss://relay.primal.net"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!--<nl-button title-btn="Sign in" start-screen="login" nl-theme="lemonade"></nl-button>-->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "nostr-login",
|
||||
"version": "1.7.11",
|
||||
"description": "",
|
||||
"main": "./dist/index.esm.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"format": "npx prettier --write src"
|
||||
},
|
||||
"author": "a-fralou",
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "^2.3.1",
|
||||
"nostr-tools": "^1.17.0",
|
||||
"tseep": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"nostr-login-components": "^1.0.3",
|
||||
"prettier": "^3.2.2",
|
||||
"rollup": "^4.9.6",
|
||||
"rollup-plugin-typescript2": "^0.36.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.esm.js',
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.json',
|
||||
}),
|
||||
resolve({
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
terser({
|
||||
compress: {
|
||||
toplevel: true,
|
||||
}
|
||||
})
|
||||
],
|
||||
},
|
||||
{
|
||||
input: 'src/iife-module.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/unpkg.js',
|
||||
format: 'iife',
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.json',
|
||||
}),
|
||||
resolve({
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
terser({
|
||||
compress: {
|
||||
toplevel: true,
|
||||
},
|
||||
})
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const CALL_TIMEOUT = 5000;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { init } from './index';
|
||||
import { NostrLoginOptions, StartScreens } from './types';
|
||||
|
||||
// wrap to hide local vars
|
||||
(() => {
|
||||
// currentScript only visible in global scope code, not event handlers
|
||||
const cs = document.currentScript;
|
||||
const start = async () => {
|
||||
const options: NostrLoginOptions = {};
|
||||
|
||||
if (cs) {
|
||||
const dm = cs.getAttribute('data-dark-mode');
|
||||
if (dm) options.darkMode = dm === 'true';
|
||||
|
||||
const bunkers = cs.getAttribute('data-bunkers');
|
||||
if (bunkers) options.bunkers = bunkers;
|
||||
|
||||
const startScreen = cs.getAttribute('data-start-screen');
|
||||
if (startScreen) options.startScreen = startScreen as StartScreens;
|
||||
|
||||
const perms = cs.getAttribute('data-perms');
|
||||
if (perms) options.perms = perms;
|
||||
|
||||
const theme = cs.getAttribute('data-theme');
|
||||
if (theme) options.theme = theme;
|
||||
|
||||
const noBanner = cs.getAttribute('data-no-banner');
|
||||
if (noBanner) options.noBanner = noBanner === 'true';
|
||||
|
||||
const localSignup = cs.getAttribute('data-local-signup');
|
||||
if (localSignup) options.localSignup = localSignup === 'true';
|
||||
|
||||
const signupNjump = cs.getAttribute('data-signup-nstart') || cs.getAttribute('data-signup-njump');
|
||||
if (signupNjump) options.signupNstart = signupNjump === 'true';
|
||||
|
||||
const followNpubs = cs.getAttribute('data-follow-npubs');
|
||||
if (followNpubs) options.followNpubs = followNpubs;
|
||||
|
||||
const otpRequestUrl = cs.getAttribute('data-otp-request-url');
|
||||
if (otpRequestUrl) options.otpRequestUrl = otpRequestUrl;
|
||||
|
||||
const otpReplyUrl = cs.getAttribute('data-otp-reply-url');
|
||||
if (otpReplyUrl) options.otpReplyUrl = otpReplyUrl;
|
||||
|
||||
if (!!otpRequestUrl !== !!otpReplyUrl) console.warn('nostr-login: need request and reply urls for OTP auth');
|
||||
|
||||
const methods = cs.getAttribute('data-methods');
|
||||
if (methods) {
|
||||
// @ts-ignore
|
||||
options.methods = methods
|
||||
.trim()
|
||||
.split(',')
|
||||
.filter(m => !!m);
|
||||
}
|
||||
|
||||
const title = cs.getAttribute('data-title');
|
||||
if (title) options.title = title;
|
||||
|
||||
const description = cs.getAttribute('data-description');
|
||||
if (description) options.description = description;
|
||||
|
||||
const signupRelays = cs.getAttribute('data-signup-relays');
|
||||
if (signupRelays) options.signupRelays = signupRelays;
|
||||
|
||||
const outboxRelays = cs.getAttribute('data-outbox-relays');
|
||||
if (outboxRelays) options.outboxRelays = outboxRelays.split(',');
|
||||
|
||||
const dev = cs.getAttribute('data-dev') === 'true';
|
||||
if (dev) options.dev = dev;
|
||||
|
||||
const custom = cs.getAttribute('data-custom-nostr-connect') === 'true';
|
||||
if (custom) options.customNostrConnect = custom;
|
||||
|
||||
console.log('nostr-login options', options);
|
||||
}
|
||||
|
||||
init(options);
|
||||
};
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|
||||
else start();
|
||||
})();
|
||||
@@ -1,347 +0,0 @@
|
||||
import 'nostr-login-components';
|
||||
import { AuthNostrService, NostrExtensionService, Popup, NostrParams, Nostr, ProcessManager, BannerManager, ModalManager } from './modules';
|
||||
import { NostrLoginAuthOptions, NostrLoginOptions, StartScreens } from './types';
|
||||
import { localStorageGetAccounts, localStorageGetCurrent, localStorageGetRecents, localStorageSetItem } from './utils';
|
||||
import { Info } from 'nostr-login-components/dist/types/types';
|
||||
import { NostrObjectParams } from './modules/Nostr';
|
||||
|
||||
export class NostrLoginInitializer {
|
||||
public extensionService: NostrExtensionService;
|
||||
public params: NostrParams;
|
||||
public authNostrService: AuthNostrService;
|
||||
public nostr: Nostr;
|
||||
public processManager: ProcessManager;
|
||||
public popupManager: Popup;
|
||||
public bannerManager: BannerManager;
|
||||
public modalManager: ModalManager;
|
||||
|
||||
private customLaunchCallback?: () => void;
|
||||
|
||||
constructor() {
|
||||
this.params = new NostrParams();
|
||||
this.processManager = new ProcessManager();
|
||||
this.popupManager = new Popup();
|
||||
this.bannerManager = new BannerManager(this.params);
|
||||
this.authNostrService = new AuthNostrService(this.params);
|
||||
this.extensionService = new NostrExtensionService(this.params);
|
||||
this.modalManager = new ModalManager(this.params, this.authNostrService, this.extensionService);
|
||||
|
||||
const nostrApi: NostrObjectParams = {
|
||||
waitReady: async () => {
|
||||
await this.authNostrService.waitReady();
|
||||
await this.modalManager.waitReady();
|
||||
},
|
||||
getUserInfo: () => this.params.userInfo,
|
||||
getSigner: () => {
|
||||
if (this.params.userInfo!.authMethod === 'readOnly') throw new Error('Read only');
|
||||
return this.params.userInfo!.authMethod === 'extension' ? this.extensionService.getExtension() : this.authNostrService;
|
||||
},
|
||||
launch: () => {
|
||||
return this.launch();
|
||||
},
|
||||
wait: cb => this.processManager.wait(cb),
|
||||
};
|
||||
|
||||
this.nostr = new Nostr(nostrApi);
|
||||
|
||||
this.processManager.on('onCallTimeout', () => {
|
||||
this.bannerManager.onCallTimeout();
|
||||
});
|
||||
|
||||
this.processManager.on('onCallEnd', () => {
|
||||
this.bannerManager.onCallEnd();
|
||||
this.modalManager.onCallEnd();
|
||||
});
|
||||
|
||||
this.processManager.on('onCallStart', () => {
|
||||
this.bannerManager.onCallStart();
|
||||
});
|
||||
|
||||
this.authNostrService.on('onIframeUrl', url => {
|
||||
this.modalManager.onIframeUrl(url);
|
||||
});
|
||||
|
||||
this.authNostrService.on('iframeRestart', ({ iframeUrl }) => {
|
||||
this.processManager.onIframeUrl();
|
||||
this.bannerManager.onIframeRestart(iframeUrl);
|
||||
});
|
||||
|
||||
this.authNostrService.on('onAuthUrl', ({ url, iframeUrl, eventToAddAccount }) => {
|
||||
this.processManager.onAuthUrl();
|
||||
|
||||
if (eventToAddAccount) {
|
||||
this.modalManager.onAuthUrl(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.params.userInfo) {
|
||||
// show the 'Please confirm' banner
|
||||
this.bannerManager.onAuthUrl(url, iframeUrl);
|
||||
} else {
|
||||
// if it fails we will either return 'failed'
|
||||
// to the window.nostr caller, or show proper error
|
||||
// in our modal
|
||||
this.modalManager.onAuthUrl(url);
|
||||
}
|
||||
});
|
||||
|
||||
this.authNostrService.on('updateAccounts', () => {
|
||||
this.updateAccounts();
|
||||
});
|
||||
|
||||
this.authNostrService.on('onUserInfo', info => {
|
||||
this.bannerManager.onUserInfo(info);
|
||||
});
|
||||
|
||||
this.modalManager.on('onAuthUrlClick', url => {
|
||||
this.openPopup(url);
|
||||
});
|
||||
|
||||
this.bannerManager.on('onIframeAuthUrlClick', url => {
|
||||
this.modalManager.showIframeUrl(url);
|
||||
});
|
||||
|
||||
this.modalManager.on('onSwitchAccount', async (info: Info) => {
|
||||
this.switchAccount(info);
|
||||
});
|
||||
|
||||
this.modalManager.on('onLogoutBanner', async (info: Info) => {
|
||||
logout();
|
||||
});
|
||||
|
||||
this.bannerManager.on('onConfirmLogout', async () => {
|
||||
// @ts-ignore
|
||||
this.launch('confirm-logout');
|
||||
});
|
||||
|
||||
this.modalManager.on('updateAccounts', () => {
|
||||
this.updateAccounts();
|
||||
});
|
||||
|
||||
this.bannerManager.on('logout', () => {
|
||||
logout();
|
||||
});
|
||||
|
||||
this.bannerManager.on('onAuthUrlClick', url => {
|
||||
this.openPopup(url);
|
||||
});
|
||||
|
||||
this.bannerManager.on('onSwitchAccount', async (info: Info) => {
|
||||
this.switchAccount(info);
|
||||
});
|
||||
|
||||
this.bannerManager.on('import', () => {
|
||||
this.launch('import');
|
||||
});
|
||||
|
||||
this.extensionService.on('extensionLogin', (pubkey: string) => {
|
||||
this.authNostrService.setExtension(pubkey);
|
||||
});
|
||||
|
||||
this.extensionService.on('extensionLogout', () => {
|
||||
logout();
|
||||
});
|
||||
|
||||
this.bannerManager.on('launch', (startScreen?: StartScreens) => {
|
||||
this.launch(startScreen);
|
||||
});
|
||||
}
|
||||
|
||||
private openPopup(url: string) {
|
||||
this.popupManager.openPopup(url);
|
||||
}
|
||||
|
||||
private async switchAccount(info: Info, signup = false) {
|
||||
console.log('nostr login switch to info', info);
|
||||
|
||||
// make sure extension is unlinked
|
||||
this.extensionService.unsetExtension(this.nostr);
|
||||
|
||||
if (info.authMethod === 'readOnly') {
|
||||
this.authNostrService.setReadOnly(info.pubkey);
|
||||
} else if (info.authMethod === 'otp') {
|
||||
this.authNostrService.setOTP(info.pubkey, info.otpData || '');
|
||||
} else if (info.authMethod === 'local' && info.sk) {
|
||||
this.authNostrService.setLocal(info, signup);
|
||||
} else if (info.authMethod === 'extension') {
|
||||
// trySetExtensionForPubkey will check if
|
||||
// we still have the extension and it's the same pubkey
|
||||
await this.extensionService.trySetExtensionForPubkey(info.pubkey);
|
||||
} else if (info.authMethod === 'connect' && info.sk && info.relays && info.relays[0]) {
|
||||
this.authNostrService.setConnect(info);
|
||||
} else {
|
||||
throw new Error('Bad auth info');
|
||||
}
|
||||
}
|
||||
|
||||
private updateAccounts() {
|
||||
const accounts = localStorageGetAccounts();
|
||||
const recents = localStorageGetRecents();
|
||||
this.bannerManager.onUpdateAccounts(accounts);
|
||||
this.modalManager.onUpdateAccounts(accounts, recents);
|
||||
}
|
||||
|
||||
public async launchCustomNostrConnect() {
|
||||
try {
|
||||
if (this.authNostrService.isAuthing()) this.authNostrService.cancelNostrConnect();
|
||||
|
||||
const customLaunchPromise = new Promise<void>(ok => (this.customLaunchCallback = ok));
|
||||
await this.authNostrService.startAuth();
|
||||
await this.authNostrService.sendNeedAuth();
|
||||
|
||||
try {
|
||||
await this.authNostrService.nostrConnect();
|
||||
await this.authNostrService.endAuth();
|
||||
} catch (e) {
|
||||
// if client manually launches the UI we'll
|
||||
// have cancelled error from the nostrConnect call,
|
||||
// and that's when we should block on the customLaunchPromise
|
||||
if (e === 'cancelled') await customLaunchPromise;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('launchCustomNostrConnect', e);
|
||||
}
|
||||
}
|
||||
|
||||
private fulfillCustomLaunchPromise() {
|
||||
if (this.customLaunchCallback) {
|
||||
const cb = this.customLaunchCallback;
|
||||
this.customLaunchCallback = undefined;
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
public launch = async (startScreen?: StartScreens | 'default') => {
|
||||
if (!startScreen) {
|
||||
if (this.params.optionsModal.customNostrConnect) {
|
||||
return this.launchCustomNostrConnect();
|
||||
}
|
||||
}
|
||||
|
||||
const recent = localStorageGetRecents();
|
||||
const accounts = localStorageGetAccounts();
|
||||
|
||||
const options = { ...this.params.optionsModal };
|
||||
if (startScreen && startScreen !== 'default') options.startScreen = startScreen;
|
||||
else if (Boolean(recent?.length) || Boolean(accounts?.length)) {
|
||||
options.startScreen = 'switch-account';
|
||||
}
|
||||
|
||||
// if we're being manually called in the middle of customNostrConnect
|
||||
// flow then we'll reset the current auth session and launch
|
||||
// our manual flow and then release the customNostrConnect session
|
||||
// as if it finished properly
|
||||
if (this.customLaunchCallback) this.authNostrService.cancelNostrConnect();
|
||||
try {
|
||||
await this.modalManager.launch(options);
|
||||
|
||||
// if custom launch was interrupted by manual
|
||||
// launch then we unlock the custom launch to make
|
||||
// it proceed
|
||||
this.fulfillCustomLaunchPromise();
|
||||
} catch (e) {
|
||||
// don't throw if cancelled
|
||||
console.log('nostr-login failed', e);
|
||||
}
|
||||
};
|
||||
|
||||
public init = async (opt: NostrLoginOptions) => {
|
||||
// watch for extension trying to overwrite our window.nostr
|
||||
this.extensionService.startCheckingExtension(this.nostr);
|
||||
|
||||
// set ourselves as nostr
|
||||
|
||||
// @ts-ignore
|
||||
window.nostr = this.nostr;
|
||||
|
||||
// connect launching of our modals to nl-button elements
|
||||
this.modalManager.connectModals(opt);
|
||||
|
||||
// launch
|
||||
this.bannerManager.launchAuthBanner(opt);
|
||||
|
||||
// store options
|
||||
if (opt) {
|
||||
this.params.optionsModal = { ...opt };
|
||||
}
|
||||
|
||||
try {
|
||||
// read conf from localstore
|
||||
const info = localStorageGetCurrent();
|
||||
|
||||
// have current session?
|
||||
if (info) {
|
||||
// wtf?
|
||||
if (!info.pubkey) throw new Error('Bad stored info');
|
||||
|
||||
// switch to it
|
||||
await this.switchAccount(info);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('nostr login init error', e);
|
||||
|
||||
await logout();
|
||||
}
|
||||
|
||||
// ensure current state
|
||||
this.updateAccounts();
|
||||
};
|
||||
|
||||
public logout = async () => {
|
||||
// replace back
|
||||
this.extensionService.unsetExtension(this.nostr);
|
||||
|
||||
await this.authNostrService.logout();
|
||||
};
|
||||
|
||||
public setDarkMode = (dark: boolean) => {
|
||||
localStorageSetItem('nl-dark-mode', `${dark}`);
|
||||
this.bannerManager.onDarkMode(dark);
|
||||
this.modalManager.onDarkMode(dark);
|
||||
};
|
||||
|
||||
public setAuth = async (o: NostrLoginAuthOptions) => {
|
||||
if (!o.type) throw new Error('Invalid auth event');
|
||||
if (o.type !== 'login' && o.type !== 'logout' && o.type !== 'signup') throw new Error('Invalid auth event');
|
||||
if (o.method && o.method !== 'connect' && o.method !== 'extension' && o.method !== 'local' && o.method !== 'otp' && o.method !== 'readOnly')
|
||||
throw new Error('Invalid auth event');
|
||||
|
||||
if (o.type === 'logout') return this.logout();
|
||||
|
||||
if (!o.method || !o.pubkey) throw new Error('Invalid pubkey');
|
||||
|
||||
const info: Info = {
|
||||
authMethod: o.method,
|
||||
pubkey: o.pubkey,
|
||||
relays: o.relays,
|
||||
sk: o.localNsec,
|
||||
otpData: o.otpData,
|
||||
name: o.name,
|
||||
};
|
||||
await this.switchAccount(info, o.type === 'signup');
|
||||
};
|
||||
|
||||
public cancelNeedAuth = () => {
|
||||
console.log("cancelNeedAuth");
|
||||
this.fulfillCustomLaunchPromise();
|
||||
this.authNostrService.cancelNostrConnect();
|
||||
};
|
||||
}
|
||||
|
||||
const initializer = new NostrLoginInitializer();
|
||||
|
||||
export const { init, launch, logout, setDarkMode, setAuth, cancelNeedAuth } = initializer;
|
||||
|
||||
document.addEventListener('nlLogout', logout);
|
||||
document.addEventListener('nlLaunch', (event: any) => {
|
||||
launch(event.detail || '');
|
||||
});
|
||||
document.addEventListener('nlNeedAuthCancel', () => {
|
||||
cancelNeedAuth();
|
||||
});
|
||||
document.addEventListener('nlDarkMode', (event: any) => {
|
||||
setDarkMode(!!event.detail);
|
||||
});
|
||||
document.addEventListener('nlSetAuth', (event: any) => {
|
||||
setAuth(event.detail as NostrLoginAuthOptions);
|
||||
});
|
||||
@@ -1,718 +0,0 @@
|
||||
import { localStorageAddAccount, bunkerUrlToInfo, isBunkerUrl, fetchProfile, getBunkerUrl, localStorageRemoveCurrentAccount, createProfile, getIcon } from '../utils';
|
||||
import { ConnectionString, Info } from 'nostr-login-components/dist/types/types';
|
||||
import { generatePrivateKey, getEventHash, getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { NostrLoginAuthOptions, Response } from '../types';
|
||||
import NDK, { NDKEvent, NDKNip46Signer, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||
import { NostrParams } from './';
|
||||
import { EventEmitter } from 'tseep';
|
||||
import { Signer } from './Nostr';
|
||||
import { Nip44 } from '../utils/nip44';
|
||||
import { IframeNostrRpc, Nip46Signer, ReadyListener } from './Nip46';
|
||||
import { PrivateKeySigner } from './Signer';
|
||||
|
||||
const OUTBOX_RELAYS = ['wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.nos.social'];
|
||||
const DEFAULT_NOSTRCONNECT_RELAY = 'wss://relay.nsec.app/';
|
||||
const NOSTRCONNECT_APPS: ConnectionString[] = [
|
||||
{
|
||||
name: 'Nsec.app',
|
||||
domain: 'nsec.app',
|
||||
canImport: true,
|
||||
img: 'https://nsec.app/assets/favicon.ico',
|
||||
link: 'https://use.nsec.app/<nostrconnect>',
|
||||
relay: 'wss://relay.nsec.app/',
|
||||
},
|
||||
{
|
||||
name: 'Amber',
|
||||
img: 'https://raw.githubusercontent.com/greenart7c3/Amber/refs/heads/master/assets/android-icon.svg',
|
||||
link: '<nostrconnect>',
|
||||
relay: 'wss://relay.nsec.app/',
|
||||
},
|
||||
{
|
||||
name: 'Other key stores',
|
||||
img: '',
|
||||
link: '<nostrconnect>',
|
||||
relay: 'wss://relay.nsec.app/',
|
||||
},
|
||||
];
|
||||
|
||||
class AuthNostrService extends EventEmitter implements Signer {
|
||||
private ndk: NDK;
|
||||
private profileNdk: NDK;
|
||||
private signer: Nip46Signer | null = null;
|
||||
private localSigner: PrivateKeySigner | null = null;
|
||||
private params: NostrParams;
|
||||
private signerPromise?: Promise<void>;
|
||||
private signerErrCallback?: (err: string) => void;
|
||||
private readyPromise?: Promise<void>;
|
||||
private readyCallback?: () => void;
|
||||
private nip44Codec = new Nip44();
|
||||
private nostrConnectKey: string = '';
|
||||
private nostrConnectSecret: string = '';
|
||||
private iframe?: HTMLIFrameElement;
|
||||
private starterReady?: ReadyListener;
|
||||
|
||||
nip04: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
|
||||
};
|
||||
nip44: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
|
||||
};
|
||||
|
||||
constructor(params: NostrParams) {
|
||||
super();
|
||||
this.params = params;
|
||||
this.ndk = new NDK({
|
||||
enableOutboxModel: false,
|
||||
});
|
||||
|
||||
this.profileNdk = new NDK({
|
||||
enableOutboxModel: true,
|
||||
explicitRelayUrls: OUTBOX_RELAYS,
|
||||
});
|
||||
this.profileNdk.connect();
|
||||
|
||||
this.nip04 = {
|
||||
encrypt: this.encrypt04.bind(this),
|
||||
decrypt: this.decrypt04.bind(this),
|
||||
};
|
||||
this.nip44 = {
|
||||
encrypt: this.encrypt44.bind(this),
|
||||
decrypt: this.decrypt44.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public isIframe() {
|
||||
return !!this.iframe;
|
||||
}
|
||||
|
||||
public async waitReady() {
|
||||
if (this.signerPromise) {
|
||||
try {
|
||||
await this.signerPromise;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (this.readyPromise) {
|
||||
try {
|
||||
await this.readyPromise;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
public cancelNostrConnect() {
|
||||
this.releaseSigner();
|
||||
this.resetAuth();
|
||||
}
|
||||
|
||||
public async nostrConnect(
|
||||
relay?: string,
|
||||
{
|
||||
domain = '',
|
||||
link = '',
|
||||
iframeUrl = '',
|
||||
importConnect = false,
|
||||
}: {
|
||||
domain?: string;
|
||||
link?: string;
|
||||
importConnect?: boolean;
|
||||
iframeUrl?: string;
|
||||
} = {},
|
||||
) {
|
||||
relay = relay || DEFAULT_NOSTRCONNECT_RELAY;
|
||||
|
||||
const info: Info = {
|
||||
authMethod: 'connect',
|
||||
pubkey: '', // unknown yet!
|
||||
signerPubkey: '', // unknown too!
|
||||
sk: this.nostrConnectKey,
|
||||
domain: domain,
|
||||
relays: [relay],
|
||||
iframeUrl,
|
||||
};
|
||||
|
||||
console.log('nostrconnect info', info, link);
|
||||
|
||||
// non-iframe flow
|
||||
if (link && !iframeUrl) window.open(link, '_blank', 'width=400,height=700');
|
||||
|
||||
// init nip46 signer
|
||||
await this.initSigner(info, { listen: true });
|
||||
|
||||
// signer learns the remote pubkey
|
||||
if (!info.pubkey || !info.signerPubkey) throw new Error('Bad remote pubkey');
|
||||
|
||||
info.bunkerUrl = `bunker://${info.signerPubkey}?relay=${relay}`;
|
||||
|
||||
// callback
|
||||
if (!importConnect) this.onAuth('login', info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public async createNostrConnect(relay?: string) {
|
||||
this.nostrConnectKey = generatePrivateKey();
|
||||
this.nostrConnectSecret = Math.random().toString(36).substring(7);
|
||||
|
||||
const pubkey = getPublicKey(this.nostrConnectKey);
|
||||
const meta = {
|
||||
name: encodeURIComponent(document.location.host),
|
||||
url: encodeURIComponent(document.location.origin),
|
||||
icon: encodeURIComponent(await getIcon()),
|
||||
perms: encodeURIComponent(this.params.optionsModal.perms || ''),
|
||||
};
|
||||
|
||||
return `nostrconnect://${pubkey}?image=${meta.icon}&url=${meta.url}&name=${meta.name}&perms=${meta.perms}&secret=${this.nostrConnectSecret}${relay ? `&relay=${relay}` : ''}`;
|
||||
}
|
||||
|
||||
public async getNostrConnectServices(): Promise<[string, ConnectionString[]]> {
|
||||
const nostrconnect = await this.createNostrConnect();
|
||||
|
||||
// copy defaults
|
||||
const apps = NOSTRCONNECT_APPS.map(a => ({ ...a }));
|
||||
// if (this.params.optionsModal.dev) {
|
||||
// apps.push({
|
||||
// name: 'Dev.Nsec.app',
|
||||
// domain: 'new.nsec.app',
|
||||
// canImport: true,
|
||||
// img: 'https://new.nsec.app/assets/favicon.ico',
|
||||
// link: 'https://dev.nsec.app/<nostrconnect>',
|
||||
// relay: 'wss://relay.nsec.app/',
|
||||
// });
|
||||
// }
|
||||
|
||||
for (const a of apps) {
|
||||
let relay = DEFAULT_NOSTRCONNECT_RELAY;
|
||||
if (a.link.startsWith('https://')) {
|
||||
let domain = a.domain || new URL(a.link).hostname;
|
||||
try {
|
||||
const info = await (await fetch(`https://${domain}/.well-known/nostr.json`)).json();
|
||||
const pubkey = info.names['_'];
|
||||
const relays = info.nip46[pubkey] as string[];
|
||||
if (relays && relays.length) relay = relays[0];
|
||||
a.iframeUrl = info.nip46.iframe_url || '';
|
||||
} catch (e) {
|
||||
console.log('Bad app info', e, a);
|
||||
}
|
||||
}
|
||||
const nc = nostrconnect + '&relay=' + relay;
|
||||
if (a.iframeUrl) {
|
||||
// pass plain nc url for iframe-based flow
|
||||
a.link = nc;
|
||||
} else {
|
||||
// we will open popup ourselves
|
||||
a.link = a.link.replace('<nostrconnect>', nc);
|
||||
}
|
||||
}
|
||||
|
||||
return [nostrconnect, apps];
|
||||
}
|
||||
|
||||
public async localSignup(name: string, sk?: string) {
|
||||
const signup = !sk;
|
||||
sk = sk || generatePrivateKey();
|
||||
const pubkey = getPublicKey(sk);
|
||||
const info: Info = {
|
||||
pubkey,
|
||||
sk,
|
||||
name,
|
||||
authMethod: 'local',
|
||||
};
|
||||
console.log(`localSignup name: ${name}`);
|
||||
await this.setLocal(info, signup);
|
||||
}
|
||||
|
||||
public async setLocal(info: Info, signup?: boolean) {
|
||||
this.releaseSigner();
|
||||
this.localSigner = new PrivateKeySigner(info.sk!);
|
||||
|
||||
if (signup) await createProfile(info, this.profileNdk, this.localSigner, this.params.optionsModal.signupRelays, this.params.optionsModal.outboxRelays);
|
||||
|
||||
this.onAuth(signup ? 'signup' : 'login', info);
|
||||
}
|
||||
|
||||
public prepareImportUrl(url: string) {
|
||||
// for OTP we choose interactive import
|
||||
if (this.params.userInfo?.authMethod === 'otp') return url + '&import=true';
|
||||
|
||||
// for local we export our existing key
|
||||
if (!this.localSigner || this.params.userInfo?.authMethod !== 'local') throw new Error('Most be local keys');
|
||||
return url + '#import=' + nip19.nsecEncode(this.localSigner.privateKey!);
|
||||
}
|
||||
|
||||
public async importAndConnect(cs: ConnectionString) {
|
||||
const { relay, domain, link, iframeUrl } = cs;
|
||||
if (!domain) throw new Error('Domain required');
|
||||
|
||||
const info = await this.nostrConnect(relay, { domain, link, importConnect: true, iframeUrl });
|
||||
|
||||
// logout to remove local keys from storage
|
||||
// but keep the connect signer
|
||||
await this.logout(/*keepSigner*/ true);
|
||||
|
||||
// release local one
|
||||
this.localSigner = null;
|
||||
|
||||
// notify app that we've switched to 'connect' keys
|
||||
this.onAuth('login', info);
|
||||
}
|
||||
|
||||
public setReadOnly(pubkey: string) {
|
||||
const info: Info = { pubkey, authMethod: 'readOnly' };
|
||||
this.onAuth('login', info);
|
||||
}
|
||||
|
||||
public setExtension(pubkey: string) {
|
||||
const info: Info = { pubkey, authMethod: 'extension' };
|
||||
this.onAuth('login', info);
|
||||
}
|
||||
|
||||
public setOTP(pubkey: string, data: string) {
|
||||
const info: Info = { pubkey, authMethod: 'otp', otpData: data };
|
||||
this.onAuth('login', info);
|
||||
}
|
||||
|
||||
public async setConnect(info: Info) {
|
||||
this.releaseSigner();
|
||||
await this.startAuth();
|
||||
await this.initSigner(info);
|
||||
this.onAuth('login', info);
|
||||
await this.endAuth();
|
||||
}
|
||||
|
||||
public async createAccount(nip05: string) {
|
||||
const [name, domain] = nip05.split('@');
|
||||
|
||||
// bunker's own url
|
||||
const bunkerUrl = await getBunkerUrl(`_@${domain}`, this.params.optionsModal);
|
||||
console.log("create account bunker's url", bunkerUrl);
|
||||
|
||||
// parse bunker url and generate local nsec
|
||||
const info = bunkerUrlToInfo(bunkerUrl);
|
||||
if (!info.signerPubkey) throw new Error('Bad bunker url');
|
||||
|
||||
const eventToAddAccount = Boolean(this.params.userInfo);
|
||||
|
||||
// init signer to talk to the bunker (not the user!)
|
||||
await this.initSigner(info, { eventToAddAccount });
|
||||
|
||||
const userPubkey = await this.signer!.createAccount2({ bunkerPubkey: info.signerPubkey!, name, domain, perms: this.params.optionsModal.perms });
|
||||
|
||||
return {
|
||||
bunkerUrl: `bunker://${userPubkey}?relay=${info.relays?.[0]}`,
|
||||
sk: info.sk, // reuse the same local key
|
||||
};
|
||||
}
|
||||
|
||||
private releaseSigner() {
|
||||
this.signer = null;
|
||||
this.signerErrCallback?.('cancelled');
|
||||
this.localSigner = null;
|
||||
|
||||
// disconnect from signer relays
|
||||
for (const r of this.ndk.pool.relays.keys()) {
|
||||
this.ndk.pool.removeRelay(r);
|
||||
}
|
||||
}
|
||||
|
||||
public async logout(keepSigner = false) {
|
||||
if (!keepSigner) this.releaseSigner();
|
||||
|
||||
// move current to recent
|
||||
localStorageRemoveCurrentAccount();
|
||||
|
||||
// notify everyone
|
||||
this.onAuth('logout');
|
||||
|
||||
this.emit('updateAccounts');
|
||||
}
|
||||
|
||||
private setUserInfo(userInfo: Info | null) {
|
||||
this.params.userInfo = userInfo;
|
||||
this.emit('onUserInfo', userInfo);
|
||||
|
||||
if (userInfo) {
|
||||
localStorageAddAccount(userInfo);
|
||||
this.emit('updateAccounts');
|
||||
}
|
||||
}
|
||||
|
||||
public exportKeys() {
|
||||
if (!this.params.userInfo) return '';
|
||||
if (this.params.userInfo.authMethod !== 'local') return '';
|
||||
return nip19.nsecEncode(this.params.userInfo.sk!);
|
||||
}
|
||||
|
||||
private onAuth(type: 'login' | 'signup' | 'logout', info: Info | null = null) {
|
||||
if (type !== 'logout' && !info) throw new Error('No user info in onAuth');
|
||||
|
||||
// make sure we emulate logout first
|
||||
if (info && this.params.userInfo && (info.pubkey !== this.params.userInfo.pubkey || info.authMethod !== this.params.userInfo.authMethod)) {
|
||||
const event = new CustomEvent('nlAuth', { detail: { type: 'logout' } });
|
||||
console.log('nostr-login auth', event.detail);
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
this.setUserInfo(info);
|
||||
|
||||
if (info) {
|
||||
// async profile fetch
|
||||
fetchProfile(info, this.profileNdk).then(p => {
|
||||
if (this.params.userInfo !== info) return;
|
||||
|
||||
const userInfo = {
|
||||
...this.params.userInfo,
|
||||
picture: p?.image || p?.picture,
|
||||
name: p?.name || p?.displayName || p?.nip05 || nip19.npubEncode(info.pubkey),
|
||||
// NOTE: do not overwrite info.nip05 with the one from profile!
|
||||
// info.nip05 refers to nip46 provider,
|
||||
// profile.nip05 is just a fancy name that user has chosen
|
||||
// nip05: p?.nip05
|
||||
};
|
||||
|
||||
this.setUserInfo(userInfo);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const npub = info ? nip19.npubEncode(info.pubkey) : '';
|
||||
|
||||
const options: NostrLoginAuthOptions = {
|
||||
type,
|
||||
};
|
||||
|
||||
if (type === 'logout') {
|
||||
// reset
|
||||
if (this.iframe) this.iframe.remove();
|
||||
this.iframe = undefined;
|
||||
} else {
|
||||
options.pubkey = info!.pubkey;
|
||||
options.name = info!.name;
|
||||
|
||||
if (info!.sk) {
|
||||
options.localNsec = nip19.nsecEncode(info!.sk);
|
||||
}
|
||||
|
||||
if (info!.relays) {
|
||||
options.relays = info!.relays;
|
||||
}
|
||||
|
||||
if (info!.otpData) {
|
||||
options.otpData = info!.otpData;
|
||||
}
|
||||
|
||||
options.method = info!.authMethod || 'connect';
|
||||
}
|
||||
|
||||
const event = new CustomEvent('nlAuth', { detail: options });
|
||||
console.log('nostr-login auth', options);
|
||||
document.dispatchEvent(event);
|
||||
|
||||
if (this.params.optionsModal.onAuth) {
|
||||
this.params.optionsModal.onAuth(npub, options);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('onAuth error', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async createIframe(iframeUrl?: string) {
|
||||
if (!iframeUrl) return undefined;
|
||||
|
||||
// ensure iframe
|
||||
const url = new URL(iframeUrl);
|
||||
const domain = url.hostname;
|
||||
let iframe: HTMLIFrameElement | undefined;
|
||||
|
||||
// one iframe per domain
|
||||
const did = domain.replaceAll('.', '-');
|
||||
const id = '__nostr-login-worker-iframe-' + did;
|
||||
iframe = document.querySelector(`#${id}`) as HTMLIFrameElement;
|
||||
console.log('iframe', id, iframe);
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('width', '0');
|
||||
iframe.setAttribute('height', '0');
|
||||
iframe.setAttribute('border', '0');
|
||||
iframe.style.display = 'none';
|
||||
// iframe.setAttribute('sandbox', 'allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts');
|
||||
iframe.id = id;
|
||||
document.body.append(iframe);
|
||||
}
|
||||
|
||||
// wait until loaded
|
||||
iframe.setAttribute('src', iframeUrl);
|
||||
|
||||
// we start listening right now to avoid races
|
||||
// with 'load' event below
|
||||
const ready = new ReadyListener(['workerReady', 'workerError'], url.origin);
|
||||
|
||||
await new Promise(ok => {
|
||||
iframe!.addEventListener('load', ok);
|
||||
});
|
||||
|
||||
// now make sure the iframe is ready,
|
||||
// timeout timer starts here
|
||||
const r = await ready.wait();
|
||||
|
||||
// FIXME wait until the iframe is ready to accept requests,
|
||||
// maybe it should send us some message?
|
||||
|
||||
console.log('nostr-login iframe ready', iframeUrl, r);
|
||||
|
||||
return { iframe, port: r[1] as MessagePort };
|
||||
}
|
||||
|
||||
// private async getIframeUrl(domain?: string) {
|
||||
// if (!domain) return '';
|
||||
// try {
|
||||
// const r = await fetch(`https://${domain}/.well-known/nostr.json`);
|
||||
// const data = await r.json();
|
||||
// return data.nip46?.iframe_url || '';
|
||||
// } catch (e) {
|
||||
// console.log('failed to fetch iframe url', e, domain);
|
||||
// return '';
|
||||
// }
|
||||
// }
|
||||
|
||||
public async sendNeedAuth() {
|
||||
const [nostrconnect] = await this.getNostrConnectServices();
|
||||
const event = new CustomEvent('nlNeedAuth', { detail: { nostrconnect } });
|
||||
console.log('nostr-login need auth', nostrconnect);
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
public isAuthing() {
|
||||
return !!this.readyCallback;
|
||||
}
|
||||
|
||||
public async startAuth() {
|
||||
console.log("startAuth");
|
||||
if (this.readyCallback) throw new Error('Already started');
|
||||
|
||||
// start the new promise
|
||||
this.readyPromise = new Promise<void>(ok => (this.readyCallback = ok));
|
||||
}
|
||||
|
||||
public async endAuth() {
|
||||
console.log('endAuth', this.params.userInfo);
|
||||
if (this.params.userInfo && this.params.userInfo.iframeUrl) {
|
||||
// create iframe
|
||||
const { iframe, port } = (await this.createIframe(this.params.userInfo.iframeUrl)) || {};
|
||||
this.iframe = iframe;
|
||||
if (!this.iframe || !port) return;
|
||||
|
||||
// assign iframe to RPC object
|
||||
(this.signer!.rpc as IframeNostrRpc).setWorkerIframePort(port);
|
||||
}
|
||||
|
||||
this.readyCallback!();
|
||||
this.readyCallback = undefined;
|
||||
}
|
||||
|
||||
public resetAuth() {
|
||||
if (this.readyCallback) this.readyCallback();
|
||||
this.readyCallback = undefined;
|
||||
}
|
||||
|
||||
private async listen(info: Info) {
|
||||
if (!info.iframeUrl) return this.signer!.listen(this.nostrConnectSecret);
|
||||
const r = await this.starterReady!.wait();
|
||||
if (r[0] === 'starterError') throw new Error(r[1]);
|
||||
return this.signer!.setListenReply(r[1], this.nostrConnectSecret);
|
||||
}
|
||||
|
||||
public async connect(info: Info, perms?: string) {
|
||||
return this.signer!.connect(info.token, perms);
|
||||
}
|
||||
|
||||
public async initSigner(info: Info, { listen = false, connect = false, eventToAddAccount = false } = {}) {
|
||||
// mutex
|
||||
if (this.signerPromise) {
|
||||
try {
|
||||
await this.signerPromise;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// we remove support for iframe from nip05 and bunker-url methods,
|
||||
// only nostrconnect flow will use it.
|
||||
// info.iframeUrl = info.iframeUrl || (await this.getIframeUrl(info.domain));
|
||||
console.log('initSigner info', info);
|
||||
|
||||
// start listening for the ready signal
|
||||
const iframeOrigin = info.iframeUrl ? new URL(info.iframeUrl!).origin : undefined;
|
||||
if (iframeOrigin) this.starterReady = new ReadyListener(['starterDone', 'starterError'], iframeOrigin);
|
||||
|
||||
// notify modals so they could show the starter iframe,
|
||||
// FIXME shouldn't this come from nostrconnect service list?
|
||||
this.emit('onIframeUrl', info.iframeUrl);
|
||||
|
||||
this.signerPromise = new Promise<void>(async (ok, err) => {
|
||||
this.signerErrCallback = err;
|
||||
try {
|
||||
// pre-connect if we're creating the connection (listen|connect) or
|
||||
// not iframe mode
|
||||
if (info.relays && !info.iframeUrl) {
|
||||
for (const r of info.relays) {
|
||||
this.ndk.addExplicitRelay(r, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// wait until we connect, otherwise
|
||||
// signer won't start properly
|
||||
await this.ndk.connect();
|
||||
|
||||
// create and prepare the signer
|
||||
const localSigner = new PrivateKeySigner(info.sk!);
|
||||
this.signer = new Nip46Signer(this.ndk, localSigner, info.signerPubkey!, iframeOrigin);
|
||||
|
||||
// we should notify the banner the same way as
|
||||
// the onAuthUrl does
|
||||
this.signer.on(`iframeRestart`, async () => {
|
||||
const iframeUrl = info.iframeUrl + (info.iframeUrl!.includes('?') ? '&' : '?') + 'pubkey=' + info.pubkey + '&rebind=' + localSigner.pubkey;
|
||||
this.emit('iframeRestart', { pubkey: info.pubkey, iframeUrl });
|
||||
});
|
||||
|
||||
// OAuth flow
|
||||
// if (!listen) {
|
||||
this.signer.on('authUrl', (url: string) => {
|
||||
console.log('nostr login auth url', url);
|
||||
|
||||
// notify our UI
|
||||
this.emit('onAuthUrl', { url, iframeUrl: info.iframeUrl, eventToAddAccount });
|
||||
});
|
||||
// }
|
||||
|
||||
if (listen) {
|
||||
// nostrconnect: flow
|
||||
// wait for the incoming message from signer
|
||||
await this.listen(info);
|
||||
} else if (connect) {
|
||||
// bunker: flow
|
||||
// send 'connect' message to signer
|
||||
await this.connect(info, this.params.optionsModal.perms);
|
||||
} else {
|
||||
// provide saved pubkey as a hint
|
||||
await this.signer!.initUserPubkey(info.pubkey);
|
||||
}
|
||||
|
||||
// ensure, we're using it in callbacks above
|
||||
// and expect info to be valid after this call
|
||||
info.pubkey = this.signer!.userPubkey;
|
||||
// learned after nostrconnect flow
|
||||
info.signerPubkey = this.signer!.remotePubkey;
|
||||
|
||||
ok();
|
||||
} catch (e) {
|
||||
console.log('initSigner failure', e);
|
||||
// make sure signer isn't set
|
||||
this.signer = null;
|
||||
err(e);
|
||||
}
|
||||
});
|
||||
|
||||
return this.signerPromise;
|
||||
}
|
||||
|
||||
public async authNip46(
|
||||
type: 'login' | 'signup',
|
||||
{ name, bunkerUrl, sk = '', domain = '', iframeUrl = '' }: { name: string; bunkerUrl: string; sk?: string; domain?: string; iframeUrl?: string },
|
||||
) {
|
||||
try {
|
||||
const info = bunkerUrlToInfo(bunkerUrl, sk);
|
||||
if (isBunkerUrl(name)) info.bunkerUrl = name;
|
||||
else {
|
||||
info.nip05 = name;
|
||||
info.domain = name.split('@')[1];
|
||||
}
|
||||
if (domain) info.domain = domain;
|
||||
if (iframeUrl) info.iframeUrl = iframeUrl;
|
||||
|
||||
// console.log('nostr login auth info', info);
|
||||
if (!info.signerPubkey || !info.sk || !info.relays?.[0]) {
|
||||
throw new Error(`Bad bunker url ${bunkerUrl}`);
|
||||
}
|
||||
|
||||
const eventToAddAccount = Boolean(this.params.userInfo);
|
||||
console.log('authNip46', type, info);
|
||||
|
||||
// updates the info
|
||||
await this.initSigner(info, { connect: true, eventToAddAccount });
|
||||
|
||||
// callback
|
||||
this.onAuth(type, info);
|
||||
} catch (e) {
|
||||
console.log('nostr login auth failed', e);
|
||||
// make ure it's closed
|
||||
// this.popupManager.closePopup();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async signEvent(event: any) {
|
||||
if (this.localSigner) {
|
||||
event.pubkey = getPublicKey(this.localSigner.privateKey!);
|
||||
event.id = getEventHash(event);
|
||||
event.sig = await this.localSigner.sign(event);
|
||||
} else {
|
||||
event.pubkey = this.signer?.remotePubkey;
|
||||
event.id = getEventHash(event);
|
||||
event.sig = await this.signer?.sign(event);
|
||||
}
|
||||
console.log('signed', { event });
|
||||
return event;
|
||||
}
|
||||
|
||||
private async codec_call(method: string, pubkey: string, param: string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.signer!.rpc.sendRequest(this.signer!.remotePubkey!, method, [pubkey, param], 24133, (response: NDKRpcResponse) => {
|
||||
if (!response.error) {
|
||||
resolve(response.result);
|
||||
} else {
|
||||
reject(response.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async encrypt04(pubkey: string, plaintext: string) {
|
||||
if (this.localSigner) {
|
||||
return this.localSigner.encrypt(new NDKUser({ pubkey }), plaintext);
|
||||
} else {
|
||||
return this.signer!.encrypt(new NDKUser({ pubkey }), plaintext);
|
||||
}
|
||||
}
|
||||
|
||||
public async decrypt04(pubkey: string, ciphertext: string) {
|
||||
if (this.localSigner) {
|
||||
return this.localSigner.decrypt(new NDKUser({ pubkey }), ciphertext);
|
||||
} else {
|
||||
// decrypt is broken in ndk v2.3.1, and latest
|
||||
// ndk v2.8.1 doesn't allow to override connect easily,
|
||||
// so we reimplement and fix decrypt here as a temporary fix
|
||||
|
||||
return this.codec_call('nip04_decrypt', pubkey, ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
public async encrypt44(pubkey: string, plaintext: string) {
|
||||
if (this.localSigner) {
|
||||
return this.nip44Codec.encrypt(this.localSigner.privateKey!, pubkey, plaintext);
|
||||
} else {
|
||||
// no support of nip44 in ndk yet
|
||||
return this.codec_call('nip44_encrypt', pubkey, plaintext);
|
||||
}
|
||||
}
|
||||
|
||||
public async decrypt44(pubkey: string, ciphertext: string) {
|
||||
if (this.localSigner) {
|
||||
return this.nip44Codec.decrypt(this.localSigner.privateKey!, pubkey, ciphertext);
|
||||
} else {
|
||||
// no support of nip44 in ndk yet
|
||||
return this.codec_call('nip44_decrypt', pubkey, ciphertext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthNostrService;
|
||||
@@ -1,146 +0,0 @@
|
||||
import { NostrLoginOptions, TypeBanner } from '../types';
|
||||
import { NostrParams } from '.';
|
||||
import { Info } from 'nostr-login-components/dist/types/types';
|
||||
import { EventEmitter } from 'tseep';
|
||||
import { getDarkMode } from '../utils';
|
||||
import { ReadyListener } from './Nip46';
|
||||
|
||||
class BannerManager extends EventEmitter {
|
||||
private banner: TypeBanner | null = null;
|
||||
private iframeReady?: ReadyListener;
|
||||
|
||||
private params: NostrParams;
|
||||
|
||||
constructor(params: NostrParams) {
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public onAuthUrl(url: string, iframeUrl: string) {
|
||||
if (this.banner) {
|
||||
if (url)
|
||||
this.banner.notify = {
|
||||
mode: iframeUrl ? 'iframeAuthUrl' : 'authUrl',
|
||||
url,
|
||||
};
|
||||
else
|
||||
this.banner.notify = {
|
||||
mode: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public onIframeRestart(iframeUrl: string) {
|
||||
if (this.banner) {
|
||||
this.iframeReady = new ReadyListener(['rebinderDone', 'rebinderError'], new URL(iframeUrl).origin);
|
||||
this.banner.notify = {
|
||||
mode: 'rebind',
|
||||
url: iframeUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public onUserInfo(info: Info | null) {
|
||||
if (this.banner) {
|
||||
this.banner.userInfo = info;
|
||||
}
|
||||
}
|
||||
|
||||
public onCallTimeout() {
|
||||
if (this.banner) {
|
||||
this.banner.notify = {
|
||||
mode: 'timeout',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public onCallStart() {
|
||||
if (this.banner) {
|
||||
this.banner.isLoading = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async onCallEnd() {
|
||||
if (this.banner) {
|
||||
if (this.iframeReady) {
|
||||
await this.iframeReady.wait();
|
||||
this.iframeReady = undefined;
|
||||
}
|
||||
this.banner.isLoading = false;
|
||||
this.banner.notify = { mode: '' };
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdateAccounts(accounts: Info[]) {
|
||||
if (this.banner) {
|
||||
this.banner.accounts = accounts;
|
||||
}
|
||||
}
|
||||
|
||||
public onDarkMode(dark: boolean) {
|
||||
if (this.banner) this.banner.darkMode = dark;
|
||||
}
|
||||
|
||||
public launchAuthBanner(opt: NostrLoginOptions) {
|
||||
this.banner = document.createElement('nl-banner');
|
||||
|
||||
this.banner.setAttribute('dark-mode', String(getDarkMode(opt)));
|
||||
|
||||
if (opt.theme) this.banner.setAttribute('theme', opt.theme);
|
||||
if (opt.noBanner) this.banner.setAttribute('hidden-mode', 'true');
|
||||
|
||||
this.banner.addEventListener('handleLoginBanner', (event: any) => {
|
||||
this.emit('launch', event.detail);
|
||||
});
|
||||
|
||||
this.banner.addEventListener('handleConfirmLogout', () => {
|
||||
this.emit('onConfirmLogout');
|
||||
});
|
||||
|
||||
this.banner.addEventListener('handleLogoutBanner', async () => {
|
||||
this.emit('logout');
|
||||
});
|
||||
|
||||
this.banner.addEventListener('handleImportModal', (event: any) => {
|
||||
this.emit('import');
|
||||
});
|
||||
|
||||
this.banner.addEventListener('handleNotifyConfirmBanner', (event: any) => {
|
||||
this.emit('onAuthUrlClick', event.detail);
|
||||
});
|
||||
|
||||
this.banner.addEventListener('handleNotifyConfirmBannerIframe', (event: any) => {
|
||||
this.emit('onIframeAuthUrlClick', event.detail);
|
||||
});
|
||||
|
||||
this.banner.addEventListener('handleSwitchAccount', (event: any) => {
|
||||
this.emit('onSwitchAccount', event.detail);
|
||||
});
|
||||
|
||||
this.banner.addEventListener('handleOpenWelcomeModal', () => {
|
||||
this.emit('launch');
|
||||
|
||||
if (this.banner) {
|
||||
this.banner.isOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
// this.banner.addEventListener('handleRetryConfirmBanner', () => {
|
||||
// const url = this.listNotifies.pop();
|
||||
// // FIXME go to nip05 domain?
|
||||
// if (!url) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (this.banner) {
|
||||
// this.banner.listNotifies = this.listNotifies;
|
||||
// }
|
||||
|
||||
// this.emit('onAuthUrlClick', url);
|
||||
// });
|
||||
|
||||
document.body.appendChild(this.banner);
|
||||
}
|
||||
}
|
||||
|
||||
export default BannerManager;
|
||||
@@ -1,635 +0,0 @@
|
||||
import { NostrLoginOptions, StartScreens, TypeModal } from '../types';
|
||||
import { checkNip05, getBunkerUrl, getDarkMode, localStorageRemoveRecent, localStorageSetItem, prepareSignupRelays } from '../utils';
|
||||
import { AuthNostrService, NostrExtensionService, NostrParams } from '.';
|
||||
import { EventEmitter } from 'tseep';
|
||||
import { ConnectionString, Info, RecentType } from 'nostr-login-components/dist/types/types';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { setDarkMode } from '..';
|
||||
|
||||
class ModalManager extends EventEmitter {
|
||||
private modal: TypeModal | null = null;
|
||||
private params: NostrParams;
|
||||
private extensionService: NostrExtensionService;
|
||||
private authNostrService: AuthNostrService;
|
||||
private launcherPromise?: Promise<void>;
|
||||
private accounts: Info[] = [];
|
||||
private recents: RecentType[] = [];
|
||||
private opt?: NostrLoginOptions;
|
||||
|
||||
constructor(params: NostrParams, authNostrService: AuthNostrService, extensionManager: NostrExtensionService) {
|
||||
super();
|
||||
this.params = params;
|
||||
this.extensionService = extensionManager;
|
||||
this.authNostrService = authNostrService;
|
||||
}
|
||||
|
||||
public async waitReady() {
|
||||
if (this.launcherPromise) {
|
||||
try {
|
||||
await this.launcherPromise;
|
||||
} catch {}
|
||||
this.launcherPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async launch(opt: NostrLoginOptions) {
|
||||
console.log('nostr-login launch', opt);
|
||||
// mutex
|
||||
if (this.launcherPromise) await this.waitReady();
|
||||
|
||||
// hmm?!
|
||||
if (this.authNostrService.isAuthing()) this.authNostrService.resetAuth();
|
||||
|
||||
this.opt = opt;
|
||||
|
||||
const dialog = document.createElement('dialog');
|
||||
this.modal = document.createElement('nl-auth');
|
||||
this.modal.accounts = this.accounts;
|
||||
this.modal.recents = this.recents;
|
||||
|
||||
this.modal.setAttribute('dark-mode', String(getDarkMode(opt)));
|
||||
|
||||
if (opt.theme) {
|
||||
this.modal.setAttribute('theme', opt.theme);
|
||||
}
|
||||
|
||||
if (opt.startScreen) {
|
||||
this.modal.setAttribute('start-screen', opt.startScreen);
|
||||
}
|
||||
|
||||
if (opt.bunkers) {
|
||||
this.modal.setAttribute('bunkers', opt.bunkers);
|
||||
} else {
|
||||
let bunkers = 'nsec.app,highlighter.com';
|
||||
// if (opt.dev) bunkers += ',new.nsec.app';
|
||||
this.modal.setAttribute('bunkers', bunkers);
|
||||
}
|
||||
|
||||
if (opt.methods !== undefined) {
|
||||
this.modal.authMethods = opt.methods;
|
||||
}
|
||||
|
||||
if (opt.localSignup !== undefined) {
|
||||
this.modal.localSignup = opt.localSignup;
|
||||
}
|
||||
|
||||
if (opt.signupNstart !== undefined) {
|
||||
this.modal.signupNjump = opt.signupNstart;
|
||||
}
|
||||
|
||||
if (opt.title) {
|
||||
this.modal.welcomeTitle = opt.title;
|
||||
}
|
||||
|
||||
if (opt.description) {
|
||||
this.modal.welcomeDescription = opt.description;
|
||||
}
|
||||
|
||||
this.modal.hasExtension = this.extensionService.hasExtension();
|
||||
this.modal.hasOTP = !!opt.otpRequestUrl && !!opt.otpReplyUrl;
|
||||
|
||||
this.modal.isLoadingExtension = false;
|
||||
this.modal.isLoading = false;
|
||||
|
||||
[this.modal.connectionString, this.modal.connectionStringServices] = await this.authNostrService.getNostrConnectServices();
|
||||
|
||||
dialog.appendChild(this.modal);
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
let otpPubkey = '';
|
||||
|
||||
this.launcherPromise = new Promise<void>((ok, err) => {
|
||||
dialog.addEventListener('close', () => {
|
||||
// noop if already resolved
|
||||
err(new Error('Closed'));
|
||||
|
||||
this.authNostrService.resetAuth();
|
||||
|
||||
if (this.modal) {
|
||||
// it's reset on modal creation
|
||||
// // reset state
|
||||
// this.modal.isLoading = false;
|
||||
// this.modal.authUrl = '';
|
||||
// this.modal.iframeUrl = '';
|
||||
// this.modal.error = '';
|
||||
// this.modal.isLoadingExtension = false;
|
||||
|
||||
// drop it
|
||||
// @ts-ignore
|
||||
document.body.removeChild(this.modal.parentNode);
|
||||
this.modal = null;
|
||||
}
|
||||
});
|
||||
|
||||
const done = async (ok: () => void) => {
|
||||
if (this.modal) this.modal.isLoading = false;
|
||||
await this.authNostrService.endAuth();
|
||||
dialog.close();
|
||||
this.modal = null;
|
||||
ok();
|
||||
};
|
||||
|
||||
const exec = async (
|
||||
body: () => Promise<void>,
|
||||
options?: {
|
||||
start?: boolean;
|
||||
end?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (this.modal) {
|
||||
this.modal.isLoading = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!options || options.start) await this.authNostrService.startAuth();
|
||||
await body();
|
||||
if (!options || options.end) await done(ok);
|
||||
} catch (e: any) {
|
||||
console.log('error', e);
|
||||
if (this.modal) {
|
||||
this.modal.isLoading = false;
|
||||
this.modal.authUrl = '';
|
||||
this.modal.iframeUrl = '';
|
||||
if (e !== 'cancelled') this.modal.error = e.toString();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (name: string, domain?: string) => {
|
||||
await exec(async () => {
|
||||
// convert name to bunker url
|
||||
const bunkerUrl = await getBunkerUrl(name, this.params.optionsModal);
|
||||
|
||||
// connect to bunker by url
|
||||
await this.authNostrService.authNip46('login', { name, bunkerUrl, domain });
|
||||
});
|
||||
};
|
||||
|
||||
const signup = async (name: string) => {
|
||||
await exec(async () => {
|
||||
// create acc on service and get bunker url
|
||||
const { bunkerUrl, sk } = await this.authNostrService.createAccount(name);
|
||||
|
||||
// connect to bunker by url
|
||||
await this.authNostrService.authNip46('signup', { name, bunkerUrl, sk });
|
||||
});
|
||||
};
|
||||
|
||||
const exportKeys = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.authNostrService.exportKeys());
|
||||
localStorageSetItem('backupKey', 'true');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard: ', err);
|
||||
}
|
||||
};
|
||||
|
||||
const importKeys = async (cs: ConnectionString) => {
|
||||
await exec(async () => {
|
||||
const { iframeUrl } = cs;
|
||||
cs.link = this.authNostrService.prepareImportUrl(cs.link);
|
||||
|
||||
if (this.modal && iframeUrl) {
|
||||
// we pass the link down to iframe so it could open it
|
||||
this.modal.authUrl = cs.link;
|
||||
this.modal.iframeUrl = iframeUrl;
|
||||
this.modal.isLoading = false;
|
||||
console.log('nostrconnect authUrl', this.modal.authUrl, this.modal.iframeUrl);
|
||||
}
|
||||
|
||||
await this.authNostrService.importAndConnect(cs);
|
||||
});
|
||||
};
|
||||
|
||||
const nostrConnect = async (cs?: ConnectionString) => {
|
||||
await exec(async () => {
|
||||
const { relay, domain, link, iframeUrl } = cs || {};
|
||||
console.log('nostrConnect', cs, relay, domain, link, iframeUrl);
|
||||
|
||||
if (this.modal) {
|
||||
if (iframeUrl) {
|
||||
// we pass the link down to iframe so it could open it
|
||||
this.modal.authUrl = link;
|
||||
this.modal.iframeUrl = iframeUrl;
|
||||
this.modal.isLoading = false;
|
||||
console.log('nostrconnect authUrl', this.modal.authUrl, this.modal.iframeUrl);
|
||||
}
|
||||
|
||||
if (!cs) this.modal.isLoading = false;
|
||||
}
|
||||
|
||||
await this.authNostrService.nostrConnect(relay, { domain, link, iframeUrl });
|
||||
});
|
||||
};
|
||||
|
||||
const localSignup = async (name?: string) => {
|
||||
await exec(async () => {
|
||||
if (!name) throw new Error('Please enter some nickname');
|
||||
await this.authNostrService.localSignup(name);
|
||||
});
|
||||
};
|
||||
|
||||
const signupNjump = async () => {
|
||||
await exec(async () => {
|
||||
const self = new URL(window.location.href);
|
||||
const name =
|
||||
self.hostname
|
||||
.toLocaleLowerCase()
|
||||
.replace(/^www\./i, '')
|
||||
.charAt(0)
|
||||
.toUpperCase() + self.hostname.slice(1);
|
||||
const relays = prepareSignupRelays(this.params.optionsModal.signupRelays);
|
||||
// const url = `https://start.njump.me/?an=${name}&at=popup&ac=${window.location.href}&s=${this.opt!.followNpubs || ''}&arr=${relays}&awr=${relays}`;
|
||||
// console.log('njump url', url);
|
||||
|
||||
this.modal!.njumpIframe = `
|
||||
<html><body>
|
||||
<script src='https://start.njump.me/modal.js'></script>
|
||||
<script>
|
||||
new NstartModal({
|
||||
baseUrl: 'https://start.njump.me',
|
||||
// Required parameters
|
||||
an: '${name}',
|
||||
// Optional parameters
|
||||
s: [${this.opt!.followNpubs ? `'${this.opt!.followNpubs}'` : ''}],
|
||||
afb: false, // forceBunker
|
||||
asb: false, // skipBunker
|
||||
aan: false, // avoidNsec
|
||||
aac: true, // avoidNcryptsec
|
||||
ahc: true, // hide close button
|
||||
arr: ${JSON.stringify(relays)}, //readRelays
|
||||
awr: ${JSON.stringify(relays)}, //writeRelays
|
||||
// Callbacks
|
||||
onComplete: (result) => {
|
||||
console.log('Login token:', result.nostrLogin);
|
||||
window.parent.location.href='${window.location.href}#nostr-login='+result.nostrLogin;
|
||||
},
|
||||
onCancel: () => {
|
||||
window.parent.location.href='${window.location.href}#nostr-login=null';
|
||||
},
|
||||
}).open();
|
||||
</script>
|
||||
</body></html>
|
||||
`.replaceAll('&', '&'); // needed?
|
||||
|
||||
return new Promise((ok, err) => {
|
||||
const process = async (nsecOrBunker: string) => {
|
||||
// process the returned value
|
||||
console.log('nsecOrBunker', nsecOrBunker);
|
||||
if (nsecOrBunker.startsWith('nsec1')) {
|
||||
let decoded;
|
||||
try {
|
||||
decoded = nip19.decode(nsecOrBunker);
|
||||
} catch (e) {
|
||||
throw new Error('Bad nsec value');
|
||||
}
|
||||
if (decoded.type !== 'nsec') throw new Error('Bad bech32 type');
|
||||
await this.authNostrService.localSignup('', decoded.data);
|
||||
ok();
|
||||
} else if (nsecOrBunker.startsWith('bunker:')) {
|
||||
await this.authNostrService.authNip46('login', { name: '', bunkerUrl: nsecOrBunker });
|
||||
ok();
|
||||
} else if (nsecOrBunker === 'null') {
|
||||
err('Cancelled');
|
||||
} else {
|
||||
err('Unknown return value');
|
||||
}
|
||||
};
|
||||
|
||||
const onOpen = async () => {
|
||||
if (window.location.hash.startsWith('#nostr-login=')) {
|
||||
const nsecOrBunker = window.location.hash.split('#nostr-login=')[1];
|
||||
|
||||
// clear hash from history
|
||||
const url = new URL(window.location.toString());
|
||||
url.hash = '';
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
|
||||
process(nsecOrBunker);
|
||||
}
|
||||
};
|
||||
|
||||
// // use random 'target' to make sure window.opener is
|
||||
// // accessible to the popup
|
||||
// window.open(url, '' + Date.now(), 'popup=true,width=600,height=950');
|
||||
window.addEventListener('hashchange', onOpen);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.modal) throw new Error('WTH?');
|
||||
|
||||
this.modal.addEventListener('handleContinue', () => {
|
||||
if (this.modal) {
|
||||
this.modal.isLoading = true;
|
||||
this.emit('onAuthUrlClick', this.modal.authUrl);
|
||||
}
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlLogin', (event: any) => {
|
||||
login(event.detail);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlSignup', (event: any) => {
|
||||
signup(event.detail);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlLocalSignup', (event: any) => {
|
||||
localSignup(event.detail);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlSignupNjump', (event: any) => {
|
||||
signupNjump();
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlImportAccount', (event: any) => {
|
||||
importKeys(event.detail);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlExportKeys', (event: any) => {
|
||||
exportKeys();
|
||||
});
|
||||
|
||||
this.modal.addEventListener('handleLogoutBanner', () => {
|
||||
this.emit('onLogoutBanner');
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlNostrConnect', (event: any) => {
|
||||
nostrConnect(event.detail);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlNostrConnectDefault', () => {
|
||||
// dedup the calls
|
||||
if (!this.authNostrService.isAuthing()) nostrConnect();
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlNostrConnectDefaultCancel', () => {
|
||||
console.log('nlNostrConnectDefaultCancel');
|
||||
this.authNostrService.cancelNostrConnect();
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlSwitchAccount', (event: any) => {
|
||||
const eventInfo: Info = event.detail as Info;
|
||||
|
||||
this.emit('onSwitchAccount', eventInfo);
|
||||
|
||||
// wait a bit, if dialog closes before
|
||||
// switching finishes then launched promise rejects
|
||||
|
||||
// FIXME this calls resetAuth which then prevents
|
||||
// endAuth from getting properly called. 300 is not
|
||||
// enough to init iframe, so there should be a
|
||||
// feedback from switchAccount here
|
||||
setTimeout(() => dialog.close(), 300);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlLoginRecentAccount', async (event: any) => {
|
||||
const userInfo: Info = event.detail as Info;
|
||||
|
||||
if (userInfo.authMethod === 'readOnly') {
|
||||
this.authNostrService.setReadOnly(userInfo.pubkey);
|
||||
dialog.close();
|
||||
} else if (userInfo.authMethod === 'otp') {
|
||||
try {
|
||||
this.modal!.dispatchEvent(
|
||||
new CustomEvent('nlLoginOTPUser', {
|
||||
detail: userInfo.nip05 || userInfo.pubkey,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else if (userInfo.authMethod === 'extension') {
|
||||
await this.extensionService.trySetExtensionForPubkey(userInfo.pubkey);
|
||||
dialog.close();
|
||||
} else {
|
||||
const input = userInfo.bunkerUrl || userInfo.nip05;
|
||||
if (!input) throw new Error('Bad connect info');
|
||||
login(input, userInfo.domain);
|
||||
}
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlRemoveRecent', (event: any) => {
|
||||
localStorageRemoveRecent(event.detail as RecentType);
|
||||
this.emit('updateAccounts');
|
||||
});
|
||||
|
||||
const nameToPubkey = async (nameNpub: string) => {
|
||||
let pubkey = '';
|
||||
if (nameNpub.includes('@')) {
|
||||
const { error, pubkey: nip05pubkey } = await checkNip05(nameNpub);
|
||||
if (nip05pubkey) pubkey = nip05pubkey;
|
||||
else throw new Error(error);
|
||||
} else if (nameNpub.startsWith('npub')) {
|
||||
const { type, data } = nip19.decode(nameNpub);
|
||||
if (type === 'npub') pubkey = data as string;
|
||||
else throw new Error('Bad npub');
|
||||
} else if (nameNpub.trim().length === 64) {
|
||||
pubkey = nameNpub.trim();
|
||||
nip19.npubEncode(pubkey); // check
|
||||
}
|
||||
return pubkey;
|
||||
};
|
||||
|
||||
this.modal.addEventListener('nlLoginReadOnly', async (event: any) => {
|
||||
await exec(async () => {
|
||||
const nameNpub = event.detail;
|
||||
const pubkey = await nameToPubkey(nameNpub);
|
||||
this.authNostrService.setReadOnly(pubkey);
|
||||
});
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlLoginExtension', async () => {
|
||||
if (!this.extensionService.hasExtension()) {
|
||||
throw new Error('No extension');
|
||||
}
|
||||
|
||||
await exec(async () => {
|
||||
if (!this.modal) return;
|
||||
this.modal.isLoadingExtension = true;
|
||||
await this.extensionService.setExtension();
|
||||
this.modal.isLoadingExtension = false;
|
||||
});
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlLoginOTPUser', async (event: any) => {
|
||||
await exec(
|
||||
async () => {
|
||||
if (!this.modal) return;
|
||||
|
||||
const nameNpub = event.detail;
|
||||
const pubkey = await nameToPubkey(nameNpub);
|
||||
const url = this.opt!.otpRequestUrl! + (this.opt!.otpRequestUrl!.includes('?') ? '&' : '?') + 'pubkey=' + pubkey;
|
||||
const r = await fetch(url);
|
||||
if (r.status !== 200) {
|
||||
console.warn('nostr-login: bad otp reply', r);
|
||||
throw new Error('Failed to send DM');
|
||||
}
|
||||
|
||||
// switch to 'enter code' mode
|
||||
this.modal.isOTP = true;
|
||||
|
||||
// remember for code handler below
|
||||
otpPubkey = pubkey;
|
||||
|
||||
// spinner off
|
||||
this.modal.isLoading = false;
|
||||
},
|
||||
{ start: true },
|
||||
);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlLoginOTPCode', async (event: any) => {
|
||||
await exec(
|
||||
async () => {
|
||||
if (!this.modal) return;
|
||||
const code = event.detail;
|
||||
const url = this.opt!.otpReplyUrl! + (this.opt!.otpRequestUrl!.includes('?') ? '&' : '?') + 'pubkey=' + otpPubkey + '&code=' + code;
|
||||
const r = await fetch(url);
|
||||
if (r.status !== 200) {
|
||||
console.warn('nostr-login: bad otp reply', r);
|
||||
throw new Error('Invalid code');
|
||||
}
|
||||
|
||||
const data = await r.text();
|
||||
this.authNostrService.setOTP(otpPubkey, data);
|
||||
|
||||
this.modal.isOTP = false;
|
||||
},
|
||||
{ end: true },
|
||||
);
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlCheckSignup', async (event: any) => {
|
||||
const { available, taken, error } = await checkNip05(event.detail);
|
||||
if (this.modal) {
|
||||
this.modal.error = String(error);
|
||||
|
||||
if (!error && taken) {
|
||||
this.modal.error = 'Already taken';
|
||||
}
|
||||
|
||||
this.modal.signupNameIsAvailable = available;
|
||||
}
|
||||
});
|
||||
|
||||
this.modal.addEventListener('nlCheckLogin', async (event: any) => {
|
||||
const { available, taken, error } = await checkNip05(event.detail);
|
||||
if (this.modal) {
|
||||
this.modal.error = String(error);
|
||||
if (available) {
|
||||
this.modal.error = 'Name not found';
|
||||
}
|
||||
this.modal.loginIsGood = taken;
|
||||
}
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
if (this.modal) {
|
||||
this.modal.isLoading = false;
|
||||
}
|
||||
|
||||
// this.authNostrService.cancelListenNostrConnect();
|
||||
|
||||
dialog.close();
|
||||
err(new Error('Cancelled'));
|
||||
};
|
||||
this.modal.addEventListener('stopFetchHandler', cancel);
|
||||
this.modal.addEventListener('nlCloseModal', cancel);
|
||||
|
||||
this.modal.addEventListener('nlChangeDarkMode', (event: any) => {
|
||||
setDarkMode(event.detail);
|
||||
document.dispatchEvent(new CustomEvent('nlDarkMode', { detail: event.detail }));
|
||||
});
|
||||
|
||||
this.on('onIframeAuthUrlCallEnd', () => {
|
||||
dialog.close();
|
||||
this.modal = null;
|
||||
ok();
|
||||
});
|
||||
|
||||
dialog.showModal();
|
||||
});
|
||||
|
||||
return this.launcherPromise;
|
||||
}
|
||||
|
||||
public async showIframeUrl(url: string) {
|
||||
// make sure we consume the previous promise,
|
||||
// otherwise launch will start await-ing
|
||||
// before modal is created and setting iframeUrl will fail
|
||||
await this.waitReady();
|
||||
|
||||
this.launch({
|
||||
startScreen: 'iframe' as StartScreens,
|
||||
}).catch(() => console.log('closed auth iframe'));
|
||||
|
||||
this.modal!.authUrl = url;
|
||||
}
|
||||
|
||||
public connectModals(defaultOpt: NostrLoginOptions) {
|
||||
const initialModals = async (opt: NostrLoginOptions) => {
|
||||
await this.launch(opt);
|
||||
};
|
||||
|
||||
const nlElements = document.getElementsByTagName('nl-button');
|
||||
|
||||
for (let i = 0; i < nlElements.length; i++) {
|
||||
const theme = nlElements[i].getAttribute('nl-theme');
|
||||
const startScreen = nlElements[i].getAttribute('start-screen');
|
||||
|
||||
const elementOpt = {
|
||||
...defaultOpt,
|
||||
};
|
||||
if (theme) elementOpt.theme = theme;
|
||||
|
||||
switch (startScreen as StartScreens) {
|
||||
case 'login':
|
||||
case 'login-bunker-url':
|
||||
case 'login-read-only':
|
||||
case 'signup':
|
||||
case 'switch-account':
|
||||
case 'welcome':
|
||||
elementOpt.startScreen = startScreen as StartScreens;
|
||||
}
|
||||
|
||||
nlElements[i].addEventListener('click', function () {
|
||||
initialModals(elementOpt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onAuthUrl(url: string) {
|
||||
if (this.modal) {
|
||||
this.modal.authUrl = url;
|
||||
this.modal.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onIframeUrl(url: string) {
|
||||
if (this.modal) {
|
||||
console.log('modal iframe url', url);
|
||||
this.modal.iframeUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
public onCallEnd() {
|
||||
if (this.modal && this.modal.authUrl && this.params.userInfo?.iframeUrl) {
|
||||
this.emit('onIframeAuthUrlCallEnd');
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdateAccounts(accounts: Info[], recents: RecentType[]) {
|
||||
this.accounts = accounts;
|
||||
this.recents = recents;
|
||||
if (!this.modal) return;
|
||||
this.modal.accounts = accounts;
|
||||
this.modal.recents = recents;
|
||||
}
|
||||
|
||||
public onDarkMode(dark: boolean) {
|
||||
if (this.modal) this.modal.darkMode = dark;
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalManager;
|
||||
@@ -1,429 +0,0 @@
|
||||
import NDK, { NDKEvent, NDKFilter, NDKNip46Signer, NDKNostrRpc, NDKRpcRequest, NDKRpcResponse, NDKSubscription, NDKSubscriptionCacheUsage, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||
import { validateEvent, verifySignature } from 'nostr-tools';
|
||||
import { PrivateKeySigner } from './Signer';
|
||||
|
||||
class NostrRpc extends NDKNostrRpc {
|
||||
protected _ndk: NDK;
|
||||
protected _signer: PrivateKeySigner;
|
||||
protected requests: Set<string> = new Set();
|
||||
private sub?: NDKSubscription;
|
||||
protected _useNip44: boolean = false;
|
||||
|
||||
public constructor(ndk: NDK, signer: PrivateKeySigner) {
|
||||
super(ndk, signer, ndk.debug.extend('nip46:signer:rpc'));
|
||||
this._ndk = ndk;
|
||||
this._signer = signer;
|
||||
}
|
||||
|
||||
public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
|
||||
// NOTE: fixing ndk
|
||||
filter.kinds = filter.kinds?.filter(k => k === 24133);
|
||||
this.sub = await super.subscribe(filter);
|
||||
return this.sub;
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.sub) {
|
||||
this.sub.stop();
|
||||
this.sub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public setUseNip44(useNip44: boolean) {
|
||||
this._useNip44 = useNip44;
|
||||
}
|
||||
|
||||
private isNip04(ciphertext: string) {
|
||||
const l = ciphertext.length;
|
||||
if (l < 28) return false;
|
||||
return ciphertext[l - 28] === '?' && ciphertext[l - 27] === 'i' && ciphertext[l - 26] === 'v' && ciphertext[l - 25] === '=';
|
||||
}
|
||||
|
||||
// override to auto-decrypt nip04/nip44
|
||||
public async parseEvent(event: NDKEvent): Promise<NDKRpcRequest | NDKRpcResponse> {
|
||||
const remoteUser = this._ndk.getUser({ pubkey: event.pubkey });
|
||||
remoteUser.ndk = this._ndk;
|
||||
const decrypt = this.isNip04(event.content) ? this._signer.decrypt : this._signer.decryptNip44;
|
||||
const decryptedContent = await decrypt.call(this._signer, remoteUser, event.content);
|
||||
const parsedContent = JSON.parse(decryptedContent);
|
||||
const { id, method, params, result, error } = parsedContent;
|
||||
|
||||
if (method) {
|
||||
return { id, pubkey: event.pubkey, method, params, event };
|
||||
} else {
|
||||
return { id, result, error, event };
|
||||
}
|
||||
}
|
||||
|
||||
public async parseNostrConnectReply(reply: any, secret: string) {
|
||||
const event = new NDKEvent(this._ndk, reply);
|
||||
const parsedEvent = await this.parseEvent(event);
|
||||
console.log('nostr connect parsedEvent', parsedEvent);
|
||||
if (!(parsedEvent as NDKRpcRequest).method) {
|
||||
const response = parsedEvent as NDKRpcResponse;
|
||||
if (response.result !== secret) throw new Error(response.error);
|
||||
return event.pubkey;
|
||||
} else {
|
||||
throw new Error('Bad nostr connect reply');
|
||||
}
|
||||
}
|
||||
|
||||
// ndk doesn't support nostrconnect:
|
||||
// we just listed to an unsolicited reply to
|
||||
// our pubkey and if it's ack/secret - we're fine
|
||||
public async listen(nostrConnectSecret: string): Promise<string> {
|
||||
const pubkey = this._signer.pubkey;
|
||||
console.log('nostr-login listening for conn to', pubkey);
|
||||
const sub = await this.subscribe({
|
||||
'kinds': [24133],
|
||||
'#p': [pubkey],
|
||||
});
|
||||
return new Promise<string>((ok, err) => {
|
||||
sub.on('event', async (event: NDKEvent) => {
|
||||
try {
|
||||
const parsedEvent = await this.parseEvent(event);
|
||||
// console.log('ack parsedEvent', parsedEvent);
|
||||
if (!(parsedEvent as NDKRpcRequest).method) {
|
||||
const response = parsedEvent as NDKRpcResponse;
|
||||
|
||||
// ignore
|
||||
if (response.result === 'auth_url') return;
|
||||
|
||||
// FIXME for now accept 'ack' replies, later on only
|
||||
// accept secrets
|
||||
if (response.result === 'ack' || response.result === nostrConnectSecret) {
|
||||
ok(event.pubkey);
|
||||
} else {
|
||||
err(response.error);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error parsing event', e, event.rawEvent());
|
||||
}
|
||||
// done
|
||||
this.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// since ndk doesn't yet support perms param
|
||||
// we reimplement the 'connect' call here
|
||||
// instead of await signer.blockUntilReady();
|
||||
public async connect(pubkey: string, token?: string, perms?: string) {
|
||||
return new Promise<void>((ok, err) => {
|
||||
const connectParams = [pubkey!, token || '', perms || ''];
|
||||
this.sendRequest(pubkey!, 'connect', connectParams, 24133, (response: NDKRpcResponse) => {
|
||||
if (response.result === 'ack') {
|
||||
ok();
|
||||
} else {
|
||||
err(response.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getId(): string {
|
||||
return Math.random().toString(36).substring(7);
|
||||
}
|
||||
|
||||
public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
|
||||
const id = this.getId();
|
||||
|
||||
// response handler will deduplicate auth urls and responses
|
||||
this.setResponseHandler(id, cb);
|
||||
|
||||
// create and sign request
|
||||
const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
|
||||
console.log("sendRequest", { event, method, remotePubkey, params });
|
||||
|
||||
// send to relays
|
||||
await event.publish();
|
||||
|
||||
// NOTE: ndk returns a promise that never resolves and
|
||||
// in fact REQUIRES cb to be provided (otherwise no way
|
||||
// to consume the result), we've already stepped on the bug
|
||||
// of waiting for this unresolvable result, so now we return
|
||||
// undefined to make sure waiters fail, not hang.
|
||||
// @ts-ignore
|
||||
return undefined as NDKRpcResponse;
|
||||
}
|
||||
|
||||
protected setResponseHandler(id: string, cb?: (res: NDKRpcResponse) => void) {
|
||||
let authUrlSent = false;
|
||||
const now = Date.now();
|
||||
return new Promise<NDKRpcResponse>(() => {
|
||||
const responseHandler = (response: NDKRpcResponse) => {
|
||||
if (response.result === 'auth_url') {
|
||||
this.once(`response-${id}`, responseHandler);
|
||||
if (!authUrlSent) {
|
||||
authUrlSent = true;
|
||||
this.emit('authUrl', response.error);
|
||||
}
|
||||
} else if (cb) {
|
||||
if (this.requests.has(id)) {
|
||||
this.requests.delete(id);
|
||||
console.log('nostr-login processed nip46 request in', Date.now() - now, 'ms');
|
||||
cb(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.once(`response-${id}`, responseHandler);
|
||||
});
|
||||
}
|
||||
|
||||
protected async createRequestEvent(id: string, remotePubkey: string, method: string, params: string[] = [], kind = 24133) {
|
||||
this.requests.add(id);
|
||||
const localUser = await this._signer.user();
|
||||
const remoteUser = this._ndk.getUser({ pubkey: remotePubkey });
|
||||
const request = { id, method, params };
|
||||
|
||||
const event = new NDKEvent(this._ndk, {
|
||||
kind,
|
||||
content: JSON.stringify(request),
|
||||
tags: [['p', remotePubkey]],
|
||||
pubkey: localUser.pubkey,
|
||||
} as NostrEvent);
|
||||
|
||||
const useNip44 = this._useNip44 && method !== 'create_account';
|
||||
const encrypt = useNip44 ? this._signer.encryptNip44 : this._signer.encrypt;
|
||||
event.content = await encrypt.call(this._signer, remoteUser, event.content);
|
||||
await event.sign(this._signer);
|
||||
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
export class IframeNostrRpc extends NostrRpc {
|
||||
private peerOrigin?: string;
|
||||
private iframePort?: MessagePort;
|
||||
private iframeRequests = new Map<string, { id: string; pubkey: string }>();
|
||||
|
||||
public constructor(ndk: NDK, localSigner: PrivateKeySigner, iframePeerOrigin?: string) {
|
||||
super(ndk, localSigner);
|
||||
this._ndk = ndk;
|
||||
this.peerOrigin = iframePeerOrigin;
|
||||
}
|
||||
|
||||
public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
|
||||
if (!this.peerOrigin) return super.subscribe(filter);
|
||||
return new NDKSubscription(
|
||||
this._ndk,
|
||||
{},
|
||||
{
|
||||
// don't send to relay
|
||||
closeOnEose: true,
|
||||
cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public setWorkerIframePort(port: MessagePort) {
|
||||
if (!this.peerOrigin) throw new Error('Unexpected iframe port');
|
||||
|
||||
this.iframePort = port;
|
||||
|
||||
// to make sure Chrome doesn't terminate the channel
|
||||
setInterval(() => {
|
||||
console.log('iframe-nip46 ping');
|
||||
this.iframePort!.postMessage('ping');
|
||||
}, 5000);
|
||||
|
||||
port.onmessage = async ev => {
|
||||
console.log('iframe-nip46 got response', ev.data);
|
||||
if (typeof ev.data === 'string' && ev.data.startsWith('errorNoKey')) {
|
||||
const event_id = ev.data.split(':')[1];
|
||||
const { id = '', pubkey = '' } = this.iframeRequests.get(event_id) || {};
|
||||
if (id && pubkey && this.requests.has(id)) this.emit(`iframeRestart-${pubkey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// a copy-paste from rpc.subscribe
|
||||
try {
|
||||
const event = ev.data;
|
||||
|
||||
if (!validateEvent(event)) throw new Error('Invalid event from iframe');
|
||||
if (!verifySignature(event)) throw new Error('Invalid event signature from iframe');
|
||||
const nevent = new NDKEvent(this._ndk, event);
|
||||
const parsedEvent = await this.parseEvent(nevent);
|
||||
// we're only implementing client-side rpc
|
||||
if (!(parsedEvent as NDKRpcRequest).method) {
|
||||
console.log('parsed response', parsedEvent);
|
||||
this.emit(`response-${parsedEvent.id}`, parsedEvent);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error parsing event', e, ev.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
|
||||
const id = this.getId();
|
||||
|
||||
// create and sign request event
|
||||
const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
|
||||
|
||||
// set response handler, it will dedup auth urls,
|
||||
// and also dedup response handlers - we're sending
|
||||
// to relays and to iframe
|
||||
this.setResponseHandler(id, cb);
|
||||
|
||||
if (this.iframePort) {
|
||||
// map request event id to request id, if iframe
|
||||
// has no key it will reply with error:event_id (it can't
|
||||
// decrypt the request id without keys)
|
||||
this.iframeRequests.set(event.id, { id, pubkey: remotePubkey });
|
||||
|
||||
// send to iframe
|
||||
console.log('iframe-nip46 sending request to', this.peerOrigin, event.rawEvent());
|
||||
this.iframePort.postMessage(event.rawEvent());
|
||||
} else {
|
||||
// send to relays
|
||||
await event.publish();
|
||||
}
|
||||
|
||||
// see notes in 'super'
|
||||
// @ts-ignore
|
||||
return undefined as NDKRpcResponse;
|
||||
}
|
||||
}
|
||||
|
||||
export class ReadyListener {
|
||||
origin: string;
|
||||
messages: string[];
|
||||
promise: Promise<any>;
|
||||
|
||||
constructor(messages: string[], origin: string) {
|
||||
this.origin = origin;
|
||||
this.messages = messages;
|
||||
this.promise = new Promise<any>(ok => {
|
||||
console.log(new Date(), 'started listener for', this.messages);
|
||||
|
||||
// ready message handler
|
||||
const onReady = async (e: MessageEvent) => {
|
||||
const originHostname = new URL(origin!).hostname;
|
||||
const messageHostname = new URL(e.origin).hostname;
|
||||
// same host or subdomain
|
||||
const validHost = messageHostname === originHostname || messageHostname.endsWith('.' + originHostname);
|
||||
if (!validHost || !Array.isArray(e.data) || !e.data.length || !this.messages.includes(e.data[0])) {
|
||||
// console.log(new Date(), 'got invalid ready message', e.origin, e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(new Date(), 'got ready message from', e.origin, e.data);
|
||||
window.removeEventListener('message', onReady);
|
||||
ok(e.data);
|
||||
};
|
||||
window.addEventListener('message', onReady);
|
||||
});
|
||||
}
|
||||
|
||||
async wait(): Promise<any> {
|
||||
console.log(new Date(), 'waiting for', this.messages);
|
||||
const r = await this.promise;
|
||||
// NOTE: timer here doesn't help bcs it must be activated when
|
||||
// user "confirms", but that's happening on a different
|
||||
// origin and we can't really know.
|
||||
// await new Promise<any>((ok, err) => {
|
||||
// // 10 sec should be more than enough
|
||||
// setTimeout(() => err(new Date() + ' timeout for ' + this.message), 10000);
|
||||
|
||||
// // if promise already resolved or will resolve in the future
|
||||
// this.promise.then(ok);
|
||||
// });
|
||||
|
||||
console.log(new Date(), 'finished waiting for', this.messages, r);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
export class Nip46Signer extends NDKNip46Signer {
|
||||
private _userPubkey: string = '';
|
||||
private _rpc: IframeNostrRpc;
|
||||
|
||||
constructor(ndk: NDK, localSigner: PrivateKeySigner, signerPubkey: string, iframeOrigin?: string) {
|
||||
super(ndk, signerPubkey, localSigner);
|
||||
|
||||
// override with our own rpc implementation
|
||||
this._rpc = new IframeNostrRpc(ndk, localSigner, iframeOrigin);
|
||||
this._rpc.setUseNip44(true); // !!this.params.optionsModal.dev);
|
||||
this._rpc.on('authUrl', (url: string) => {
|
||||
this.emit('authUrl', url);
|
||||
});
|
||||
|
||||
this.rpc = this._rpc;
|
||||
}
|
||||
|
||||
get userPubkey() {
|
||||
return this._userPubkey;
|
||||
}
|
||||
|
||||
private async setSignerPubkey(signerPubkey: string, sameAsUser: boolean = false) {
|
||||
console.log("setSignerPubkey", signerPubkey);
|
||||
|
||||
// ensure it's set
|
||||
this.remotePubkey = signerPubkey;
|
||||
|
||||
// when we're sure it's known
|
||||
this._rpc.on(`iframeRestart-${signerPubkey}`, () => {
|
||||
this.emit('iframeRestart');
|
||||
});
|
||||
|
||||
// now call getPublicKey and swap remotePubkey w/ that
|
||||
await this.initUserPubkey(sameAsUser ? signerPubkey : '');
|
||||
}
|
||||
|
||||
public async initUserPubkey(hintPubkey?: string) {
|
||||
if (this._userPubkey) throw new Error('Already called initUserPubkey');
|
||||
|
||||
if (hintPubkey) {
|
||||
this._userPubkey = hintPubkey;
|
||||
return;
|
||||
}
|
||||
|
||||
this._userPubkey = await new Promise<string>((ok, err) => {
|
||||
if (!this.remotePubkey) throw new Error('Signer pubkey not set');
|
||||
|
||||
console.log("get_public_key", this.remotePubkey);
|
||||
this._rpc.sendRequest(this.remotePubkey, 'get_public_key', [], 24133, (response: NDKRpcResponse) => {
|
||||
ok(response.result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async listen(nostrConnectSecret: string) {
|
||||
const signerPubkey = await (this.rpc as IframeNostrRpc).listen(nostrConnectSecret);
|
||||
await this.setSignerPubkey(signerPubkey);
|
||||
}
|
||||
|
||||
public async connect(token?: string, perms?: string) {
|
||||
if (!this.remotePubkey) throw new Error('No signer pubkey');
|
||||
await this._rpc.connect(this.remotePubkey, token, perms);
|
||||
await this.setSignerPubkey(this.remotePubkey);
|
||||
}
|
||||
|
||||
public async setListenReply(reply: any, nostrConnectSecret: string) {
|
||||
const signerPubkey = await this._rpc.parseNostrConnectReply(reply, nostrConnectSecret);
|
||||
await this.setSignerPubkey(signerPubkey, true);
|
||||
}
|
||||
|
||||
public async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
|
||||
const params = [
|
||||
name,
|
||||
domain,
|
||||
'', // email
|
||||
perms,
|
||||
];
|
||||
|
||||
const r = await new Promise<NDKRpcResponse>(ok => {
|
||||
this.rpc.sendRequest(bunkerPubkey, 'create_account', params, undefined, ok);
|
||||
});
|
||||
|
||||
console.log('create_account pubkey', r);
|
||||
if (r.result === 'error') {
|
||||
throw new Error(r.error);
|
||||
}
|
||||
|
||||
return r.result;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { Info } from 'nostr-login-components/dist/types/types';
|
||||
|
||||
export interface Signer {
|
||||
signEvent: (event: any) => Promise<any>;
|
||||
nip04: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
|
||||
};
|
||||
nip44: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NostrObjectParams {
|
||||
waitReady(): Promise<void>;
|
||||
getUserInfo(): Info | null;
|
||||
launch(): Promise<void>;
|
||||
getSigner(): Signer;
|
||||
wait<T>(cb: () => Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
class Nostr {
|
||||
#params: NostrObjectParams;
|
||||
private nip04: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<any>;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<any>;
|
||||
};
|
||||
private nip44: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<any>;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<any>;
|
||||
};
|
||||
|
||||
constructor(params: NostrObjectParams) {
|
||||
this.#params = params;
|
||||
|
||||
this.getPublicKey = this.getPublicKey.bind(this);
|
||||
this.signEvent = this.signEvent.bind(this);
|
||||
this.getRelays = this.getRelays.bind(this);
|
||||
this.nip04 = {
|
||||
encrypt: this.encrypt04.bind(this),
|
||||
decrypt: this.decrypt04.bind(this),
|
||||
};
|
||||
this.nip44 = {
|
||||
encrypt: this.encrypt44.bind(this),
|
||||
decrypt: this.decrypt44.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureAuth() {
|
||||
await this.#params.waitReady();
|
||||
|
||||
// authed?
|
||||
if (this.#params.getUserInfo()) return;
|
||||
|
||||
// launch auth flow
|
||||
await this.#params.launch();
|
||||
|
||||
// give up
|
||||
if (!this.#params.getUserInfo()) {
|
||||
throw new Error('Rejected by user');
|
||||
}
|
||||
}
|
||||
|
||||
async getPublicKey() {
|
||||
await this.ensureAuth();
|
||||
const userInfo = this.#params.getUserInfo();
|
||||
if (userInfo) {
|
||||
return userInfo.pubkey;
|
||||
} else {
|
||||
throw new Error('No user');
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
async signEvent(event) {
|
||||
await this.ensureAuth();
|
||||
return this.#params.wait(async () => await this.#params.getSigner().signEvent(event));
|
||||
}
|
||||
|
||||
async getRelays() {
|
||||
// FIXME implement!
|
||||
return {};
|
||||
}
|
||||
|
||||
async encrypt04(pubkey: string, plaintext: string) {
|
||||
await this.ensureAuth();
|
||||
return this.#params.wait(async () => await this.#params.getSigner().nip04.encrypt(pubkey, plaintext));
|
||||
}
|
||||
|
||||
async decrypt04(pubkey: string, ciphertext: string) {
|
||||
await this.ensureAuth();
|
||||
return this.#params.wait(async () => await this.#params.getSigner().nip04.decrypt(pubkey, ciphertext));
|
||||
}
|
||||
|
||||
async encrypt44(pubkey: string, plaintext: string) {
|
||||
await this.ensureAuth();
|
||||
return this.#params.wait(async () => await this.#params.getSigner().nip44.encrypt(pubkey, plaintext));
|
||||
}
|
||||
|
||||
async decrypt44(pubkey: string, ciphertext: string) {
|
||||
await this.ensureAuth();
|
||||
return this.#params.wait(async () => await this.#params.getSigner().nip44.decrypt(pubkey, ciphertext));
|
||||
}
|
||||
}
|
||||
|
||||
export default Nostr;
|
||||
@@ -1,99 +0,0 @@
|
||||
import { Nostr, NostrParams } from './';
|
||||
import { EventEmitter } from 'tseep';
|
||||
|
||||
class NostrExtensionService extends EventEmitter {
|
||||
private params: NostrParams;
|
||||
private nostrExtension: any | undefined;
|
||||
|
||||
constructor(params: NostrParams) {
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public startCheckingExtension(nostr: Nostr) {
|
||||
if (this.checkExtension(nostr)) return;
|
||||
|
||||
// watch out for extension trying to overwrite us
|
||||
const to = setInterval(() => {
|
||||
if (this.checkExtension(nostr)) clearTimeout(to);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private checkExtension(nostr: Nostr) {
|
||||
// @ts-ignore
|
||||
if (!this.nostrExtension && window.nostr && window.nostr !== nostr) {
|
||||
this.initExtension(nostr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async initExtension(nostr: Nostr, lastTry?: boolean) {
|
||||
// @ts-ignore
|
||||
this.nostrExtension = window.nostr;
|
||||
// @ts-ignore
|
||||
window.nostr = nostr;
|
||||
// we're signed in with extesions? well execute that
|
||||
if (this.params.userInfo?.authMethod === 'extension') {
|
||||
await this.trySetExtensionForPubkey(this.params.userInfo.pubkey);
|
||||
}
|
||||
|
||||
// schedule another check
|
||||
if (!lastTry) {
|
||||
setTimeout(() => {
|
||||
// NOTE: we can't know if user has >1 extension and thus
|
||||
// if the current one we detected is the actual 'last one'
|
||||
// that will set the window.nostr. So the simplest
|
||||
// solution is to wait a bit more, hoping that if one
|
||||
// extension started then the rest are likely to start soon,
|
||||
// and then just capture the most recent one
|
||||
|
||||
// @ts-ignore
|
||||
if (window.nostr !== nostr && this.nostrExtension !== window.nostr) {
|
||||
this.initExtension(nostr, true);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// in the worst case of app saving the nostrExtension reference
|
||||
// it will be calling it directly, not a big deal
|
||||
}
|
||||
|
||||
private async setExtensionReadPubkey(expectedPubkey?: string) {
|
||||
window.nostr = this.nostrExtension;
|
||||
// @ts-ignore
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
if (expectedPubkey && expectedPubkey !== pubkey) {
|
||||
this.emit('extensionLogout');
|
||||
} else {
|
||||
this.emit('extensionLogin', pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
public async trySetExtensionForPubkey(expectedPubkey: string) {
|
||||
if (this.nostrExtension) {
|
||||
return this.setExtensionReadPubkey(expectedPubkey);
|
||||
}
|
||||
}
|
||||
|
||||
public async setExtension() {
|
||||
return this.setExtensionReadPubkey();
|
||||
}
|
||||
|
||||
public unsetExtension(nostr: Nostr) {
|
||||
if (window.nostr === this.nostrExtension) {
|
||||
// @ts-ignore
|
||||
window.nostr = nostr;
|
||||
}
|
||||
}
|
||||
|
||||
public getExtension() {
|
||||
return this.nostrExtension;
|
||||
}
|
||||
|
||||
public hasExtension() {
|
||||
return !!this.nostrExtension;
|
||||
}
|
||||
}
|
||||
|
||||
export default NostrExtensionService;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Info } from 'nostr-login-components/dist/types/types';
|
||||
import { NostrLoginOptions } from '../types';
|
||||
|
||||
class NostrParams {
|
||||
public userInfo: Info | null;
|
||||
public optionsModal: NostrLoginOptions;
|
||||
constructor() {
|
||||
this.userInfo = null;
|
||||
|
||||
this.optionsModal = {
|
||||
theme: 'default',
|
||||
startScreen: 'welcome',
|
||||
devOverrideBunkerOrigin: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default NostrParams;
|
||||
@@ -1,27 +0,0 @@
|
||||
class Popup {
|
||||
private popup: Window | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public openPopup(url: string) {
|
||||
// user might have closed it already
|
||||
if (!this.popup || this.popup.closed) {
|
||||
// NOTE: do not set noreferrer, bunker might use referrer to
|
||||
// simplify the naming of the connected app.
|
||||
// NOTE: do not pass noopener, otherwise null is returned
|
||||
this.popup = window.open(url, '_blank', 'width=400,height=700');
|
||||
console.log('popup', this.popup);
|
||||
if (!this.popup) throw new Error('Popup blocked. Try again, please!');
|
||||
}
|
||||
}
|
||||
|
||||
public closePopup() {
|
||||
// make sure we release the popup
|
||||
try {
|
||||
this.popup?.close();
|
||||
this.popup = null;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export default Popup;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { EventEmitter } from 'tseep';
|
||||
import { CALL_TIMEOUT } from '../const';
|
||||
|
||||
class ProcessManager extends EventEmitter {
|
||||
private callCount: number = 0;
|
||||
private callTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public onAuthUrl() {
|
||||
if (Boolean(this.callTimer)) {
|
||||
clearTimeout(this.callTimer);
|
||||
}
|
||||
}
|
||||
|
||||
public onIframeUrl() {
|
||||
if (Boolean(this.callTimer)) {
|
||||
clearTimeout(this.callTimer);
|
||||
}
|
||||
}
|
||||
|
||||
public async wait<T>(cb: () => Promise<T>): Promise<T> {
|
||||
// FIXME only allow 1 parallel req
|
||||
|
||||
if (!this.callTimer) {
|
||||
this.callTimer = setTimeout(() => this.emit('onCallTimeout'), CALL_TIMEOUT);
|
||||
}
|
||||
|
||||
if (!this.callCount) {
|
||||
this.emit('onCallStart');
|
||||
}
|
||||
|
||||
this.callCount++;
|
||||
|
||||
let error;
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await cb();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
this.callCount--;
|
||||
|
||||
this.emit('onCallEnd');
|
||||
|
||||
if (this.callTimer) {
|
||||
clearTimeout(this.callTimer);
|
||||
}
|
||||
|
||||
this.callTimer = undefined;
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// we can't return undefined bcs an exception is
|
||||
// thrown above on error
|
||||
// @ts-ignore
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default ProcessManager;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { Nip44 } from '../utils/nip44';
|
||||
import { getPublicKey } from 'nostr-tools';
|
||||
|
||||
export class PrivateKeySigner extends NDKPrivateKeySigner {
|
||||
private nip44: Nip44 = new Nip44();
|
||||
private _pubkey: string;
|
||||
|
||||
constructor(privateKey: string) {
|
||||
super(privateKey);
|
||||
this._pubkey = getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
get pubkey() {
|
||||
return this._pubkey;
|
||||
}
|
||||
|
||||
encryptNip44(recipient: NDKUser, value: string): Promise<string> {
|
||||
return Promise.resolve(this.nip44.encrypt(this.privateKey!, recipient.pubkey, value));
|
||||
}
|
||||
|
||||
decryptNip44(sender: NDKUser, value: string): Promise<string> {
|
||||
return Promise.resolve(this.nip44.decrypt(this.privateKey!, sender.pubkey, value));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export { default as BannerManager } from './BannerManager';
|
||||
export { default as AuthNostrService } from './AuthNostrService';
|
||||
export { default as ModalManager } from './ModalManager';
|
||||
export { default as Nostr } from './Nostr';
|
||||
export { default as NostrExtensionService } from './NostrExtensionService';
|
||||
export { default as NostrParams } from './NostrParams';
|
||||
export { default as Popup } from './Popup';
|
||||
export { default as ProcessManager } from './ProcessManager';
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Info, AuthMethod, ConnectionString, RecentType, BannerNotify } from 'nostr-login-components/dist/types/types';
|
||||
|
||||
export interface NostrLoginAuthOptions {
|
||||
localNsec?: string;
|
||||
relays?: string[];
|
||||
type: 'login' | 'signup' | 'logout';
|
||||
method?: AuthMethod;
|
||||
pubkey?: string;
|
||||
otpData?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// NOTE: must be a subset of CURRENT_MODULE enum
|
||||
export type StartScreens =
|
||||
| 'welcome'
|
||||
| 'welcome-login'
|
||||
| 'welcome-signup'
|
||||
| 'signup'
|
||||
| 'local-signup'
|
||||
| 'login'
|
||||
| 'otp'
|
||||
| 'connect'
|
||||
| 'login-bunker-url'
|
||||
| 'login-read-only'
|
||||
| 'connection-string'
|
||||
| 'switch-account'
|
||||
| 'import';
|
||||
|
||||
export interface NostrLoginOptions {
|
||||
// optional
|
||||
theme?: string;
|
||||
startScreen?: StartScreens;
|
||||
bunkers?: string;
|
||||
onAuth?: (npub: string, options: NostrLoginAuthOptions) => void;
|
||||
perms?: string;
|
||||
darkMode?: boolean;
|
||||
|
||||
// do not show the banner, modals must be `launch`-ed
|
||||
noBanner?: boolean;
|
||||
|
||||
// forward reqs to this bunker origin for testing
|
||||
devOverrideBunkerOrigin?: string;
|
||||
|
||||
// deprecated, use methods=['local']
|
||||
// use local signup instead of nostr connect
|
||||
localSignup?: boolean;
|
||||
|
||||
// allowed auth methods
|
||||
methods?: AuthMethod[];
|
||||
|
||||
// otp endpoints
|
||||
otpRequestUrl?: string;
|
||||
otpReplyUrl?: string;
|
||||
|
||||
// welcome screen's title/desc
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
||||
// comma-separated list of relays added
|
||||
// to relay list of new profiles created with local signup
|
||||
signupRelays?: string;
|
||||
|
||||
// relay list to override hardcoded `OUTBOX_RELAYS` constant
|
||||
outboxRelays?: string[];
|
||||
|
||||
// dev mode
|
||||
dev?: boolean;
|
||||
|
||||
// use start.njump.me instead of local signup
|
||||
signupNstart?: boolean;
|
||||
|
||||
// list of npubs to auto/suggest-follow on signup
|
||||
followNpubs?: string;
|
||||
|
||||
// when method call auth needed, instead of showing
|
||||
// the modal, we start waiting for incoming nip46
|
||||
// connection and send the nostrconnect string using
|
||||
// nlNeedAuth event
|
||||
customNostrConnect?: boolean;
|
||||
}
|
||||
|
||||
export interface IBanner {
|
||||
userInfo?: Info | null;
|
||||
titleBanner?: string;
|
||||
isLoading?: boolean;
|
||||
listNotifies?: string[];
|
||||
accounts?: Info[];
|
||||
isOpen?: boolean;
|
||||
darkMode?: boolean;
|
||||
notify?: BannerNotify;
|
||||
}
|
||||
|
||||
export type TypeBanner = IBanner & HTMLElement;
|
||||
|
||||
export interface IModal {
|
||||
authUrl?: string;
|
||||
iframeUrl?: string;
|
||||
isLoading?: boolean;
|
||||
isOTP?: boolean;
|
||||
isLoadingExtension?: boolean;
|
||||
localSignup?: boolean;
|
||||
signupNjump?: boolean;
|
||||
njumpIframe?: string;
|
||||
authMethods?: AuthMethod[];
|
||||
hasExtension?: boolean;
|
||||
hasOTP?: boolean;
|
||||
error?: string;
|
||||
signupNameIsAvailable?: string | boolean;
|
||||
loginIsGood?: string | boolean;
|
||||
recents?: RecentType[];
|
||||
accounts?: Info[];
|
||||
darkMode?: boolean;
|
||||
welcomeTitle?: string;
|
||||
welcomeDescription?: string;
|
||||
connectionString?: string;
|
||||
connectionStringServices?: ConnectionString[];
|
||||
}
|
||||
|
||||
export type TypeModal = IModal & HTMLElement;
|
||||
|
||||
export interface Response {
|
||||
result?: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
import { Info, RecentType } from 'nostr-login-components/dist/types/types';
|
||||
import NDK, { NDKEvent, NDKRelaySet, NDKSigner, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { generatePrivateKey } from 'nostr-tools';
|
||||
import { NostrLoginOptions } from '../types';
|
||||
|
||||
const LOCAL_STORE_KEY = '__nostrlogin_nip46';
|
||||
const LOGGED_IN_ACCOUNTS = '__nostrlogin_accounts';
|
||||
const RECENT_ACCOUNTS = '__nostrlogin_recent';
|
||||
const OUTBOX_RELAYS = ['wss://purplepag.es', 'wss://relay.nos.social', 'wss://user.kindpag.es', 'wss://relay.damus.io', 'wss://nos.lol'];
|
||||
const DEFAULT_SIGNUP_RELAYS = ['wss://relay.damus.io/', 'wss://nos.lol/', 'wss://relay.primal.net/'];
|
||||
|
||||
export const localStorageSetItem = (key: string, value: string) => {
|
||||
localStorage.setItem(key, value);
|
||||
};
|
||||
|
||||
export const localStorageGetItem = (key: string) => {
|
||||
const value = window.localStorage.getItem(key);
|
||||
|
||||
if (value) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const localStorageRemoveItem = (key: string) => {
|
||||
localStorage.removeItem(key);
|
||||
};
|
||||
|
||||
export const fetchProfile = async (info: Info, profileNdk: NDK) => {
|
||||
const user = new NDKUser({ pubkey: info.pubkey });
|
||||
|
||||
user.ndk = profileNdk;
|
||||
|
||||
return await user.fetchProfile();
|
||||
};
|
||||
|
||||
export const prepareSignupRelays = (signupRelays?: string) => {
|
||||
const relays = (signupRelays || '')
|
||||
.split(',')
|
||||
.map(r => r.trim())
|
||||
.filter(r => r.startsWith('ws'));
|
||||
if (!relays.length) relays.push(...DEFAULT_SIGNUP_RELAYS);
|
||||
return relays;
|
||||
};
|
||||
|
||||
export const createProfile = async (info: Info, profileNdk: NDK, signer: NDKSigner, signupRelays?: string, outboxRelays?: string[]) => {
|
||||
const meta = {
|
||||
name: info.name,
|
||||
};
|
||||
|
||||
const profileEvent = new NDKEvent(profileNdk, {
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: info.pubkey,
|
||||
content: JSON.stringify(meta),
|
||||
tags: [],
|
||||
});
|
||||
if (window.location.hostname) profileEvent.tags.push(['client', window.location.hostname]);
|
||||
|
||||
const relaysEvent = new NDKEvent(profileNdk, {
|
||||
kind: 10002,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: info.pubkey,
|
||||
content: '',
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const relays = prepareSignupRelays(signupRelays)
|
||||
for (const r of relays) {
|
||||
relaysEvent.tags.push(['r', r]);
|
||||
}
|
||||
|
||||
await profileEvent.sign(signer);
|
||||
console.log('signed profile', profileEvent);
|
||||
await relaysEvent.sign(signer);
|
||||
console.log('signed relays', relaysEvent);
|
||||
|
||||
const outboxRelaysFinal = outboxRelays && outboxRelays.length ? outboxRelays : OUTBOX_RELAYS;
|
||||
|
||||
await profileEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
|
||||
console.log('published profile', profileEvent);
|
||||
await relaysEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
|
||||
console.log('published relays', relaysEvent);
|
||||
};
|
||||
|
||||
export const bunkerUrlToInfo = (bunkerUrl: string, sk = ''): Info => {
|
||||
const url = new URL(bunkerUrl);
|
||||
|
||||
return {
|
||||
pubkey: '',
|
||||
signerPubkey: url.hostname || url.pathname.split('//')[1],
|
||||
sk: sk || generatePrivateKey(),
|
||||
relays: url.searchParams.getAll('relay'),
|
||||
token: url.searchParams.get('secret') || '',
|
||||
authMethod: 'connect',
|
||||
};
|
||||
};
|
||||
|
||||
export const isBunkerUrl = (value: string) => value.startsWith('bunker://');
|
||||
|
||||
export const getBunkerUrl = async (value: string, optionsModal: NostrLoginOptions) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isBunkerUrl(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.includes('@')) {
|
||||
const [name, domain] = value.toLocaleLowerCase().split('@');
|
||||
const origin = optionsModal.devOverrideBunkerOrigin || `https://${domain}`;
|
||||
const bunkerUrl = `${origin}/.well-known/nostr.json?name=_`;
|
||||
const userUrl = `${origin}/.well-known/nostr.json?name=${name}`;
|
||||
const bunker = await fetch(bunkerUrl);
|
||||
const bunkerData = await bunker.json();
|
||||
const bunkerPubkey = bunkerData.names['_'];
|
||||
const bunkerRelays = bunkerData.nip46[bunkerPubkey];
|
||||
const user = await fetch(userUrl);
|
||||
const userData = await user.json();
|
||||
const userPubkey = userData.names[name];
|
||||
// console.log({
|
||||
// bunkerData, userData, bunkerPubkey, bunkerRelays, userPubkey,
|
||||
// name, domain, origin
|
||||
// })
|
||||
if (!bunkerRelays.length) {
|
||||
throw new Error('Bunker relay not provided');
|
||||
}
|
||||
|
||||
return `bunker://${userPubkey}?relay=${bunkerRelays[0]}`;
|
||||
}
|
||||
|
||||
throw new Error('Invalid user name or bunker url');
|
||||
};
|
||||
|
||||
export const checkNip05 = async (nip05: string) => {
|
||||
let available = false;
|
||||
let error = '';
|
||||
let pubkey = '';
|
||||
await (async () => {
|
||||
if (!nip05 || !nip05.includes('@')) return;
|
||||
|
||||
const [name, domain] = nip05.toLocaleLowerCase().split('@');
|
||||
if (!name) return;
|
||||
|
||||
const REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g);
|
||||
if (!REGEXP.test(nip05)) {
|
||||
error = 'Invalid name';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
error = 'Select service';
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `https://${domain}/.well-known/nostr.json?name=${name.toLowerCase()}`;
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
const d = await r.json();
|
||||
if (d.names[name]) {
|
||||
pubkey = d.names[name];
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
available = true;
|
||||
})();
|
||||
|
||||
return {
|
||||
available,
|
||||
taken: pubkey != '',
|
||||
error,
|
||||
pubkey,
|
||||
};
|
||||
};
|
||||
|
||||
const upgradeInfo = (info: Info | RecentType) => {
|
||||
if ('typeAuthMethod' in info) delete info['typeAuthMethod'];
|
||||
|
||||
if (!info.authMethod) {
|
||||
if ('extension' in info && info['extension']) info.authMethod = 'extension';
|
||||
else if ('readOnly' in info && info['readOnly']) info.authMethod = 'readOnly';
|
||||
else info.authMethod = 'connect';
|
||||
}
|
||||
|
||||
if (info.nip05 && isBunkerUrl(info.nip05)) {
|
||||
info.bunkerUrl = info.nip05;
|
||||
info.nip05 = '';
|
||||
}
|
||||
|
||||
if (info.authMethod === 'connect' && !info.signerPubkey) {
|
||||
info.signerPubkey = info.pubkey;
|
||||
}
|
||||
};
|
||||
|
||||
export const localStorageAddAccount = (info: Info) => {
|
||||
// make current
|
||||
localStorageSetItem(LOCAL_STORE_KEY, JSON.stringify(info));
|
||||
|
||||
const loggedInAccounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
|
||||
const recentAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
|
||||
|
||||
// upgrade first
|
||||
loggedInAccounts.forEach(a => upgradeInfo(a));
|
||||
recentAccounts.forEach(a => upgradeInfo(a));
|
||||
|
||||
// upsert new info into accounts
|
||||
const accounts: Info[] = loggedInAccounts;
|
||||
const index = loggedInAccounts.findIndex((el: Info) => el.pubkey === info.pubkey && el.authMethod === info.authMethod);
|
||||
if (index !== -1) {
|
||||
accounts[index] = info;
|
||||
} else {
|
||||
accounts.push(info);
|
||||
}
|
||||
|
||||
// remove new info from recent
|
||||
const recents = recentAccounts.filter(el => el.pubkey !== info.pubkey || el.authMethod !== info.authMethod);
|
||||
|
||||
localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
|
||||
localStorageSetItem(LOGGED_IN_ACCOUNTS, JSON.stringify(accounts));
|
||||
};
|
||||
|
||||
export const localStorageRemoveCurrentAccount = () => {
|
||||
const user: Info = localStorageGetItem(LOCAL_STORE_KEY);
|
||||
if (!user) return;
|
||||
|
||||
// make sure it's valid
|
||||
upgradeInfo(user);
|
||||
|
||||
// remove secret fields
|
||||
const recentUser: RecentType = { ...user };
|
||||
|
||||
// make sure session keys are dropped
|
||||
// @ts-ignore
|
||||
delete recentUser['sk'];
|
||||
// @ts-ignore
|
||||
delete recentUser['otpData'];
|
||||
|
||||
// get accounts and recent
|
||||
const loggedInAccounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
|
||||
const recentsAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
|
||||
|
||||
// upgrade first
|
||||
loggedInAccounts.forEach(a => upgradeInfo(a));
|
||||
recentsAccounts.forEach(a => upgradeInfo(a));
|
||||
|
||||
const recents: RecentType[] = recentsAccounts;
|
||||
if (recentUser.authMethod === 'connect' && recentUser.bunkerUrl && recentUser.bunkerUrl.includes('secret=')) {
|
||||
console.log('nostr login bunker conn with a secret not saved to recent');
|
||||
} else if (recentUser.authMethod === 'local') {
|
||||
console.log('nostr login temporary local keys not save to recent');
|
||||
} else {
|
||||
// upsert to recent
|
||||
const index = recentsAccounts.findIndex((el: RecentType) => el.pubkey === recentUser.pubkey && el.authMethod === recentUser.authMethod);
|
||||
if (index !== -1) {
|
||||
recents[index] = recentUser;
|
||||
} else {
|
||||
recents.push(recentUser);
|
||||
}
|
||||
}
|
||||
|
||||
// remove from accounts
|
||||
const accounts = loggedInAccounts.filter(el => el.pubkey !== user.pubkey || el.authMethod !== user.authMethod);
|
||||
|
||||
// update accounts and recent, clear current
|
||||
localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
|
||||
localStorageSetItem(LOGGED_IN_ACCOUNTS, JSON.stringify(accounts));
|
||||
localStorageRemoveItem(LOCAL_STORE_KEY);
|
||||
};
|
||||
|
||||
export const localStorageRemoveRecent = (user: RecentType) => {
|
||||
const recentsAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
|
||||
recentsAccounts.forEach(a => upgradeInfo(a));
|
||||
const recents = recentsAccounts.filter(el => el.pubkey !== user.pubkey || el.authMethod !== user.authMethod);
|
||||
localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
|
||||
};
|
||||
|
||||
export const localStorageGetRecents = (): RecentType[] => {
|
||||
const recents: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
|
||||
recents.forEach(r => upgradeInfo(r));
|
||||
return recents;
|
||||
};
|
||||
|
||||
export const localStorageGetAccounts = (): Info[] => {
|
||||
const accounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
|
||||
accounts.forEach(a => upgradeInfo(a));
|
||||
return accounts;
|
||||
};
|
||||
|
||||
export const localStorageGetCurrent = (): Info | null => {
|
||||
const info = localStorageGetItem(LOCAL_STORE_KEY);
|
||||
if (info) upgradeInfo(info);
|
||||
return info;
|
||||
};
|
||||
|
||||
export const setDarkMode = (dark: boolean) => {
|
||||
localStorageSetItem('nl-dark-mode', dark ? 'true' : 'false');
|
||||
};
|
||||
|
||||
export const getDarkMode = (opt: NostrLoginOptions) => {
|
||||
const getDarkModeLocal = localStorage.getItem('nl-dark-mode');
|
||||
|
||||
if (getDarkModeLocal) {
|
||||
// user already changed it
|
||||
return Boolean(JSON.parse(getDarkModeLocal));
|
||||
} else if (opt.darkMode !== undefined) {
|
||||
// app provided an option
|
||||
return opt.darkMode;
|
||||
} else {
|
||||
// auto-detect
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getIcon = async () => {
|
||||
// FIXME look at meta tags or manifest
|
||||
return document.location.origin + '/favicon.ico';
|
||||
};
|
||||
@@ -1,185 +0,0 @@
|
||||
import { chacha20 } from "@noble/ciphers/chacha"
|
||||
import { concatBytes, randomBytes, utf8ToBytes } from "@noble/hashes/utils"
|
||||
import { equalBytes } from "@noble/ciphers/utils";
|
||||
import { secp256k1 } from "@noble/curves/secp256k1"
|
||||
import {
|
||||
expand as hkdf_expand,
|
||||
extract as hkdf_extract,
|
||||
} from "@noble/hashes/hkdf"
|
||||
import { sha256 } from "@noble/hashes/sha256"
|
||||
import { hmac } from "@noble/hashes/hmac";
|
||||
import { base64 } from "@scure/base";
|
||||
import { getPublicKey } from 'nostr-tools'
|
||||
|
||||
// from https://github.com/nbd-wtf/nostr-tools
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
const u = {
|
||||
minPlaintextSize: 0x0001, // 1b msg => padded to 32b
|
||||
maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb
|
||||
|
||||
utf8Encode: utf8ToBytes,
|
||||
utf8Decode(bytes: Uint8Array): string {
|
||||
return decoder.decode(bytes);
|
||||
},
|
||||
|
||||
getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array {
|
||||
const sharedX = secp256k1
|
||||
.getSharedSecret(privkeyA, "02" + pubkeyB)
|
||||
.subarray(1, 33);
|
||||
return hkdf_extract(sha256, sharedX, "nip44-v2");
|
||||
},
|
||||
|
||||
getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) {
|
||||
const keys = hkdf_expand(sha256, conversationKey, nonce, 76);
|
||||
return {
|
||||
chacha_key: keys.subarray(0, 32),
|
||||
chacha_nonce: keys.subarray(32, 44),
|
||||
hmac_key: keys.subarray(44, 76),
|
||||
};
|
||||
},
|
||||
|
||||
calcPaddedLen(len: number): number {
|
||||
if (!Number.isSafeInteger(len) || len < 1)
|
||||
throw new Error("expected positive integer");
|
||||
if (len <= 32) return 32;
|
||||
const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1);
|
||||
const chunk = nextPower <= 256 ? 32 : nextPower / 8;
|
||||
return chunk * (Math.floor((len - 1) / chunk) + 1);
|
||||
},
|
||||
|
||||
writeU16BE(num: number): Uint8Array {
|
||||
if (
|
||||
!Number.isSafeInteger(num) ||
|
||||
num < u.minPlaintextSize ||
|
||||
num > u.maxPlaintextSize
|
||||
)
|
||||
throw new Error(
|
||||
"invalid plaintext size: must be between 1 and 65535 bytes"
|
||||
);
|
||||
const arr = new Uint8Array(2);
|
||||
new DataView(arr.buffer).setUint16(0, num, false);
|
||||
return arr;
|
||||
},
|
||||
|
||||
pad(plaintext: string): Uint8Array {
|
||||
const unpadded = u.utf8Encode(plaintext);
|
||||
const unpaddedLen = unpadded.length;
|
||||
const prefix = u.writeU16BE(unpaddedLen);
|
||||
const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen);
|
||||
return concatBytes(prefix, unpadded, suffix);
|
||||
},
|
||||
|
||||
unpad(padded: Uint8Array): string {
|
||||
const unpaddedLen = new DataView(padded.buffer).getUint16(0);
|
||||
const unpadded = padded.subarray(2, 2 + unpaddedLen);
|
||||
if (
|
||||
unpaddedLen < u.minPlaintextSize ||
|
||||
unpaddedLen > u.maxPlaintextSize ||
|
||||
unpadded.length !== unpaddedLen ||
|
||||
padded.length !== 2 + u.calcPaddedLen(unpaddedLen)
|
||||
)
|
||||
throw new Error("invalid padding");
|
||||
return u.utf8Decode(unpadded);
|
||||
},
|
||||
|
||||
hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array {
|
||||
if (aad.length !== 32)
|
||||
throw new Error("AAD associated data must be 32 bytes");
|
||||
const combined = concatBytes(aad, message);
|
||||
return hmac(sha256, key, combined);
|
||||
},
|
||||
|
||||
// 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
|
||||
decodePayload(payload: string): {
|
||||
nonce: Uint8Array;
|
||||
ciphertext: Uint8Array;
|
||||
mac: Uint8Array;
|
||||
} {
|
||||
if (typeof payload !== "string")
|
||||
throw new Error("payload must be a valid string");
|
||||
const plen = payload.length;
|
||||
if (plen < 132 || plen > 87472)
|
||||
throw new Error("invalid payload length: " + plen);
|
||||
if (payload[0] === "#") throw new Error("unknown encryption version");
|
||||
let data: Uint8Array;
|
||||
try {
|
||||
data = base64.decode(payload);
|
||||
} catch (error) {
|
||||
throw new Error("invalid base64: " + (error as Error).message);
|
||||
}
|
||||
const dlen = data.length;
|
||||
if (dlen < 99 || dlen > 65603)
|
||||
throw new Error("invalid data length: " + dlen);
|
||||
const vers = data[0];
|
||||
if (vers !== 2) throw new Error("unknown encryption version " + vers);
|
||||
return {
|
||||
nonce: data.subarray(1, 33),
|
||||
ciphertext: data.subarray(33, -32),
|
||||
mac: data.subarray(-32),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export function encryptNip44(
|
||||
plaintext: string,
|
||||
conversationKey: Uint8Array,
|
||||
nonce: Uint8Array = randomBytes(32)
|
||||
): string {
|
||||
const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(
|
||||
conversationKey,
|
||||
nonce
|
||||
);
|
||||
const padded = u.pad(plaintext);
|
||||
const ciphertext = chacha20(chacha_key, chacha_nonce, padded);
|
||||
const mac = u.hmacAad(hmac_key, ciphertext, nonce);
|
||||
return base64.encode(
|
||||
concatBytes(new Uint8Array([2]), nonce, ciphertext, mac)
|
||||
);
|
||||
}
|
||||
|
||||
export function decryptNip44(payload: string, conversationKey: Uint8Array): string {
|
||||
const { nonce, ciphertext, mac } = u.decodePayload(payload);
|
||||
const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(
|
||||
conversationKey,
|
||||
nonce
|
||||
);
|
||||
const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce);
|
||||
if (!equalBytes(calculatedMac, mac)) throw new Error("invalid MAC");
|
||||
const padded = chacha20(chacha_key, chacha_nonce, ciphertext);
|
||||
return u.unpad(padded);
|
||||
}
|
||||
|
||||
export class Nip44 {
|
||||
private cache = new Map<string, Uint8Array>()
|
||||
|
||||
public createKey(privkey: string, pubkey: string) {
|
||||
return u.getConversationKey(privkey, pubkey)
|
||||
}
|
||||
|
||||
private getKey(privkey: string, pubkey: string, extractable?: boolean) {
|
||||
const id = getPublicKey(privkey) + pubkey
|
||||
let cryptoKey = this.cache.get(id)
|
||||
if (cryptoKey) return cryptoKey
|
||||
|
||||
const key = this.createKey(privkey, pubkey)
|
||||
this.cache.set(id, key)
|
||||
return key
|
||||
}
|
||||
|
||||
public encrypt(privkey: string, pubkey: string, text: string): string {
|
||||
const key = this.getKey(privkey, pubkey)
|
||||
return encryptNip44(text, key)
|
||||
}
|
||||
|
||||
public decrypt(privkey: string, pubkey: string, data: string): string {
|
||||
const key = this.getKey(privkey, pubkey)
|
||||
return decryptNip44(data, key)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
26
packages/components/.gitignore
vendored
26
packages/components/.gitignore
vendored
@@ -1,26 +0,0 @@
|
||||
dist/
|
||||
www/
|
||||
loader/
|
||||
|
||||
*~
|
||||
*.sw[mnpcod]
|
||||
*.log
|
||||
*.lock
|
||||
*.tmp
|
||||
*.tmp.*
|
||||
log.txt
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
.stencil/
|
||||
.idea/
|
||||
.vscode/
|
||||
.sass-cache/
|
||||
.versions/
|
||||
node_modules/
|
||||
$RECYCLE.BIN/
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
UserInterfaceState.xcuserstate
|
||||
.env
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "consistent",
|
||||
"printWidth": 180,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "nostr-login-components",
|
||||
"version": "1.0.3",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.js",
|
||||
"es2015": "dist/esm/index.js",
|
||||
"es2017": "dist/esm/index.js",
|
||||
"types": "dist/types/index.d.ts",
|
||||
"collection": "dist/collection/collection-manifest.json",
|
||||
"collection:main": "dist/collection/index.js",
|
||||
"author": "a-fralou",
|
||||
"files": [
|
||||
"dist/",
|
||||
"loader/"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/components/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "stencil build --docs --prod && node post-build-plugin.js",
|
||||
"dev": "stencil build --dev --watch --serve",
|
||||
"test": "stencil test --spec --e2e",
|
||||
"test.watch": "stencil test --spec --e2e --watchAll",
|
||||
"generate": "stencil generate",
|
||||
"format": "npx prettier --write src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.20.0",
|
||||
"@stencil/sass": "^3.0.12",
|
||||
"@stencil/store": "^2.0.16",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"qrcode": "^1.5.4",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"tailwindcss-rtl": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/node": "^16.18.11",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"jest": "^29.7.0",
|
||||
"jest-cli": "^29.7.0",
|
||||
"prettier": "^3.2.2",
|
||||
"puppeteer": "21.1.1",
|
||||
"stencil-tailwind-plugin": "^1.8.0",
|
||||
"typescript": "^5.3.3",
|
||||
"workbox-build": "^4.3.1"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const folderPath = './dist/components';
|
||||
const cssFilePath = './dist/components/css.js';
|
||||
let countFileRead = 0;
|
||||
|
||||
// Function to get all JavaScript files in the directory
|
||||
function getFilesInDirectory(dir) {
|
||||
const files = fs.readdirSync(dir);
|
||||
return files.filter(file => file.endsWith('.js')); // Filter out only .js files
|
||||
}
|
||||
|
||||
// Function to process a single file
|
||||
function processFile(filePath, cssContent) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Replace the CSS variable declarations and import them from css.js
|
||||
const updatedContent = fileContent.replace(/const\s+(\w+Css)\s*=\s*(`[^`]*`|".*?"|'.*?')(?:\s*;|(\s*\/\/.*?))?(?=\s*const|$)/g, (match, varName, varValue) => {
|
||||
// Save CSS variables and their values in cssContent
|
||||
if (countFileRead === 0) {
|
||||
cssContent.push(`export const baseCss = ${varValue};`);
|
||||
countFileRead = countFileRead + 1;
|
||||
}
|
||||
// Return the string with the import from css.js
|
||||
return `import { baseCss } from './css.js';\nconst ${varName} = baseCss;\n`;
|
||||
});
|
||||
|
||||
// Write the modified content back to the file
|
||||
fs.writeFileSync(filePath, updatedContent, 'utf8');
|
||||
}
|
||||
|
||||
// Main function to process all files
|
||||
function main() {
|
||||
const cssContent = [];
|
||||
const files = getFilesInDirectory(folderPath);
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(folderPath, file);
|
||||
processFile(filePath, cssContent);
|
||||
});
|
||||
|
||||
// Write collected CSS variables to css.js
|
||||
fs.writeFileSync(cssFilePath, cssContent.join('\n'), 'utf8');
|
||||
console.log('Done! All CSS variables have been moved to css.js.');
|
||||
}
|
||||
|
||||
main();
|
||||
929
packages/components/src/components.d.ts
vendored
929
packages/components/src/components.d.ts
vendored
@@ -1,929 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This is an autogenerated file created by the Stencil compiler.
|
||||
* It contains typing information for all components that exist in this project.
|
||||
*/
|
||||
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
||||
import { AuthMethod, BannerNotify, ConnectionString, Info, NlTheme, RecentType } from "./types/index";
|
||||
import { OptionType } from "./components/nl-select/nl-select";
|
||||
export { AuthMethod, BannerNotify, ConnectionString, Info, NlTheme, RecentType } from "./types/index";
|
||||
export { OptionType } from "./components/nl-select/nl-select";
|
||||
export namespace Components {
|
||||
interface ButtonBase {
|
||||
"darkMode": boolean;
|
||||
"disabled": boolean;
|
||||
"theme": NlTheme;
|
||||
"titleBtn": string;
|
||||
}
|
||||
interface NlAuth {
|
||||
"accounts": Info[];
|
||||
"authMethods": AuthMethod[];
|
||||
"authUrl": string;
|
||||
"bunkers": string;
|
||||
"connectionString": string;
|
||||
"connectionStringServices": ConnectionString[];
|
||||
"darkMode": boolean;
|
||||
"error": string;
|
||||
"hasExtension": boolean;
|
||||
"hasOTP": boolean;
|
||||
"iframeUrl": string;
|
||||
"isLoading": boolean;
|
||||
"isLoadingExtension": boolean;
|
||||
"isOTP": boolean;
|
||||
"localSignup": boolean;
|
||||
"njumpIframe": string;
|
||||
"recents": RecentType[];
|
||||
"signupNjump": boolean;
|
||||
"startScreen": string;
|
||||
"theme": NlTheme;
|
||||
"welcomeDescription": string;
|
||||
"welcomeTitle": string;
|
||||
}
|
||||
interface NlBanner {
|
||||
"accounts": Info[];
|
||||
"darkMode": boolean;
|
||||
"hiddenMode": boolean;
|
||||
"isLoading": boolean;
|
||||
"isOpen": boolean;
|
||||
"notify": BannerNotify | null;
|
||||
"theme": NlTheme;
|
||||
"titleBanner": string;
|
||||
"userInfo": Info | null;
|
||||
}
|
||||
interface NlButton {
|
||||
"darkMode": boolean;
|
||||
"disabled": boolean;
|
||||
"theme": NlTheme;
|
||||
"titleBtn": string;
|
||||
}
|
||||
interface NlChangeAccount {
|
||||
"accounts": Info[];
|
||||
"currentAccount": Info;
|
||||
"darkMode": boolean;
|
||||
"theme": 'default' | 'ocean' | 'lemonade' | 'purple';
|
||||
}
|
||||
interface NlConfirmLogout {
|
||||
"description": string;
|
||||
"titleModal": string;
|
||||
}
|
||||
interface NlConnect {
|
||||
"authMethods": AuthMethod[];
|
||||
"connectionStringServices": ConnectionString[];
|
||||
"hasOTP": boolean;
|
||||
"titleWelcome": string;
|
||||
}
|
||||
interface NlDialog {
|
||||
}
|
||||
interface NlIframe {
|
||||
"description": string;
|
||||
"iframeUrl": string;
|
||||
"titleModal": string;
|
||||
}
|
||||
interface NlImportFlow {
|
||||
"services": ConnectionString[];
|
||||
"titleImport": string;
|
||||
"titleInfo": string;
|
||||
}
|
||||
interface NlInfo {
|
||||
"darkMode": boolean;
|
||||
"theme": NlTheme;
|
||||
}
|
||||
interface NlInfoExtension {
|
||||
}
|
||||
interface NlLoading {
|
||||
"path": string;
|
||||
}
|
||||
interface NlLocalSignup {
|
||||
"description": string;
|
||||
"descriptionNjump": string;
|
||||
"signupNjump": boolean;
|
||||
"titleSignup": string;
|
||||
}
|
||||
interface NlLoginStatus {
|
||||
"info": RecentType | Info | undefined;
|
||||
}
|
||||
interface NlOtpMigrate {
|
||||
"services": ConnectionString[];
|
||||
"textImport": string;
|
||||
"titleImport": string;
|
||||
"titleInfo": string;
|
||||
}
|
||||
interface NlPreviouslyLogged {
|
||||
"accounts": Info[];
|
||||
"description": string;
|
||||
"recents": RecentType[];
|
||||
"titlePage": string;
|
||||
}
|
||||
interface NlSelect {
|
||||
"darkMode": boolean;
|
||||
"options": OptionType[];
|
||||
"selected": number;
|
||||
"theme": 'default' | 'ocean' | 'lemonade' | 'purple';
|
||||
}
|
||||
interface NlSignin {
|
||||
"description": string;
|
||||
"titleLogin": string;
|
||||
}
|
||||
interface NlSigninBunkerUrl {
|
||||
"description": string;
|
||||
"titleLogin": string;
|
||||
}
|
||||
interface NlSigninConnectionString {
|
||||
"connectionString": string;
|
||||
"description": string;
|
||||
"titleLogin": string;
|
||||
}
|
||||
interface NlSigninOtp {
|
||||
"description": string;
|
||||
"descriptionOTP": string;
|
||||
"titleLogin": string;
|
||||
"titleLoginOTP": string;
|
||||
}
|
||||
interface NlSigninReadOnly {
|
||||
"description": string;
|
||||
"titleLogin": string;
|
||||
}
|
||||
interface NlSignup {
|
||||
"bunkers": string;
|
||||
"description": string;
|
||||
"titleSignup": string;
|
||||
}
|
||||
interface NlWelcome {
|
||||
"description": string;
|
||||
"titleWelcome": string;
|
||||
}
|
||||
interface NlWelcomeSignin {
|
||||
"authMethods": AuthMethod[];
|
||||
"hasExtension": boolean;
|
||||
"hasOTP": boolean;
|
||||
"titleWelcome": string;
|
||||
}
|
||||
interface NlWelcomeSignup {
|
||||
"description": string;
|
||||
"titleWelcome": string;
|
||||
}
|
||||
}
|
||||
export interface NlAuthCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlAuthElement;
|
||||
}
|
||||
export interface NlBannerCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlBannerElement;
|
||||
}
|
||||
export interface NlChangeAccountCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlChangeAccountElement;
|
||||
}
|
||||
export interface NlConfirmLogoutCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlConfirmLogoutElement;
|
||||
}
|
||||
export interface NlConnectCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlConnectElement;
|
||||
}
|
||||
export interface NlIframeCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlIframeElement;
|
||||
}
|
||||
export interface NlImportFlowCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlImportFlowElement;
|
||||
}
|
||||
export interface NlLoadingCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlLoadingElement;
|
||||
}
|
||||
export interface NlLocalSignupCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlLocalSignupElement;
|
||||
}
|
||||
export interface NlOtpMigrateCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlOtpMigrateElement;
|
||||
}
|
||||
export interface NlPreviouslyLoggedCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlPreviouslyLoggedElement;
|
||||
}
|
||||
export interface NlSelectCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlSelectElement;
|
||||
}
|
||||
export interface NlSigninCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlSigninElement;
|
||||
}
|
||||
export interface NlSigninBunkerUrlCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlSigninBunkerUrlElement;
|
||||
}
|
||||
export interface NlSigninConnectionStringCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlSigninConnectionStringElement;
|
||||
}
|
||||
export interface NlSigninOtpCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlSigninOtpElement;
|
||||
}
|
||||
export interface NlSigninReadOnlyCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlSigninReadOnlyElement;
|
||||
}
|
||||
export interface NlSignupCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlSignupElement;
|
||||
}
|
||||
export interface NlWelcomeSigninCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLNlWelcomeSigninElement;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLButtonBaseElement extends Components.ButtonBase, HTMLStencilElement {
|
||||
}
|
||||
var HTMLButtonBaseElement: {
|
||||
prototype: HTMLButtonBaseElement;
|
||||
new (): HTMLButtonBaseElement;
|
||||
};
|
||||
interface HTMLNlAuthElementEventMap {
|
||||
"nlCloseModal": any;
|
||||
"nlChangeDarkMode": boolean;
|
||||
"nlNostrConnectDefaultCancel": void;
|
||||
}
|
||||
interface HTMLNlAuthElement extends Components.NlAuth, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlAuthElementEventMap>(type: K, listener: (this: HTMLNlAuthElement, ev: NlAuthCustomEvent<HTMLNlAuthElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlAuthElementEventMap>(type: K, listener: (this: HTMLNlAuthElement, ev: NlAuthCustomEvent<HTMLNlAuthElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlAuthElement: {
|
||||
prototype: HTMLNlAuthElement;
|
||||
new (): HTMLNlAuthElement;
|
||||
};
|
||||
interface HTMLNlBannerElementEventMap {
|
||||
"handleNotifyConfirmBanner": string;
|
||||
"handleNotifyConfirmBannerIframe": string;
|
||||
"handleLoginBanner": string;
|
||||
"handleLogoutBanner": string;
|
||||
"handleOpenWelcomeModal": string;
|
||||
"handleConfirmLogout": string;
|
||||
"handleImportModal": string;
|
||||
}
|
||||
interface HTMLNlBannerElement extends Components.NlBanner, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlBannerElementEventMap>(type: K, listener: (this: HTMLNlBannerElement, ev: NlBannerCustomEvent<HTMLNlBannerElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlBannerElementEventMap>(type: K, listener: (this: HTMLNlBannerElement, ev: NlBannerCustomEvent<HTMLNlBannerElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlBannerElement: {
|
||||
prototype: HTMLNlBannerElement;
|
||||
new (): HTMLNlBannerElement;
|
||||
};
|
||||
interface HTMLNlButtonElement extends Components.NlButton, HTMLStencilElement {
|
||||
}
|
||||
var HTMLNlButtonElement: {
|
||||
prototype: HTMLNlButtonElement;
|
||||
new (): HTMLNlButtonElement;
|
||||
};
|
||||
interface HTMLNlChangeAccountElementEventMap {
|
||||
"handleOpenWelcomeModal": string;
|
||||
"handleSwitchAccount": Info;
|
||||
}
|
||||
interface HTMLNlChangeAccountElement extends Components.NlChangeAccount, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlChangeAccountElementEventMap>(type: K, listener: (this: HTMLNlChangeAccountElement, ev: NlChangeAccountCustomEvent<HTMLNlChangeAccountElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlChangeAccountElementEventMap>(type: K, listener: (this: HTMLNlChangeAccountElement, ev: NlChangeAccountCustomEvent<HTMLNlChangeAccountElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlChangeAccountElement: {
|
||||
prototype: HTMLNlChangeAccountElement;
|
||||
new (): HTMLNlChangeAccountElement;
|
||||
};
|
||||
interface HTMLNlConfirmLogoutElementEventMap {
|
||||
"handleLogoutBanner": string;
|
||||
"handleBackUpModal": string;
|
||||
"nlCloseModal": any;
|
||||
}
|
||||
interface HTMLNlConfirmLogoutElement extends Components.NlConfirmLogout, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlConfirmLogoutElementEventMap>(type: K, listener: (this: HTMLNlConfirmLogoutElement, ev: NlConfirmLogoutCustomEvent<HTMLNlConfirmLogoutElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlConfirmLogoutElementEventMap>(type: K, listener: (this: HTMLNlConfirmLogoutElement, ev: NlConfirmLogoutCustomEvent<HTMLNlConfirmLogoutElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlConfirmLogoutElement: {
|
||||
prototype: HTMLNlConfirmLogoutElement;
|
||||
new (): HTMLNlConfirmLogoutElement;
|
||||
};
|
||||
interface HTMLNlConnectElementEventMap {
|
||||
"nlNostrConnect": ConnectionString;
|
||||
}
|
||||
interface HTMLNlConnectElement extends Components.NlConnect, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlConnectElementEventMap>(type: K, listener: (this: HTMLNlConnectElement, ev: NlConnectCustomEvent<HTMLNlConnectElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlConnectElementEventMap>(type: K, listener: (this: HTMLNlConnectElement, ev: NlConnectCustomEvent<HTMLNlConnectElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlConnectElement: {
|
||||
prototype: HTMLNlConnectElement;
|
||||
new (): HTMLNlConnectElement;
|
||||
};
|
||||
interface HTMLNlDialogElement extends Components.NlDialog, HTMLStencilElement {
|
||||
}
|
||||
var HTMLNlDialogElement: {
|
||||
prototype: HTMLNlDialogElement;
|
||||
new (): HTMLNlDialogElement;
|
||||
};
|
||||
interface HTMLNlIframeElementEventMap {
|
||||
"nlCloseModal": any;
|
||||
}
|
||||
interface HTMLNlIframeElement extends Components.NlIframe, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlIframeElementEventMap>(type: K, listener: (this: HTMLNlIframeElement, ev: NlIframeCustomEvent<HTMLNlIframeElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlIframeElementEventMap>(type: K, listener: (this: HTMLNlIframeElement, ev: NlIframeCustomEvent<HTMLNlIframeElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlIframeElement: {
|
||||
prototype: HTMLNlIframeElement;
|
||||
new (): HTMLNlIframeElement;
|
||||
};
|
||||
interface HTMLNlImportFlowElementEventMap {
|
||||
"nlImportAccount": ConnectionString;
|
||||
"nlExportKeys": void;
|
||||
}
|
||||
interface HTMLNlImportFlowElement extends Components.NlImportFlow, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlImportFlowElementEventMap>(type: K, listener: (this: HTMLNlImportFlowElement, ev: NlImportFlowCustomEvent<HTMLNlImportFlowElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlImportFlowElementEventMap>(type: K, listener: (this: HTMLNlImportFlowElement, ev: NlImportFlowCustomEvent<HTMLNlImportFlowElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlImportFlowElement: {
|
||||
prototype: HTMLNlImportFlowElement;
|
||||
new (): HTMLNlImportFlowElement;
|
||||
};
|
||||
interface HTMLNlInfoElement extends Components.NlInfo, HTMLStencilElement {
|
||||
}
|
||||
var HTMLNlInfoElement: {
|
||||
prototype: HTMLNlInfoElement;
|
||||
new (): HTMLNlInfoElement;
|
||||
};
|
||||
interface HTMLNlInfoExtensionElement extends Components.NlInfoExtension, HTMLStencilElement {
|
||||
}
|
||||
var HTMLNlInfoExtensionElement: {
|
||||
prototype: HTMLNlInfoExtensionElement;
|
||||
new (): HTMLNlInfoExtensionElement;
|
||||
};
|
||||
interface HTMLNlLoadingElementEventMap {
|
||||
"stopFetchHandler": boolean;
|
||||
"handleContinue": boolean;
|
||||
}
|
||||
interface HTMLNlLoadingElement extends Components.NlLoading, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlLoadingElementEventMap>(type: K, listener: (this: HTMLNlLoadingElement, ev: NlLoadingCustomEvent<HTMLNlLoadingElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlLoadingElementEventMap>(type: K, listener: (this: HTMLNlLoadingElement, ev: NlLoadingCustomEvent<HTMLNlLoadingElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlLoadingElement: {
|
||||
prototype: HTMLNlLoadingElement;
|
||||
new (): HTMLNlLoadingElement;
|
||||
};
|
||||
interface HTMLNlLocalSignupElementEventMap {
|
||||
"nlLocalSignup": string;
|
||||
"nlSignupNjump": void;
|
||||
"fetchHandler": boolean;
|
||||
}
|
||||
interface HTMLNlLocalSignupElement extends Components.NlLocalSignup, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlLocalSignupElementEventMap>(type: K, listener: (this: HTMLNlLocalSignupElement, ev: NlLocalSignupCustomEvent<HTMLNlLocalSignupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlLocalSignupElementEventMap>(type: K, listener: (this: HTMLNlLocalSignupElement, ev: NlLocalSignupCustomEvent<HTMLNlLocalSignupElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlLocalSignupElement: {
|
||||
prototype: HTMLNlLocalSignupElement;
|
||||
new (): HTMLNlLocalSignupElement;
|
||||
};
|
||||
interface HTMLNlLoginStatusElement extends Components.NlLoginStatus, HTMLStencilElement {
|
||||
}
|
||||
var HTMLNlLoginStatusElement: {
|
||||
prototype: HTMLNlLoginStatusElement;
|
||||
new (): HTMLNlLoginStatusElement;
|
||||
};
|
||||
interface HTMLNlOtpMigrateElementEventMap {
|
||||
"nlImportAccount": ConnectionString;
|
||||
}
|
||||
interface HTMLNlOtpMigrateElement extends Components.NlOtpMigrate, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlOtpMigrateElementEventMap>(type: K, listener: (this: HTMLNlOtpMigrateElement, ev: NlOtpMigrateCustomEvent<HTMLNlOtpMigrateElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlOtpMigrateElementEventMap>(type: K, listener: (this: HTMLNlOtpMigrateElement, ev: NlOtpMigrateCustomEvent<HTMLNlOtpMigrateElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlOtpMigrateElement: {
|
||||
prototype: HTMLNlOtpMigrateElement;
|
||||
new (): HTMLNlOtpMigrateElement;
|
||||
};
|
||||
interface HTMLNlPreviouslyLoggedElementEventMap {
|
||||
"nlSwitchAccount": Info;
|
||||
"nlLoginRecentAccount": RecentType;
|
||||
"nlRemoveRecent": RecentType;
|
||||
}
|
||||
interface HTMLNlPreviouslyLoggedElement extends Components.NlPreviouslyLogged, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlPreviouslyLoggedElementEventMap>(type: K, listener: (this: HTMLNlPreviouslyLoggedElement, ev: NlPreviouslyLoggedCustomEvent<HTMLNlPreviouslyLoggedElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlPreviouslyLoggedElementEventMap>(type: K, listener: (this: HTMLNlPreviouslyLoggedElement, ev: NlPreviouslyLoggedCustomEvent<HTMLNlPreviouslyLoggedElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlPreviouslyLoggedElement: {
|
||||
prototype: HTMLNlPreviouslyLoggedElement;
|
||||
new (): HTMLNlPreviouslyLoggedElement;
|
||||
};
|
||||
interface HTMLNlSelectElementEventMap {
|
||||
"selectDomain": string;
|
||||
}
|
||||
interface HTMLNlSelectElement extends Components.NlSelect, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlSelectElementEventMap>(type: K, listener: (this: HTMLNlSelectElement, ev: NlSelectCustomEvent<HTMLNlSelectElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlSelectElementEventMap>(type: K, listener: (this: HTMLNlSelectElement, ev: NlSelectCustomEvent<HTMLNlSelectElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlSelectElement: {
|
||||
prototype: HTMLNlSelectElement;
|
||||
new (): HTMLNlSelectElement;
|
||||
};
|
||||
interface HTMLNlSigninElementEventMap {
|
||||
"nlLogin": string;
|
||||
"nlCheckLogin": string;
|
||||
}
|
||||
interface HTMLNlSigninElement extends Components.NlSignin, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlSigninElementEventMap>(type: K, listener: (this: HTMLNlSigninElement, ev: NlSigninCustomEvent<HTMLNlSigninElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlSigninElementEventMap>(type: K, listener: (this: HTMLNlSigninElement, ev: NlSigninCustomEvent<HTMLNlSigninElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlSigninElement: {
|
||||
prototype: HTMLNlSigninElement;
|
||||
new (): HTMLNlSigninElement;
|
||||
};
|
||||
interface HTMLNlSigninBunkerUrlElementEventMap {
|
||||
"nlLogin": string;
|
||||
"nlCheckLogin": string;
|
||||
}
|
||||
interface HTMLNlSigninBunkerUrlElement extends Components.NlSigninBunkerUrl, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlSigninBunkerUrlElementEventMap>(type: K, listener: (this: HTMLNlSigninBunkerUrlElement, ev: NlSigninBunkerUrlCustomEvent<HTMLNlSigninBunkerUrlElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlSigninBunkerUrlElementEventMap>(type: K, listener: (this: HTMLNlSigninBunkerUrlElement, ev: NlSigninBunkerUrlCustomEvent<HTMLNlSigninBunkerUrlElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlSigninBunkerUrlElement: {
|
||||
prototype: HTMLNlSigninBunkerUrlElement;
|
||||
new (): HTMLNlSigninBunkerUrlElement;
|
||||
};
|
||||
interface HTMLNlSigninConnectionStringElementEventMap {
|
||||
"nlNostrConnectDefault": void;
|
||||
}
|
||||
interface HTMLNlSigninConnectionStringElement extends Components.NlSigninConnectionString, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlSigninConnectionStringElementEventMap>(type: K, listener: (this: HTMLNlSigninConnectionStringElement, ev: NlSigninConnectionStringCustomEvent<HTMLNlSigninConnectionStringElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlSigninConnectionStringElementEventMap>(type: K, listener: (this: HTMLNlSigninConnectionStringElement, ev: NlSigninConnectionStringCustomEvent<HTMLNlSigninConnectionStringElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlSigninConnectionStringElement: {
|
||||
prototype: HTMLNlSigninConnectionStringElement;
|
||||
new (): HTMLNlSigninConnectionStringElement;
|
||||
};
|
||||
interface HTMLNlSigninOtpElementEventMap {
|
||||
"nlLoginOTPUser": string;
|
||||
"nlLoginOTPCode": string;
|
||||
"nlCheckLogin": string;
|
||||
}
|
||||
interface HTMLNlSigninOtpElement extends Components.NlSigninOtp, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlSigninOtpElementEventMap>(type: K, listener: (this: HTMLNlSigninOtpElement, ev: NlSigninOtpCustomEvent<HTMLNlSigninOtpElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlSigninOtpElementEventMap>(type: K, listener: (this: HTMLNlSigninOtpElement, ev: NlSigninOtpCustomEvent<HTMLNlSigninOtpElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlSigninOtpElement: {
|
||||
prototype: HTMLNlSigninOtpElement;
|
||||
new (): HTMLNlSigninOtpElement;
|
||||
};
|
||||
interface HTMLNlSigninReadOnlyElementEventMap {
|
||||
"nlLoginReadOnly": string;
|
||||
"nlCheckLogin": string;
|
||||
}
|
||||
interface HTMLNlSigninReadOnlyElement extends Components.NlSigninReadOnly, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlSigninReadOnlyElementEventMap>(type: K, listener: (this: HTMLNlSigninReadOnlyElement, ev: NlSigninReadOnlyCustomEvent<HTMLNlSigninReadOnlyElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlSigninReadOnlyElementEventMap>(type: K, listener: (this: HTMLNlSigninReadOnlyElement, ev: NlSigninReadOnlyCustomEvent<HTMLNlSigninReadOnlyElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlSigninReadOnlyElement: {
|
||||
prototype: HTMLNlSigninReadOnlyElement;
|
||||
new (): HTMLNlSigninReadOnlyElement;
|
||||
};
|
||||
interface HTMLNlSignupElementEventMap {
|
||||
"nlSignup": string;
|
||||
"nlCheckSignup": string;
|
||||
"fetchHandler": boolean;
|
||||
}
|
||||
interface HTMLNlSignupElement extends Components.NlSignup, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlSignupElementEventMap>(type: K, listener: (this: HTMLNlSignupElement, ev: NlSignupCustomEvent<HTMLNlSignupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlSignupElementEventMap>(type: K, listener: (this: HTMLNlSignupElement, ev: NlSignupCustomEvent<HTMLNlSignupElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlSignupElement: {
|
||||
prototype: HTMLNlSignupElement;
|
||||
new (): HTMLNlSignupElement;
|
||||
};
|
||||
interface HTMLNlWelcomeElement extends Components.NlWelcome, HTMLStencilElement {
|
||||
}
|
||||
var HTMLNlWelcomeElement: {
|
||||
prototype: HTMLNlWelcomeElement;
|
||||
new (): HTMLNlWelcomeElement;
|
||||
};
|
||||
interface HTMLNlWelcomeSigninElementEventMap {
|
||||
"nlLoginExtension": void;
|
||||
}
|
||||
interface HTMLNlWelcomeSigninElement extends Components.NlWelcomeSignin, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLNlWelcomeSigninElementEventMap>(type: K, listener: (this: HTMLNlWelcomeSigninElement, ev: NlWelcomeSigninCustomEvent<HTMLNlWelcomeSigninElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLNlWelcomeSigninElementEventMap>(type: K, listener: (this: HTMLNlWelcomeSigninElement, ev: NlWelcomeSigninCustomEvent<HTMLNlWelcomeSigninElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLNlWelcomeSigninElement: {
|
||||
prototype: HTMLNlWelcomeSigninElement;
|
||||
new (): HTMLNlWelcomeSigninElement;
|
||||
};
|
||||
interface HTMLNlWelcomeSignupElement extends Components.NlWelcomeSignup, HTMLStencilElement {
|
||||
}
|
||||
var HTMLNlWelcomeSignupElement: {
|
||||
prototype: HTMLNlWelcomeSignupElement;
|
||||
new (): HTMLNlWelcomeSignupElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"button-base": HTMLButtonBaseElement;
|
||||
"nl-auth": HTMLNlAuthElement;
|
||||
"nl-banner": HTMLNlBannerElement;
|
||||
"nl-button": HTMLNlButtonElement;
|
||||
"nl-change-account": HTMLNlChangeAccountElement;
|
||||
"nl-confirm-logout": HTMLNlConfirmLogoutElement;
|
||||
"nl-connect": HTMLNlConnectElement;
|
||||
"nl-dialog": HTMLNlDialogElement;
|
||||
"nl-iframe": HTMLNlIframeElement;
|
||||
"nl-import-flow": HTMLNlImportFlowElement;
|
||||
"nl-info": HTMLNlInfoElement;
|
||||
"nl-info-extension": HTMLNlInfoExtensionElement;
|
||||
"nl-loading": HTMLNlLoadingElement;
|
||||
"nl-local-signup": HTMLNlLocalSignupElement;
|
||||
"nl-login-status": HTMLNlLoginStatusElement;
|
||||
"nl-otp-migrate": HTMLNlOtpMigrateElement;
|
||||
"nl-previously-logged": HTMLNlPreviouslyLoggedElement;
|
||||
"nl-select": HTMLNlSelectElement;
|
||||
"nl-signin": HTMLNlSigninElement;
|
||||
"nl-signin-bunker-url": HTMLNlSigninBunkerUrlElement;
|
||||
"nl-signin-connection-string": HTMLNlSigninConnectionStringElement;
|
||||
"nl-signin-otp": HTMLNlSigninOtpElement;
|
||||
"nl-signin-read-only": HTMLNlSigninReadOnlyElement;
|
||||
"nl-signup": HTMLNlSignupElement;
|
||||
"nl-welcome": HTMLNlWelcomeElement;
|
||||
"nl-welcome-signin": HTMLNlWelcomeSigninElement;
|
||||
"nl-welcome-signup": HTMLNlWelcomeSignupElement;
|
||||
}
|
||||
}
|
||||
declare namespace LocalJSX {
|
||||
interface ButtonBase {
|
||||
"darkMode"?: boolean;
|
||||
"disabled"?: boolean;
|
||||
"theme"?: NlTheme;
|
||||
"titleBtn"?: string;
|
||||
}
|
||||
interface NlAuth {
|
||||
"accounts"?: Info[];
|
||||
"authMethods"?: AuthMethod[];
|
||||
"authUrl"?: string;
|
||||
"bunkers"?: string;
|
||||
"connectionString"?: string;
|
||||
"connectionStringServices"?: ConnectionString[];
|
||||
"darkMode"?: boolean;
|
||||
"error"?: string;
|
||||
"hasExtension"?: boolean;
|
||||
"hasOTP"?: boolean;
|
||||
"iframeUrl"?: string;
|
||||
"isLoading"?: boolean;
|
||||
"isLoadingExtension"?: boolean;
|
||||
"isOTP"?: boolean;
|
||||
"localSignup"?: boolean;
|
||||
"njumpIframe"?: string;
|
||||
"onNlChangeDarkMode"?: (event: NlAuthCustomEvent<boolean>) => void;
|
||||
"onNlCloseModal"?: (event: NlAuthCustomEvent<any>) => void;
|
||||
"onNlNostrConnectDefaultCancel"?: (event: NlAuthCustomEvent<void>) => void;
|
||||
"recents"?: RecentType[];
|
||||
"signupNjump"?: boolean;
|
||||
"startScreen"?: string;
|
||||
"theme"?: NlTheme;
|
||||
"welcomeDescription"?: string;
|
||||
"welcomeTitle"?: string;
|
||||
}
|
||||
interface NlBanner {
|
||||
"accounts"?: Info[];
|
||||
"darkMode"?: boolean;
|
||||
"hiddenMode"?: boolean;
|
||||
"isLoading"?: boolean;
|
||||
"isOpen"?: boolean;
|
||||
"notify"?: BannerNotify | null;
|
||||
"onHandleConfirmLogout"?: (event: NlBannerCustomEvent<string>) => void;
|
||||
"onHandleImportModal"?: (event: NlBannerCustomEvent<string>) => void;
|
||||
"onHandleLoginBanner"?: (event: NlBannerCustomEvent<string>) => void;
|
||||
"onHandleLogoutBanner"?: (event: NlBannerCustomEvent<string>) => void;
|
||||
"onHandleNotifyConfirmBanner"?: (event: NlBannerCustomEvent<string>) => void;
|
||||
"onHandleNotifyConfirmBannerIframe"?: (event: NlBannerCustomEvent<string>) => void;
|
||||
"onHandleOpenWelcomeModal"?: (event: NlBannerCustomEvent<string>) => void;
|
||||
"theme"?: NlTheme;
|
||||
"titleBanner"?: string;
|
||||
"userInfo"?: Info | null;
|
||||
}
|
||||
interface NlButton {
|
||||
"darkMode"?: boolean;
|
||||
"disabled"?: boolean;
|
||||
"theme"?: NlTheme;
|
||||
"titleBtn"?: string;
|
||||
}
|
||||
interface NlChangeAccount {
|
||||
"accounts"?: Info[];
|
||||
"currentAccount"?: Info;
|
||||
"darkMode"?: boolean;
|
||||
"onHandleOpenWelcomeModal"?: (event: NlChangeAccountCustomEvent<string>) => void;
|
||||
"onHandleSwitchAccount"?: (event: NlChangeAccountCustomEvent<Info>) => void;
|
||||
"theme"?: 'default' | 'ocean' | 'lemonade' | 'purple';
|
||||
}
|
||||
interface NlConfirmLogout {
|
||||
"description"?: string;
|
||||
"onHandleBackUpModal"?: (event: NlConfirmLogoutCustomEvent<string>) => void;
|
||||
"onHandleLogoutBanner"?: (event: NlConfirmLogoutCustomEvent<string>) => void;
|
||||
"onNlCloseModal"?: (event: NlConfirmLogoutCustomEvent<any>) => void;
|
||||
"titleModal"?: string;
|
||||
}
|
||||
interface NlConnect {
|
||||
"authMethods"?: AuthMethod[];
|
||||
"connectionStringServices"?: ConnectionString[];
|
||||
"hasOTP"?: boolean;
|
||||
"onNlNostrConnect"?: (event: NlConnectCustomEvent<ConnectionString>) => void;
|
||||
"titleWelcome"?: string;
|
||||
}
|
||||
interface NlDialog {
|
||||
}
|
||||
interface NlIframe {
|
||||
"description"?: string;
|
||||
"iframeUrl"?: string;
|
||||
"onNlCloseModal"?: (event: NlIframeCustomEvent<any>) => void;
|
||||
"titleModal"?: string;
|
||||
}
|
||||
interface NlImportFlow {
|
||||
"onNlExportKeys"?: (event: NlImportFlowCustomEvent<void>) => void;
|
||||
"onNlImportAccount"?: (event: NlImportFlowCustomEvent<ConnectionString>) => void;
|
||||
"services"?: ConnectionString[];
|
||||
"titleImport"?: string;
|
||||
"titleInfo"?: string;
|
||||
}
|
||||
interface NlInfo {
|
||||
"darkMode"?: boolean;
|
||||
"theme"?: NlTheme;
|
||||
}
|
||||
interface NlInfoExtension {
|
||||
}
|
||||
interface NlLoading {
|
||||
"onHandleContinue"?: (event: NlLoadingCustomEvent<boolean>) => void;
|
||||
"onStopFetchHandler"?: (event: NlLoadingCustomEvent<boolean>) => void;
|
||||
"path"?: string;
|
||||
}
|
||||
interface NlLocalSignup {
|
||||
"description"?: string;
|
||||
"descriptionNjump"?: string;
|
||||
"onFetchHandler"?: (event: NlLocalSignupCustomEvent<boolean>) => void;
|
||||
"onNlLocalSignup"?: (event: NlLocalSignupCustomEvent<string>) => void;
|
||||
"onNlSignupNjump"?: (event: NlLocalSignupCustomEvent<void>) => void;
|
||||
"signupNjump"?: boolean;
|
||||
"titleSignup"?: string;
|
||||
}
|
||||
interface NlLoginStatus {
|
||||
"info"?: RecentType | Info | undefined;
|
||||
}
|
||||
interface NlOtpMigrate {
|
||||
"onNlImportAccount"?: (event: NlOtpMigrateCustomEvent<ConnectionString>) => void;
|
||||
"services"?: ConnectionString[];
|
||||
"textImport"?: string;
|
||||
"titleImport"?: string;
|
||||
"titleInfo"?: string;
|
||||
}
|
||||
interface NlPreviouslyLogged {
|
||||
"accounts"?: Info[];
|
||||
"description"?: string;
|
||||
"onNlLoginRecentAccount"?: (event: NlPreviouslyLoggedCustomEvent<RecentType>) => void;
|
||||
"onNlRemoveRecent"?: (event: NlPreviouslyLoggedCustomEvent<RecentType>) => void;
|
||||
"onNlSwitchAccount"?: (event: NlPreviouslyLoggedCustomEvent<Info>) => void;
|
||||
"recents"?: RecentType[];
|
||||
"titlePage"?: string;
|
||||
}
|
||||
interface NlSelect {
|
||||
"darkMode"?: boolean;
|
||||
"onSelectDomain"?: (event: NlSelectCustomEvent<string>) => void;
|
||||
"options"?: OptionType[];
|
||||
"selected"?: number;
|
||||
"theme"?: 'default' | 'ocean' | 'lemonade' | 'purple';
|
||||
}
|
||||
interface NlSignin {
|
||||
"description"?: string;
|
||||
"onNlCheckLogin"?: (event: NlSigninCustomEvent<string>) => void;
|
||||
"onNlLogin"?: (event: NlSigninCustomEvent<string>) => void;
|
||||
"titleLogin"?: string;
|
||||
}
|
||||
interface NlSigninBunkerUrl {
|
||||
"description"?: string;
|
||||
"onNlCheckLogin"?: (event: NlSigninBunkerUrlCustomEvent<string>) => void;
|
||||
"onNlLogin"?: (event: NlSigninBunkerUrlCustomEvent<string>) => void;
|
||||
"titleLogin"?: string;
|
||||
}
|
||||
interface NlSigninConnectionString {
|
||||
"connectionString"?: string;
|
||||
"description"?: string;
|
||||
"onNlNostrConnectDefault"?: (event: NlSigninConnectionStringCustomEvent<void>) => void;
|
||||
"titleLogin"?: string;
|
||||
}
|
||||
interface NlSigninOtp {
|
||||
"description"?: string;
|
||||
"descriptionOTP"?: string;
|
||||
"onNlCheckLogin"?: (event: NlSigninOtpCustomEvent<string>) => void;
|
||||
"onNlLoginOTPCode"?: (event: NlSigninOtpCustomEvent<string>) => void;
|
||||
"onNlLoginOTPUser"?: (event: NlSigninOtpCustomEvent<string>) => void;
|
||||
"titleLogin"?: string;
|
||||
"titleLoginOTP"?: string;
|
||||
}
|
||||
interface NlSigninReadOnly {
|
||||
"description"?: string;
|
||||
"onNlCheckLogin"?: (event: NlSigninReadOnlyCustomEvent<string>) => void;
|
||||
"onNlLoginReadOnly"?: (event: NlSigninReadOnlyCustomEvent<string>) => void;
|
||||
"titleLogin"?: string;
|
||||
}
|
||||
interface NlSignup {
|
||||
"bunkers"?: string;
|
||||
"description"?: string;
|
||||
"onFetchHandler"?: (event: NlSignupCustomEvent<boolean>) => void;
|
||||
"onNlCheckSignup"?: (event: NlSignupCustomEvent<string>) => void;
|
||||
"onNlSignup"?: (event: NlSignupCustomEvent<string>) => void;
|
||||
"titleSignup"?: string;
|
||||
}
|
||||
interface NlWelcome {
|
||||
"description"?: string;
|
||||
"titleWelcome"?: string;
|
||||
}
|
||||
interface NlWelcomeSignin {
|
||||
"authMethods"?: AuthMethod[];
|
||||
"hasExtension"?: boolean;
|
||||
"hasOTP"?: boolean;
|
||||
"onNlLoginExtension"?: (event: NlWelcomeSigninCustomEvent<void>) => void;
|
||||
"titleWelcome"?: string;
|
||||
}
|
||||
interface NlWelcomeSignup {
|
||||
"description"?: string;
|
||||
"titleWelcome"?: string;
|
||||
}
|
||||
interface IntrinsicElements {
|
||||
"button-base": ButtonBase;
|
||||
"nl-auth": NlAuth;
|
||||
"nl-banner": NlBanner;
|
||||
"nl-button": NlButton;
|
||||
"nl-change-account": NlChangeAccount;
|
||||
"nl-confirm-logout": NlConfirmLogout;
|
||||
"nl-connect": NlConnect;
|
||||
"nl-dialog": NlDialog;
|
||||
"nl-iframe": NlIframe;
|
||||
"nl-import-flow": NlImportFlow;
|
||||
"nl-info": NlInfo;
|
||||
"nl-info-extension": NlInfoExtension;
|
||||
"nl-loading": NlLoading;
|
||||
"nl-local-signup": NlLocalSignup;
|
||||
"nl-login-status": NlLoginStatus;
|
||||
"nl-otp-migrate": NlOtpMigrate;
|
||||
"nl-previously-logged": NlPreviouslyLogged;
|
||||
"nl-select": NlSelect;
|
||||
"nl-signin": NlSignin;
|
||||
"nl-signin-bunker-url": NlSigninBunkerUrl;
|
||||
"nl-signin-connection-string": NlSigninConnectionString;
|
||||
"nl-signin-otp": NlSigninOtp;
|
||||
"nl-signin-read-only": NlSigninReadOnly;
|
||||
"nl-signup": NlSignup;
|
||||
"nl-welcome": NlWelcome;
|
||||
"nl-welcome-signin": NlWelcomeSignin;
|
||||
"nl-welcome-signup": NlWelcomeSignup;
|
||||
}
|
||||
}
|
||||
export { LocalJSX as JSX };
|
||||
declare module "@stencil/core" {
|
||||
export namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"button-base": LocalJSX.ButtonBase & JSXBase.HTMLAttributes<HTMLButtonBaseElement>;
|
||||
"nl-auth": LocalJSX.NlAuth & JSXBase.HTMLAttributes<HTMLNlAuthElement>;
|
||||
"nl-banner": LocalJSX.NlBanner & JSXBase.HTMLAttributes<HTMLNlBannerElement>;
|
||||
"nl-button": LocalJSX.NlButton & JSXBase.HTMLAttributes<HTMLNlButtonElement>;
|
||||
"nl-change-account": LocalJSX.NlChangeAccount & JSXBase.HTMLAttributes<HTMLNlChangeAccountElement>;
|
||||
"nl-confirm-logout": LocalJSX.NlConfirmLogout & JSXBase.HTMLAttributes<HTMLNlConfirmLogoutElement>;
|
||||
"nl-connect": LocalJSX.NlConnect & JSXBase.HTMLAttributes<HTMLNlConnectElement>;
|
||||
"nl-dialog": LocalJSX.NlDialog & JSXBase.HTMLAttributes<HTMLNlDialogElement>;
|
||||
"nl-iframe": LocalJSX.NlIframe & JSXBase.HTMLAttributes<HTMLNlIframeElement>;
|
||||
"nl-import-flow": LocalJSX.NlImportFlow & JSXBase.HTMLAttributes<HTMLNlImportFlowElement>;
|
||||
"nl-info": LocalJSX.NlInfo & JSXBase.HTMLAttributes<HTMLNlInfoElement>;
|
||||
"nl-info-extension": LocalJSX.NlInfoExtension & JSXBase.HTMLAttributes<HTMLNlInfoExtensionElement>;
|
||||
"nl-loading": LocalJSX.NlLoading & JSXBase.HTMLAttributes<HTMLNlLoadingElement>;
|
||||
"nl-local-signup": LocalJSX.NlLocalSignup & JSXBase.HTMLAttributes<HTMLNlLocalSignupElement>;
|
||||
"nl-login-status": LocalJSX.NlLoginStatus & JSXBase.HTMLAttributes<HTMLNlLoginStatusElement>;
|
||||
"nl-otp-migrate": LocalJSX.NlOtpMigrate & JSXBase.HTMLAttributes<HTMLNlOtpMigrateElement>;
|
||||
"nl-previously-logged": LocalJSX.NlPreviouslyLogged & JSXBase.HTMLAttributes<HTMLNlPreviouslyLoggedElement>;
|
||||
"nl-select": LocalJSX.NlSelect & JSXBase.HTMLAttributes<HTMLNlSelectElement>;
|
||||
"nl-signin": LocalJSX.NlSignin & JSXBase.HTMLAttributes<HTMLNlSigninElement>;
|
||||
"nl-signin-bunker-url": LocalJSX.NlSigninBunkerUrl & JSXBase.HTMLAttributes<HTMLNlSigninBunkerUrlElement>;
|
||||
"nl-signin-connection-string": LocalJSX.NlSigninConnectionString & JSXBase.HTMLAttributes<HTMLNlSigninConnectionStringElement>;
|
||||
"nl-signin-otp": LocalJSX.NlSigninOtp & JSXBase.HTMLAttributes<HTMLNlSigninOtpElement>;
|
||||
"nl-signin-read-only": LocalJSX.NlSigninReadOnly & JSXBase.HTMLAttributes<HTMLNlSigninReadOnlyElement>;
|
||||
"nl-signup": LocalJSX.NlSignup & JSXBase.HTMLAttributes<HTMLNlSignupElement>;
|
||||
"nl-welcome": LocalJSX.NlWelcome & JSXBase.HTMLAttributes<HTMLNlWelcomeElement>;
|
||||
"nl-welcome-signin": LocalJSX.NlWelcomeSignin & JSXBase.HTMLAttributes<HTMLNlWelcomeSigninElement>;
|
||||
"nl-welcome-signup": LocalJSX.NlWelcomeSignup & JSXBase.HTMLAttributes<HTMLNlWelcomeSignupElement>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.animate-spin-loading {
|
||||
background: var(--qa-dark-color);
|
||||
animation: spin2 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin2 {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
animation: blink 0.7s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Component, h, Prop, Element } from '@stencil/core';
|
||||
import { NlTheme } from '@/types';
|
||||
import { IButton } from '@/types/button';
|
||||
|
||||
@Component({
|
||||
tag: 'button-base',
|
||||
styleUrl: 'button-base.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class ButtonBase implements IButton {
|
||||
@Element() element: HTMLElement;
|
||||
@Prop({ mutable: true }) theme: NlTheme = 'default';
|
||||
@Prop({ mutable: true }) darkMode: boolean = false;
|
||||
@Prop() titleBtn = 'Open modal';
|
||||
@Prop() disabled = false;
|
||||
|
||||
componentDidRender() {
|
||||
const svgElement = this.element.querySelector('svg');
|
||||
|
||||
if (svgElement) {
|
||||
svgElement.classList.add('flex-shrink-0', 'w-4', 'h-4', 'block');
|
||||
svgElement.removeAttribute('style'); // hack frieze svg
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class={`theme-${this.theme}`}>
|
||||
<div class="animate-spin-loading active"></div>
|
||||
<button
|
||||
disabled={this.disabled}
|
||||
type="button"
|
||||
class="nl-button py-2.5 px-3 w-full inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
|
||||
>
|
||||
<slot name="icon-start" />
|
||||
{this.titleBtn}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
# button-base
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------- | ----------- | ----------- | -------------------------------------------------------------------- | -------------- |
|
||||
| `darkMode` | `dark-mode` | | `boolean` | `false` |
|
||||
| `disabled` | `disabled` | | `boolean` | `false` |
|
||||
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
|
||||
| `titleBtn` | `title-btn` | | `string` | `'Open modal'` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-banner](../nl-banner)
|
||||
- [nl-button](../nl-button)
|
||||
- [nl-confirm-logout](../nl-confirm-logout)
|
||||
- [nl-connect](../nl-connect)
|
||||
- [nl-import-flow](../nl-import-flow)
|
||||
- [nl-loading](../nl-loading)
|
||||
- [nl-local-signup](../nl-local-signup)
|
||||
- [nl-otp-migrate](../nl-otp-migrate)
|
||||
- [nl-signin](../nl-signin)
|
||||
- [nl-signin-bunker-url](../nl-signin-bunker-url)
|
||||
- [nl-signin-otp](../nl-signin-otp)
|
||||
- [nl-signin-read-only](../nl-signin-read-only)
|
||||
- [nl-signup](../nl-signup)
|
||||
- [nl-welcome](../nl-welcome)
|
||||
- [nl-welcome-signin](../nl-welcome-signin)
|
||||
- [nl-welcome-signup](../nl-welcome-signup)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-banner --> button-base
|
||||
nl-button --> button-base
|
||||
nl-confirm-logout --> button-base
|
||||
nl-connect --> button-base
|
||||
nl-import-flow --> button-base
|
||||
nl-loading --> button-base
|
||||
nl-local-signup --> button-base
|
||||
nl-otp-migrate --> button-base
|
||||
nl-signin --> button-base
|
||||
nl-signin-bunker-url --> button-base
|
||||
nl-signin-otp --> button-base
|
||||
nl-signin-read-only --> button-base
|
||||
nl-signup --> button-base
|
||||
nl-welcome --> button-base
|
||||
nl-welcome-signin --> button-base
|
||||
nl-welcome-signup --> button-base
|
||||
style button-base fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
import { Component, Event, EventEmitter, Fragment, h, Prop, Watch } from '@stencil/core';
|
||||
import { AuthMethod, ConnectionString, CURRENT_MODULE, Info, NlTheme, RecentType } from '@/types';
|
||||
import { state } from '@/store';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-auth',
|
||||
styleUrl: 'nl-auth.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class NlAuth {
|
||||
@Prop({ mutable: true }) theme: NlTheme = 'default';
|
||||
@Prop() bunkers: string = '';
|
||||
@Prop() startScreen: string = CURRENT_MODULE.WELCOME;
|
||||
@Prop() authMethods: AuthMethod[] = [];
|
||||
@Prop() hasExtension: boolean = false;
|
||||
@Prop() hasOTP: boolean = false;
|
||||
@Prop() isLoading: boolean = false;
|
||||
@Prop() isLoadingExtension: boolean = false;
|
||||
@Prop() isOTP: boolean = false;
|
||||
@Prop() authUrl: string = '';
|
||||
@Prop() iframeUrl: string = '';
|
||||
@Prop() error: string = '';
|
||||
@Prop() localSignup: boolean = false;
|
||||
@Prop() signupNjump: boolean = false;
|
||||
@Prop() njumpIframe: string = '';
|
||||
@Prop({ mutable: true }) accounts: Info[] = [];
|
||||
@Prop({ mutable: true }) recents: RecentType[] = [];
|
||||
@Prop({ mutable: true }) darkMode: boolean = false;
|
||||
@Prop() welcomeTitle: string = '';
|
||||
@Prop() welcomeDescription: string = '';
|
||||
@Prop() connectionString: string = '';
|
||||
@Prop() connectionStringServices: ConnectionString[] = [];
|
||||
|
||||
@Event() nlCloseModal: EventEmitter;
|
||||
@Event() nlChangeDarkMode: EventEmitter<boolean>;
|
||||
@Event() nlNostrConnectDefaultCancel: EventEmitter<void>;
|
||||
|
||||
prevPath: string = '';
|
||||
|
||||
@Watch('isLoading')
|
||||
watchLoadingHandler(newValue: boolean) {
|
||||
state.isLoading = newValue;
|
||||
}
|
||||
|
||||
@Watch('isLoadingExtension')
|
||||
watchLoadingExtensionHandler(newValue: boolean) {
|
||||
state.isLoadingExtension = newValue;
|
||||
}
|
||||
|
||||
@Watch('isOTP')
|
||||
watchOTPHandler(newValue: boolean) {
|
||||
state.isOTP = newValue;
|
||||
}
|
||||
|
||||
@Watch('authUrl')
|
||||
watchAuthUrlHandler(newValue: string) {
|
||||
state.authUrl = newValue;
|
||||
}
|
||||
|
||||
@Watch('iframeUrl')
|
||||
watchIframeUrlHandler(newValue: string) {
|
||||
state.iframeUrl = newValue;
|
||||
}
|
||||
|
||||
@Watch('njumpIframe')
|
||||
watchNjumpIframeHandler(newValue: string) {
|
||||
state.njumpIframe = newValue;
|
||||
}
|
||||
|
||||
@Watch('error')
|
||||
watchErrorHandler(newValue: string) {
|
||||
state.error = newValue;
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.nlCloseModal.emit();
|
||||
}
|
||||
|
||||
handleChangeDarkMode() {
|
||||
this.nlChangeDarkMode.emit(!this.darkMode);
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
// init state
|
||||
state.path = [this.startScreen as CURRENT_MODULE];
|
||||
state.error = '';
|
||||
state.iframeUrl = '';
|
||||
state.authUrl = '';
|
||||
state.isLoading = false;
|
||||
state.isLoadingExtension = false;
|
||||
state.isOTP = false;
|
||||
|
||||
console.log('path', state.path);
|
||||
}
|
||||
|
||||
handleClickToBack() {
|
||||
state.path.pop();
|
||||
state.path = [...state.path];
|
||||
|
||||
// reset
|
||||
state.isLoading = false;
|
||||
state.isLoadingExtension = false;
|
||||
state.authUrl = '';
|
||||
state.isOTP = false;
|
||||
}
|
||||
|
||||
switchSignSignUpStrategy(str: CURRENT_MODULE) {
|
||||
if (CURRENT_MODULE.LOCAL_SIGNUP === str) {
|
||||
state.path = [CURRENT_MODULE.WELCOME, CURRENT_MODULE.WELCOME_SIGNUP, str];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.path = [CURRENT_MODULE.WELCOME, str];
|
||||
}
|
||||
|
||||
render() {
|
||||
const classWrapper = `w-full h-full fixed top-0 start-0 z-[80] overflow-x-hidden overflow-y-auto flex items-center ${this.darkMode ? 'dark' : ''}`;
|
||||
const currentModule = state.path.at(-1);
|
||||
|
||||
if (currentModule !== this.prevPath && this.prevPath === CURRENT_MODULE.CONNECTION_STRING) {
|
||||
this.nlNostrConnectDefaultCancel.emit();
|
||||
}
|
||||
this.prevPath = currentModule;
|
||||
|
||||
const renderModule = () => {
|
||||
if (state.isOTP) return <nl-signin-otp />;
|
||||
|
||||
// @ts-ignore
|
||||
// const t: CURRENT_MODULE = 'import' // lastValuePath
|
||||
|
||||
switch (currentModule) {
|
||||
case CURRENT_MODULE.WELCOME:
|
||||
return <nl-welcome titleWelcome={this.welcomeTitle || undefined} description={this.welcomeDescription || undefined} />;
|
||||
case CURRENT_MODULE.LOGIN:
|
||||
return <nl-signin />;
|
||||
case CURRENT_MODULE.SIGNUP:
|
||||
return <nl-signup bunkers={this.bunkers} />;
|
||||
case CURRENT_MODULE.LOCAL_SIGNUP:
|
||||
return <nl-local-signup signupNjump={this.signupNjump} />;
|
||||
case CURRENT_MODULE.CONFIRM_LOGOUT:
|
||||
return <nl-confirm-logout />;
|
||||
case CURRENT_MODULE.IMPORT_FLOW:
|
||||
return <nl-import-flow services={this.connectionStringServices} />;
|
||||
case CURRENT_MODULE.IMPORT_OTP:
|
||||
return <nl-otp-migrate services={this.connectionStringServices} />;
|
||||
case CURRENT_MODULE.INFO:
|
||||
return <nl-info theme={this.theme} darkMode={this.darkMode} />;
|
||||
case CURRENT_MODULE.EXTENSION:
|
||||
return <nl-info-extension />;
|
||||
case CURRENT_MODULE.LOGIN_READ_ONLY:
|
||||
return <nl-signin-read-only />;
|
||||
case CURRENT_MODULE.LOGIN_BUNKER_URL:
|
||||
return <nl-signin-bunker-url />;
|
||||
case CURRENT_MODULE.LOGIN_OTP:
|
||||
return <nl-signin-otp />;
|
||||
case CURRENT_MODULE.WELCOME_LOGIN:
|
||||
return <nl-welcome-signin hasOTP={this.hasOTP} authMethods={this.authMethods} hasExtension={this.hasExtension} />;
|
||||
case CURRENT_MODULE.WELCOME_SIGNUP:
|
||||
return <nl-welcome-signup />;
|
||||
case CURRENT_MODULE.CONNECTION_STRING:
|
||||
return <nl-signin-connection-string connectionString={this.connectionString} />;
|
||||
case CURRENT_MODULE.CONNECT:
|
||||
return <nl-connect connectionStringServices={this.connectionStringServices} authMethods={this.authMethods} />;
|
||||
case CURRENT_MODULE.PREVIOUSLY_LOGGED:
|
||||
return <nl-previously-logged accounts={this.accounts} recents={this.recents} />;
|
||||
case CURRENT_MODULE.IFRAME:
|
||||
return <nl-iframe iframeUrl={this.authUrl} />;
|
||||
default:
|
||||
return <nl-welcome />;
|
||||
}
|
||||
};
|
||||
|
||||
const showLogin =
|
||||
state.isOTP ||
|
||||
(currentModule !== CURRENT_MODULE.INFO &&
|
||||
currentModule !== CURRENT_MODULE.CONFIRM_LOGOUT &&
|
||||
currentModule !== CURRENT_MODULE.IMPORT_FLOW &&
|
||||
currentModule !== CURRENT_MODULE.WELCOME &&
|
||||
currentModule !== CURRENT_MODULE.EXTENSION &&
|
||||
currentModule !== CURRENT_MODULE.IFRAME &&
|
||||
currentModule !== CURRENT_MODULE.PREVIOUSLY_LOGGED);
|
||||
|
||||
const showSignup =
|
||||
currentModule !== CURRENT_MODULE.IFRAME &&
|
||||
(!this.authMethods.length || (!this.localSignup && this.authMethods.includes('connect')) || (this.localSignup && this.authMethods.includes('local')));
|
||||
|
||||
return (
|
||||
<div class={`theme-${this.theme}`} dir="ltr">
|
||||
<div class={classWrapper}>
|
||||
<div onClick={() => this.handleClose()} class="absolute top-0 left-0 w-full h-full bg-gray-500 bg-opacity-75 z-[80]" />
|
||||
|
||||
<div class="nl-bg relative z-[81] w-full flex flex-col rounded-xl sm:max-w-lg sm:w-full sm:mx-auto">
|
||||
<div class={`flex justify-between items-center py-3 px-4`}>
|
||||
<div class="flex gap-2 items-center">
|
||||
<svg class="w-7 h-7" width="225" height="224" viewBox="0 0 225 224" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="224.047" height="224" rx="64" fill={this.theme === 'laan' ? (this.darkMode ? 'white' : 'black') : '#6951FA'} />
|
||||
<path
|
||||
d="M162.441 135.941V88.0593C170.359 85.1674 176 77.5348 176 68.6696C176 57.2919 166.708 48 155.33 48C143.953 48 134.661 57.2444 134.661 68.6696C134.661 77.5822 140.302 85.1674 148.219 88.0593V135.941C147.698 136.13 147.176 136.367 146.655 136.604L87.3956 77.3452C88.6282 74.6904 89.2919 71.7511 89.2919 68.6696C89.2919 57.2444 80.0474 48 68.6696 48C57.2919 48 48 57.2444 48 68.6696C48 77.5822 53.6415 85.1674 61.5585 88.0593V135.941C53.6415 138.833 48 146.465 48 155.33C48 166.708 57.2444 176 68.6696 176C80.0948 176 89.3393 166.708 89.3393 155.33C89.3393 146.418 83.6978 138.833 75.7807 135.941V88.0593C76.3022 87.8696 76.8237 87.6326 77.3452 87.3956L136.604 146.655C135.372 149.31 134.708 152.249 134.708 155.33C134.708 166.708 143.953 176 155.378 176C166.803 176 176.047 166.708 176.047 155.33C176.047 146.418 170.406 138.833 162.489 135.941H162.441Z"
|
||||
fill={this.theme === 'laan' ? (this.darkMode ? 'black' : 'white') : 'white'}
|
||||
/>
|
||||
</svg>
|
||||
<p class="font-bold nl-logo text-base">
|
||||
Nostr <span class="font-light">Login</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
onClick={() => this.handleChangeDarkMode()}
|
||||
type="button"
|
||||
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent"
|
||||
>
|
||||
<span class="sr-only">Change theme</span>
|
||||
{this.darkMode ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{!state.isLoading && (
|
||||
<button
|
||||
onClick={() => (state.path = [...state.path, CURRENT_MODULE.INFO])}
|
||||
type="button"
|
||||
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent"
|
||||
>
|
||||
<span class="sr-only">Info</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => this.handleClose()}
|
||||
type="button"
|
||||
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent"
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<svg
|
||||
class="flex-shrink-0 w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{state.path.length > 1 && !state.isLoading && (
|
||||
<div class="p-4">
|
||||
<button
|
||||
onClick={() => this.handleClickToBack()}
|
||||
type="button"
|
||||
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
|
||||
data-hs-overlay="#hs-vertically-centered-modal"
|
||||
>
|
||||
<span class="sr-only">Back</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{state.isLoading || state.authUrl ? (
|
||||
<nl-loading path={currentModule} />
|
||||
) : (
|
||||
<Fragment>
|
||||
{renderModule()}
|
||||
{showLogin && (
|
||||
<Fragment>
|
||||
{currentModule === CURRENT_MODULE.WELCOME_SIGNUP || currentModule === CURRENT_MODULE.SIGNUP || currentModule === CURRENT_MODULE.LOCAL_SIGNUP ? (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<p class="nl-footer font-light text-center text-sm pt-3 max-w-96 mx-auto">
|
||||
If you already have a profile please{' '}
|
||||
<span onClick={() => this.switchSignSignUpStrategy(CURRENT_MODULE.WELCOME_LOGIN)} class="cursor-pointer text-blue-400">
|
||||
log in
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
showSignup && (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<p class="nl-footer font-light text-center text-sm pt-3 max-w-96 mx-auto">
|
||||
If you don't have a profile please{' '}
|
||||
<span
|
||||
onClick={() =>
|
||||
this.localSignup ? this.switchSignSignUpStrategy(CURRENT_MODULE.LOCAL_SIGNUP) : this.switchSignSignUpStrategy(CURRENT_MODULE.WELCOME_SIGNUP)
|
||||
}
|
||||
class="cursor-pointer text-blue-400"
|
||||
>
|
||||
sign up
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
# nl-auth
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| -------------------------- | ---------------------- | ----------- | -------------------------------------------------------------------- | ------------------------ |
|
||||
| `accounts` | -- | | `Info[]` | `[]` |
|
||||
| `authMethods` | -- | | `AuthMethod[]` | `[]` |
|
||||
| `authUrl` | `auth-url` | | `string` | `''` |
|
||||
| `bunkers` | `bunkers` | | `string` | `''` |
|
||||
| `connectionString` | `connection-string` | | `string` | `''` |
|
||||
| `connectionStringServices` | -- | | `ConnectionString[]` | `[]` |
|
||||
| `darkMode` | `dark-mode` | | `boolean` | `false` |
|
||||
| `error` | `error` | | `string` | `''` |
|
||||
| `hasExtension` | `has-extension` | | `boolean` | `false` |
|
||||
| `hasOTP` | `has-o-t-p` | | `boolean` | `false` |
|
||||
| `iframeUrl` | `iframe-url` | | `string` | `''` |
|
||||
| `isLoading` | `is-loading` | | `boolean` | `false` |
|
||||
| `isLoadingExtension` | `is-loading-extension` | | `boolean` | `false` |
|
||||
| `isOTP` | `is-o-t-p` | | `boolean` | `false` |
|
||||
| `localSignup` | `local-signup` | | `boolean` | `false` |
|
||||
| `njumpIframe` | `njump-iframe` | | `string` | `''` |
|
||||
| `recents` | -- | | `RecentType[]` | `[]` |
|
||||
| `signupNjump` | `signup-njump` | | `boolean` | `false` |
|
||||
| `startScreen` | `start-screen` | | `string` | `CURRENT_MODULE.WELCOME` |
|
||||
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
|
||||
| `welcomeDescription` | `welcome-description` | | `string` | `''` |
|
||||
| `welcomeTitle` | `welcome-title` | | `string` | `''` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| ----------------------------- | ----------- | ---------------------- |
|
||||
| `nlChangeDarkMode` | | `CustomEvent<boolean>` |
|
||||
| `nlCloseModal` | | `CustomEvent<any>` |
|
||||
| `nlNostrConnectDefaultCancel` | | `CustomEvent<void>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Depends on
|
||||
|
||||
- [nl-signin-otp](../nl-signin-otp)
|
||||
- [nl-welcome](../nl-welcome)
|
||||
- [nl-signin](../nl-signin)
|
||||
- [nl-signup](../nl-signup)
|
||||
- [nl-local-signup](../nl-local-signup)
|
||||
- [nl-confirm-logout](../nl-confirm-logout)
|
||||
- [nl-import-flow](../nl-import-flow)
|
||||
- [nl-otp-migrate](../nl-otp-migrate)
|
||||
- [nl-info](../nl-info)
|
||||
- [nl-info-extension](../nl-info-extension)
|
||||
- [nl-signin-read-only](../nl-signin-read-only)
|
||||
- [nl-signin-bunker-url](../nl-signin-bunker-url)
|
||||
- [nl-welcome-signin](../nl-welcome-signin)
|
||||
- [nl-welcome-signup](../nl-welcome-signup)
|
||||
- [nl-signin-connection-string](../nl-signin-connection-string)
|
||||
- [nl-connect](../nl-connect)
|
||||
- [nl-previously-logged](../nl-previously-logged)
|
||||
- [nl-iframe](../nl-iframe)
|
||||
- [nl-loading](../nl-loading)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-auth --> nl-signin-otp
|
||||
nl-auth --> nl-welcome
|
||||
nl-auth --> nl-signin
|
||||
nl-auth --> nl-signup
|
||||
nl-auth --> nl-local-signup
|
||||
nl-auth --> nl-confirm-logout
|
||||
nl-auth --> nl-import-flow
|
||||
nl-auth --> nl-otp-migrate
|
||||
nl-auth --> nl-info
|
||||
nl-auth --> nl-info-extension
|
||||
nl-auth --> nl-signin-read-only
|
||||
nl-auth --> nl-signin-bunker-url
|
||||
nl-auth --> nl-welcome-signin
|
||||
nl-auth --> nl-welcome-signup
|
||||
nl-auth --> nl-signin-connection-string
|
||||
nl-auth --> nl-connect
|
||||
nl-auth --> nl-previously-logged
|
||||
nl-auth --> nl-iframe
|
||||
nl-auth --> nl-loading
|
||||
nl-signin-otp --> button-base
|
||||
nl-welcome --> button-base
|
||||
nl-signin --> button-base
|
||||
nl-signup --> nl-select
|
||||
nl-signup --> button-base
|
||||
nl-local-signup --> button-base
|
||||
nl-confirm-logout --> button-base
|
||||
nl-import-flow --> button-base
|
||||
nl-import-flow --> nl-select
|
||||
nl-otp-migrate --> nl-select
|
||||
nl-otp-migrate --> button-base
|
||||
nl-signin-read-only --> button-base
|
||||
nl-signin-bunker-url --> button-base
|
||||
nl-welcome-signin --> button-base
|
||||
nl-welcome-signup --> button-base
|
||||
nl-connect --> button-base
|
||||
nl-previously-logged --> nl-login-status
|
||||
nl-loading --> button-base
|
||||
style nl-auth fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,4 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
import { Component, Event, EventEmitter, Fragment, h, Prop, State, Watch } from '@stencil/core';
|
||||
import { BannerNotify, BannerNotifyMode, Info, METHOD_MODULE, NlTheme } from '@/types';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-banner',
|
||||
styleUrl: 'nl-banner.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class NlBanner {
|
||||
@Prop({ mutable: true }) theme: NlTheme = 'default';
|
||||
@Prop({ mutable: true }) darkMode: boolean = false;
|
||||
@Prop({ mutable: true }) hiddenMode: boolean = false;
|
||||
@Prop() titleBanner: string = '';
|
||||
@Prop({ mutable: true }) isOpen: boolean = false;
|
||||
|
||||
@Prop() isLoading: boolean = false;
|
||||
@Prop() notify: BannerNotify | null = null;
|
||||
@Prop() userInfo: Info | null = null;
|
||||
@Prop({ mutable: true }) accounts: Info[] = [];
|
||||
|
||||
@State() isUserImgError = false;
|
||||
|
||||
@State() domain: string = '';
|
||||
@State() mode: BannerNotifyMode = '';
|
||||
@State() url: string = '';
|
||||
@State() isOpenConfirm: boolean = false;
|
||||
|
||||
@Event() handleNotifyConfirmBanner: EventEmitter<string>;
|
||||
@Event() handleNotifyConfirmBannerIframe: EventEmitter<string>;
|
||||
@Event() handleLoginBanner: EventEmitter<string>;
|
||||
@Event() handleLogoutBanner: EventEmitter<string>;
|
||||
@Event() handleOpenWelcomeModal: EventEmitter<string>;
|
||||
@Event() handleConfirmLogout: EventEmitter<string>;
|
||||
@Event() handleImportModal: EventEmitter<string>;
|
||||
|
||||
@Watch('notify')
|
||||
watchNotifyHandler(notify: BannerNotify) {
|
||||
this.isOpen = true;
|
||||
this.isOpenConfirm = true;
|
||||
this.domain = this.userInfo?.domain || this.userInfo?.nip05?.split('@')?.[1] || '';
|
||||
|
||||
this.mode = notify.mode;
|
||||
this.url = notify.url;
|
||||
if (!this.mode) {
|
||||
this.isOpenConfirm = false;
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleOpen() {
|
||||
if (this.userInfo) {
|
||||
this.isOpen = true;
|
||||
} else {
|
||||
this.handleOpenWelcomeModal.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
handleLogin() {
|
||||
this.handleLoginBanner.emit(METHOD_MODULE.LOGIN);
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
handleSignup() {
|
||||
this.handleLoginBanner.emit(METHOD_MODULE.SIGNUP);
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
handleImport() {
|
||||
this.handleImportModal.emit();
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
handleLogout() {
|
||||
const isBackupKey = localStorage.getItem('backupKey');
|
||||
|
||||
if (isBackupKey) {
|
||||
this.handleLogoutBanner.emit(METHOD_MODULE.LOGOUT);
|
||||
this.handleClose();
|
||||
localStorage.removeItem('backupKey');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.userInfo.authMethod === 'local') {
|
||||
this.handleConfirmLogout.emit();
|
||||
} else {
|
||||
this.handleLogoutBanner.emit(METHOD_MODULE.LOGOUT);
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
switch (this.mode) {
|
||||
case 'authUrl':
|
||||
this.handleNotifyConfirmBanner.emit(this.url);
|
||||
break;
|
||||
case 'iframeAuthUrl':
|
||||
this.handleNotifyConfirmBannerIframe.emit(this.url);
|
||||
break;
|
||||
}
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const isShowImg = Boolean(this.userInfo?.picture);
|
||||
const userName = this.userInfo?.name || this.userInfo?.nip05?.split('@')?.[0] || this.userInfo?.pubkey || '';
|
||||
const isShowUserName = Boolean(userName);
|
||||
const isTemporary = this.userInfo && this.userInfo.authMethod === 'local';
|
||||
const isBackupKey = localStorage.getItem('backupKey');
|
||||
|
||||
const defaultUserAvatar = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<Fragment>
|
||||
<div class="block w-[48px] h-[46px] relative z-10">
|
||||
<div onClick={() => this.handleOpen()} class={`flex w-52 h-[46px] items-center pl-[11px]`}>
|
||||
<span
|
||||
class={`${this.isLoading ? 'w-5 h-5 border-[2px] mr-3.5 ml-[2px] opacity-1' : 'w-0 h-0 border-[0px] mr-0 opacity-0 ml-0'} animate-spin transition-all duration-300 ease-in-out inline-block border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full`}
|
||||
role="status"
|
||||
aria-label="loading"
|
||||
></span>
|
||||
|
||||
{this.userInfo ? (
|
||||
<div class={`uppercase font-bold w-6 h-6 mr-2 rounded-full border border-gray-200 flex justify-center items-center`}>
|
||||
{isShowImg ? (
|
||||
this.isUserImgError ? (
|
||||
defaultUserAvatar
|
||||
) : (
|
||||
<img class="w-full rounded-full" src={this.userInfo.picture} alt="Logo" onError={() => (this.isUserImgError = true)} />
|
||||
)
|
||||
) : isShowUserName ? (
|
||||
userName[0]
|
||||
) : (
|
||||
defaultUserAvatar
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex justify-center items-center">
|
||||
<svg class="w-6 h-6" width="225" height="224" viewBox="0 0 225 224" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="224.047" height="224" rx="64" fill={this.theme === 'laan' ? (this.darkMode ? 'white' : 'black') : '#6951FA'} />
|
||||
<path
|
||||
d="M162.441 135.941V88.0593C170.359 85.1674 176 77.5348 176 68.6696C176 57.2919 166.708 48 155.33 48C143.953 48 134.661 57.2444 134.661 68.6696C134.661 77.5822 140.302 85.1674 148.219 88.0593V135.941C147.698 136.13 147.176 136.367 146.655 136.604L87.3956 77.3452C88.6282 74.6904 89.2919 71.7511 89.2919 68.6696C89.2919 57.2444 80.0474 48 68.6696 48C57.2919 48 48 57.2444 48 68.6696C48 77.5822 53.6415 85.1674 61.5585 88.0593V135.941C53.6415 138.833 48 146.465 48 155.33C48 166.708 57.2444 176 68.6696 176C80.0948 176 89.3393 166.708 89.3393 155.33C89.3393 146.418 83.6978 138.833 75.7807 135.941V88.0593C76.3022 87.8696 76.8237 87.6326 77.3452 87.3956L136.604 146.655C135.372 149.31 134.708 152.249 134.708 155.33C134.708 166.708 143.953 176 155.378 176C166.803 176 176.047 166.708 176.047 155.33C176.047 146.418 170.406 138.833 162.489 135.941H162.441Z"
|
||||
fill={this.theme === 'laan' ? (this.darkMode ? 'black' : 'white') : 'white'}
|
||||
/>
|
||||
</svg>
|
||||
{this.isOpen && (
|
||||
<span class="px-2">
|
||||
<b>Nostr</b> Login
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.isOpen && isShowUserName && <div class="show-slow truncate w-16 text-xs">{userName}</div>}
|
||||
{this.isOpen && isShowUserName && <nl-login-status info={this.userInfo} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => this.handleClose()}
|
||||
type="button"
|
||||
class={`${this.isOpen ? 'z-20' : 'z-0'} nl-action-button absolute right-2 top-2 z-0 show-slow grid place-items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent`}
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<svg
|
||||
class="flex-shrink-0 w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="p-3 show-slow">
|
||||
{this.isOpenConfirm ? (
|
||||
<div>
|
||||
<div class="w-8 h-8 p-1/2 rounded-full border border-gray-200 bg-white mb-2 mt-2 show-slow m-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#5a68ff" class="w-full">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="mb-2 text-center max-w-40 min-w-40 mx-auto">
|
||||
{this.mode === 'timeout' ? 'Keys not responding, check your key storage app' : `Confirmation required at ${this.domain}`}
|
||||
</p>
|
||||
|
||||
{this.mode === 'timeout' ? (
|
||||
<a
|
||||
onClick={() => this.handleClose()}
|
||||
href={`https://${this.domain}`}
|
||||
target="_blank"
|
||||
class="nl-button text-nowrap py-2.5 px-3 w-full inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
|
||||
>
|
||||
Go to {this.domain}
|
||||
</a>
|
||||
) : this.mode === 'rebind' ? (
|
||||
<iframe src={this.url} width={'180'} height={'80'} frameBorder={'0'}></iframe>
|
||||
) : (
|
||||
<button-base onClick={() => this.handleConfirm()} titleBtn="Confirm" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div>
|
||||
{this.titleBanner && <p class="mb-2 text-center show-slow max-w-40 min-w-40 mx-auto">{this.titleBanner}</p>}
|
||||
{isTemporary && (
|
||||
<Fragment>
|
||||
{!isBackupKey && <p class="mb-2 text-center show-slow text-red-400 max-w-40 min-w-40 mx-auto">Your profile may be lost if you close this tab</p>}
|
||||
<div class="mb-2">
|
||||
<button-base onClick={() => this.handleImport()} theme="lemonade" titleBtn="Back up profile" />
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
<div class="mb-2">
|
||||
<nl-change-account currentAccount={this.userInfo} accounts={this.accounts} />
|
||||
</div>
|
||||
{/* {Boolean(this.listNotifies.length) && (
|
||||
<div
|
||||
onClick={() => this.handleRetryConfirm()}
|
||||
class="show-slow border border-yellow-600 text-yellow-600 bg-yellow-100 p-2 rounded-lg mb-2 cursor-pointer w-44 text-xs m-auto text-center"
|
||||
>
|
||||
Requests: {this.listNotifies.length}
|
||||
</div>
|
||||
)} */}
|
||||
{!this.userInfo ? (
|
||||
<div>
|
||||
<button-base onClick={() => this.handleLogin()} titleBtn="Log in">
|
||||
<svg
|
||||
style={{ display: 'none' }}
|
||||
slot="icon-start"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="flex-shrink-0 w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</button-base>
|
||||
<button-base onClick={() => this.handleSignup()} titleBtn="Sign up">
|
||||
<svg
|
||||
style={{ display: 'none' }}
|
||||
slot="icon-start"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="flex-shrink-0 w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
|
||||
/>
|
||||
</svg>
|
||||
</button-base>
|
||||
</div>
|
||||
) : (
|
||||
<button-base onClick={() => this.handleLogout()} titleBtn="Log out" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
// https://gist.github.com/Haprog/848fc451c25da00b540e6d34c301e96a
|
||||
function deepQuerySelectorAll(selector: string, root?: Element) {
|
||||
root = root || document.body;
|
||||
const results = Array.from(root.querySelectorAll(selector));
|
||||
const pushNestedResults = function (root) {
|
||||
deepQuerySelectorAll(selector, root).forEach(elem => {
|
||||
if (!results.includes(elem)) {
|
||||
results.push(elem);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (root.shadowRoot) {
|
||||
pushNestedResults(root.shadowRoot);
|
||||
}
|
||||
for (const elem of Array.from(root.querySelectorAll('*'))) {
|
||||
if (elem.shadowRoot) {
|
||||
pushNestedResults(elem.shadowRoot);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const dialogs = deepQuerySelectorAll('dialog');
|
||||
const needDialog = !!dialogs.find(d => (d as HTMLDialogElement).open && !d.classList.contains('nl-banner-dialog'));
|
||||
|
||||
return (
|
||||
<div class={`theme-${this.theme} ${!this.isOpen && this.hiddenMode ? 'hidden' : ''}`}>
|
||||
<div class={this.darkMode && 'dark'} dir="ltr">
|
||||
{this.isOpenConfirm && needDialog ? (
|
||||
<nl-dialog>
|
||||
<div
|
||||
class={`nl-banner ${this.isOpen ? 'w-52 h-auto right-2 rounded-r-lg isOpen ' : 'rounded-r-none hover:rounded-r-lg cursor-pointer'} z-50 w-12 h-12 fixed top-52 right-0 inline-block gap-x-2 text-sm font-medium rounded-lg hover:right-2 transition-all duration-300 ease-in-out`}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</nl-dialog>
|
||||
) : (
|
||||
<div
|
||||
class={`nl-banner ${this.isOpen ? 'w-52 h-auto right-2 rounded-r-lg isOpen' : 'rounded-r-none hover:rounded-r-lg cursor-pointer'} z-50 w-12 h-12 fixed top-52 right-0 inline-block gap-x-2 text-sm font-medium rounded-lg hover:right-2 transition-all duration-300 ease-in-out`}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
# nl-banner
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------- | -------------- | ----------- | -------------------------------------------------------------------- | ----------- |
|
||||
| `accounts` | -- | | `Info[]` | `[]` |
|
||||
| `darkMode` | `dark-mode` | | `boolean` | `false` |
|
||||
| `hiddenMode` | `hidden-mode` | | `boolean` | `false` |
|
||||
| `isLoading` | `is-loading` | | `boolean` | `false` |
|
||||
| `isOpen` | `is-open` | | `boolean` | `false` |
|
||||
| `notify` | -- | | `BannerNotify` | `null` |
|
||||
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
|
||||
| `titleBanner` | `title-banner` | | `string` | `''` |
|
||||
| `userInfo` | -- | | `Info` | `null` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| --------------------------------- | ----------- | --------------------- |
|
||||
| `handleConfirmLogout` | | `CustomEvent<string>` |
|
||||
| `handleImportModal` | | `CustomEvent<string>` |
|
||||
| `handleLoginBanner` | | `CustomEvent<string>` |
|
||||
| `handleLogoutBanner` | | `CustomEvent<string>` |
|
||||
| `handleNotifyConfirmBanner` | | `CustomEvent<string>` |
|
||||
| `handleNotifyConfirmBannerIframe` | | `CustomEvent<string>` |
|
||||
| `handleOpenWelcomeModal` | | `CustomEvent<string>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Depends on
|
||||
|
||||
- [nl-login-status](../nl-login-status)
|
||||
- [button-base](../button-base)
|
||||
- [nl-change-account](../nl-change-account)
|
||||
- [nl-dialog](../nl-dialog)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-banner --> nl-login-status
|
||||
nl-banner --> button-base
|
||||
nl-banner --> nl-change-account
|
||||
nl-banner --> nl-dialog
|
||||
nl-change-account --> nl-login-status
|
||||
style nl-banner fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Component, h, Prop } from '@stencil/core';
|
||||
import { NlTheme } from '@/types';
|
||||
import { IButton } from '@/types/button';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-button',
|
||||
shadow: true,
|
||||
})
|
||||
export class NlButton implements IButton {
|
||||
@Prop() theme: NlTheme = 'default';
|
||||
@Prop() darkMode: boolean = false;
|
||||
@Prop() titleBtn = 'Open modal';
|
||||
@Prop() disabled = false;
|
||||
|
||||
render() {
|
||||
return <button-base theme={this.theme} darkMode={this.darkMode} titleBtn={this.titleBtn} disabled={this.disabled} />;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
# nl-button
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------- | ----------- | ----------- | -------------------------------------------------------------------- | -------------- |
|
||||
| `darkMode` | `dark-mode` | | `boolean` | `false` |
|
||||
| `disabled` | `disabled` | | `boolean` | `false` |
|
||||
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
|
||||
| `titleBtn` | `title-btn` | | `string` | `'Open modal'` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Depends on
|
||||
|
||||
- [button-base](../button-base)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-button --> button-base
|
||||
style nl-button fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { Component, h, Listen, Prop, State, Watch, Element, Event, EventEmitter } from '@stencil/core';
|
||||
import { Info } from '@/types';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-change-account',
|
||||
styleUrl: 'nl-change-account.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NLChangeAccount {
|
||||
@State() isOpen: boolean = false;
|
||||
@State() options: Info[] = [];
|
||||
@Prop() accounts: Info[] = [];
|
||||
@Prop() currentAccount: Info = null;
|
||||
|
||||
@Element() element: HTMLElement;
|
||||
@Event() handleOpenWelcomeModal: EventEmitter<string>;
|
||||
@Event() handleSwitchAccount: EventEmitter<Info>;
|
||||
|
||||
buttonRef: HTMLButtonElement;
|
||||
ulRef: HTMLUListElement;
|
||||
wrapperRef: HTMLDivElement;
|
||||
|
||||
@Listen('click', { target: 'window' })
|
||||
handleWindowClick() {
|
||||
if (this.wrapperRef.querySelector('.listClass')) {
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
toggleDropdown() {
|
||||
this.isOpen = !this.isOpen;
|
||||
this.calculateDropdownPosition();
|
||||
}
|
||||
|
||||
@State() mode: boolean = false;
|
||||
@Prop() darkMode: boolean = false;
|
||||
@State() themeState: 'default' | 'ocean' | 'lemonade' | 'purple' = 'default';
|
||||
@Prop() theme: 'default' | 'ocean' | 'lemonade' | 'purple' = 'default';
|
||||
@Watch('theme')
|
||||
watchPropHandler(newValue: 'default' | 'ocean' | 'lemonade' | 'purple') {
|
||||
this.themeState = newValue;
|
||||
}
|
||||
|
||||
@Watch('darkMode')
|
||||
watchModeHandler(newValue: boolean) {
|
||||
this.mode = newValue;
|
||||
}
|
||||
|
||||
@Watch('accounts')
|
||||
watchAccountsHandler(newValue: Info[]) {
|
||||
this.options = newValue;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.themeState = this.theme;
|
||||
this.mode = this.darkMode;
|
||||
}
|
||||
|
||||
calculateDropdownPosition() {
|
||||
if (this.isOpen && this.buttonRef) {
|
||||
const buttonRect = this.buttonRef.getBoundingClientRect();
|
||||
this.ulRef.style.top = `${buttonRect.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(el: Info) {
|
||||
this.handleSwitchAccount.emit(el);
|
||||
}
|
||||
|
||||
handleOpenModal() {
|
||||
this.handleOpenWelcomeModal.emit();
|
||||
}
|
||||
|
||||
render() {
|
||||
const listClass = `${this.isOpen ? 'listClass flex flex-col gap-2' : 'hidden'} w-full nl-select-list absolute z-10 left-0 shadow-md rounded-lg p-2 mt-1 after:h-4 after:absolute after:-bottom-4 after:start-0 after:w-full before:h-4 before:absolute before:-top-4 before:start-0 before:w-full`;
|
||||
const arrowClass = `${this.isOpen ? 'rotate-180' : 'rotate-0'} duration-300 flex-shrink-0 w-4 h-4 text-gray-500`;
|
||||
const filteredOptions =
|
||||
this.options && this.currentAccount ? this.options.filter(el => el.pubkey !== this.currentAccount.pubkey || el.authMethod !== this.currentAccount.authMethod) : [];
|
||||
|
||||
return (
|
||||
<div class={`theme-${this.themeState}`}>
|
||||
<div class="relative" ref={el => (this.wrapperRef = el)}>
|
||||
<button
|
||||
ref={el => (this.buttonRef = el)}
|
||||
onClick={() => this.toggleDropdown()}
|
||||
type="button"
|
||||
class="nl-select peer py-3 px-4 flex items-center w-full justify-between border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
|
||||
>
|
||||
<span class="text-gray-500">Switch profile</span>
|
||||
<svg
|
||||
class={arrowClass}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<ul ref={el => (this.ulRef = el)} class={listClass}>
|
||||
{this.options &&
|
||||
filteredOptions.map(el => {
|
||||
const isShowImg = Boolean(el?.picture);
|
||||
const userName = el.name || el.nip05 || el.pubkey;
|
||||
const isShowUserName = Boolean(userName);
|
||||
|
||||
return (
|
||||
<li onClick={() => this.handleChange(el)} class="nl-select-option flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm">
|
||||
<div class="uppercase font-bold w-full max-w-6 h-6 rounded-full border border-gray-400 flex justify-center items-center">
|
||||
{isShowImg ? (
|
||||
<img class="w-full rounded-full" src={el.picture} alt="Logo" />
|
||||
) : isShowUserName ? (
|
||||
userName[0]
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{/*<div class="truncate overflow-hidden w-full">{userName}</div>*/}
|
||||
|
||||
<div class="overflow-hidden flex flex-col w-full">
|
||||
<div class="truncate overflow-hidden">{userName}</div>
|
||||
<nl-login-status info={el} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<li class="first:pt-0 pt-2 border-t-[1px] first:border-none border-gray-300">
|
||||
<div onClick={() => this.handleOpenModal()} class="nl-select-option flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm">
|
||||
<div class="uppercase font-bold w-6 h-6 rounded-full border border-gray-400 flex justify-center items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</div>
|
||||
Add profile
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
# nl-change-account
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------------- | ----------- | ----------- | ------------------------------------------------ | ----------- |
|
||||
| `accounts` | -- | | `Info[]` | `[]` |
|
||||
| `currentAccount` | -- | | `Info` | `null` |
|
||||
| `darkMode` | `dark-mode` | | `boolean` | `false` |
|
||||
| `theme` | `theme` | | `"default" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| ------------------------ | ----------- | --------------------- |
|
||||
| `handleOpenWelcomeModal` | | `CustomEvent<string>` |
|
||||
| `handleSwitchAccount` | | `CustomEvent<Info>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-banner](../nl-banner)
|
||||
|
||||
### Depends on
|
||||
|
||||
- [nl-login-status](../nl-login-status)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-change-account --> nl-login-status
|
||||
nl-banner --> nl-change-account
|
||||
style nl-change-account fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
|
||||
import { CURRENT_MODULE, METHOD_MODULE } from '@/types';
|
||||
import { state } from '@/store';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-confirm-logout',
|
||||
styleUrl: 'nl-confirm-logout.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlConfirmLogout {
|
||||
@Prop() titleModal = "Delete keys?";
|
||||
@Prop() description = "Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible.";
|
||||
@Event() handleLogoutBanner: EventEmitter<string>;
|
||||
@Event() handleBackUpModal: EventEmitter<string>;
|
||||
@Event() nlCloseModal: EventEmitter;
|
||||
|
||||
handleLogout() {
|
||||
this.handleLogoutBanner.emit(METHOD_MODULE.LOGOUT);
|
||||
this.nlCloseModal.emit();
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
this.nlCloseModal.emit();
|
||||
}
|
||||
|
||||
handleBackUp() {
|
||||
state.path = [CURRENT_MODULE.IMPORT_FLOW];
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<h1 class="nl-title font-bold text-center text-4xl">{this.titleModal}</h1>
|
||||
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">{this.description}</p>
|
||||
|
||||
<div class="mt-3 ml-auto mr-auto w-60 flex flex-col gap-2">
|
||||
{/* <button-base onClick={() => this.handleCancel()} titleBtn="Cancel" /> */}
|
||||
<button-base onClick={() => this.handleBackUp()} titleBtn="Backup keys" theme="lemonade" />
|
||||
<button-base onClick={() => this.handleLogout()} theme="crab" titleBtn="Logout and delete keys" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
# nl-confirm-logout
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------- | ------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `description` | `description` | | `string` | `"Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible."` |
|
||||
| `titleModal` | `title-modal` | | `string` | `"Delete keys?"` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| -------------------- | ----------- | --------------------- |
|
||||
| `handleBackUpModal` | | `CustomEvent<string>` |
|
||||
| `handleLogoutBanner` | | `CustomEvent<string>` |
|
||||
| `nlCloseModal` | | `CustomEvent<any>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Depends on
|
||||
|
||||
- [button-base](../button-base)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-confirm-logout --> button-base
|
||||
nl-auth --> nl-confirm-logout
|
||||
style nl-confirm-logout fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { Component, Event, EventEmitter, Fragment, h, Prop, State } from '@stencil/core';
|
||||
import { AuthMethod, ConnectionString, CURRENT_MODULE } from '@/types';
|
||||
import { state } from '@/store';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-connect',
|
||||
styleUrl: 'nl-connect.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlConnect {
|
||||
@Prop() titleWelcome = 'Connect to key store';
|
||||
@Prop() authMethods: AuthMethod[] = [];
|
||||
@Prop() hasOTP: boolean = false;
|
||||
@Prop() connectionStringServices: ConnectionString[] = [];
|
||||
|
||||
@State() isOpenAdvancedLogin: boolean = false;
|
||||
@Event() nlNostrConnect: EventEmitter<ConnectionString>;
|
||||
|
||||
handleChangeScreen(screen) {
|
||||
state.path = [...state.path, screen];
|
||||
}
|
||||
|
||||
handleOpenAdvanced() {
|
||||
this.isOpenAdvancedLogin = !this.isOpenAdvancedLogin;
|
||||
}
|
||||
|
||||
allowAuthMethod(m: AuthMethod) {
|
||||
return !this.authMethods.length || this.authMethods.includes(m);
|
||||
}
|
||||
|
||||
componentWillLoad() {}
|
||||
|
||||
handleOpenLink(e: Event, cs: ConnectionString) {
|
||||
e.preventDefault();
|
||||
this.nlNostrConnect.emit(cs);
|
||||
}
|
||||
|
||||
handleConnectionString() {
|
||||
this.handleChangeScreen(CURRENT_MODULE.CONNECTION_STRING)
|
||||
}
|
||||
|
||||
render() {
|
||||
const arrowClass = `${this.isOpenAdvancedLogin ? 'rotate-180' : 'rotate-0'} duration-300 flex-shrink-0 w-4 h-4 text-blue-500`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<h1 class="nl-title font-bold text-center text-3xl">{this.titleWelcome}</h1>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
{Boolean(this.connectionStringServices.length) && (
|
||||
<div class="max-w-96 mx-auto pt-5">
|
||||
<p class="nl-description font-medium text-sm pb-1.5">Select key store:</p>
|
||||
<ul class="p-2 rounded-lg border border-gray-200 flex flex-col w-full gap-0.5">
|
||||
{this.connectionStringServices.map(el => {
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={el.link}
|
||||
target="_blank"
|
||||
onClick={e => this.handleOpenLink(e, el)}
|
||||
class="flex items-center gap-x-3.5 w-full hover:bg-gray-300 flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm justify-between"
|
||||
>
|
||||
<div class="w-full max-w-7 h-7 flex relative">
|
||||
<div class="uppercase font-bold w-full h-full rounded-full border border-gray-400 flex justify-center items-center">
|
||||
{Boolean(el.img) ? (
|
||||
<img class="w-full rounded-full" src={el.img} alt={el.name} />
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#9ca3af" class="w-4 h-4 block">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden flex flex-col w-full">
|
||||
<div class="nl-title truncate overflow-hidden">{el.name}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="ps-4 pe-4 overflow-y-auto">
|
||||
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
|
||||
</div>
|
||||
<div class="max-w-52 mx-auto pb-5">
|
||||
{(this.allowAuthMethod('connect') || this.allowAuthMethod('readOnly')) && (
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
onClick={() => this.handleOpenAdvanced()}
|
||||
class="text-blue-500 mt-3 decoration-dashed cursor-pointer inline-flex gap-2 items-center pb-1 border-dashed border-b-[1px] border-blue-500 text-sm font-light"
|
||||
>
|
||||
Advanced
|
||||
<svg
|
||||
class={arrowClass}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
class={`${this.isOpenAdvancedLogin ? 'max-h-[500px] mt-3 duration-300' : 'max-h-0 mt-0 duration-[0.25s]'} transition-max-height ease-in flex gap-3 flex-col overflow-hidden`}
|
||||
>
|
||||
{/* {this.hasExtension && !this.allowAuthMethod('extension') && this.renderSignInWithExtension()} */}
|
||||
{this.allowAuthMethod('connect') && (
|
||||
<button-base titleBtn="User name" onClick={() => this.handleChangeScreen(CURRENT_MODULE.LOGIN)}>
|
||||
<svg
|
||||
style={{ display: 'none' }}
|
||||
slot="icon-start"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
// class="flex-shrink-0 w-4 h-4 text-gray-500"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
|
||||
/>
|
||||
</svg>
|
||||
</button-base>
|
||||
)}
|
||||
|
||||
{this.allowAuthMethod('connect') && (
|
||||
<button-base titleBtn="Connection string" onClick={() => this.handleConnectionString()}>
|
||||
<svg style={{ display: 'none' }} slot="icon-start" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 3.75 9.375v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 13.5 9.375v-4.5Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 6.75h.75v.75h-.75v-.75ZM6.75 16.5h.75v.75h-.75v-.75ZM16.5 6.75h.75v.75h-.75v-.75ZM13.5 13.5h.75v.75h-.75v-.75ZM13.5 19.5h.75v.75h-.75v-.75ZM19.5 13.5h.75v.75h-.75v-.75ZM19.5 19.5h.75v.75h-.75v-.75ZM16.5 16.5h.75v.75h-.75v-.75Z"
|
||||
/>
|
||||
</svg>
|
||||
</button-base>
|
||||
)}
|
||||
|
||||
{this.allowAuthMethod('connect') && (
|
||||
<button-base onClick={() => this.handleChangeScreen(CURRENT_MODULE.LOGIN_BUNKER_URL)} titleBtn="Bunker URL">
|
||||
<svg style={{ display: 'none' }} slot="icon-start" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
</button-base>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
# nl-connect
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| -------------------------- | --------------- | ----------- | -------------------- | ------------------------ |
|
||||
| `authMethods` | -- | | `AuthMethod[]` | `[]` |
|
||||
| `connectionStringServices` | -- | | `ConnectionString[]` | `[]` |
|
||||
| `hasOTP` | `has-o-t-p` | | `boolean` | `false` |
|
||||
| `titleWelcome` | `title-welcome` | | `string` | `'Connect to key store'` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| ---------------- | ----------- | ------------------------------- |
|
||||
| `nlNostrConnect` | | `CustomEvent<ConnectionString>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Depends on
|
||||
|
||||
- [button-base](../button-base)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-connect --> button-base
|
||||
nl-auth --> nl-connect
|
||||
style nl-connect fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Component, h } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-dialog',
|
||||
styleUrl: 'nl-dialog.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class NlDialog {
|
||||
private dialogElement?: HTMLDialogElement;
|
||||
|
||||
componentDidLoad() {
|
||||
this.dialogElement?.showModal();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.dialogElement?.close();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<dialog ref={el => (this.dialogElement = el as HTMLDialogElement)} class={'m-auto nl-banner-dialog'} style={{ border: '0', backgroundColor: 'transparent' }}>
|
||||
<slot></slot>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# nl-dialog
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-banner](../nl-banner)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-banner --> nl-dialog
|
||||
style nl-dialog fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-iframe',
|
||||
styleUrl: 'nl-iframe.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlConfirmLogout {
|
||||
@Prop() titleModal = 'Confirm';
|
||||
@Prop() description = 'Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible.';
|
||||
@Prop() iframeUrl = '';
|
||||
@Event() nlCloseModal: EventEmitter;
|
||||
|
||||
handleCancel() {
|
||||
this.nlCloseModal.emit();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
{/* <h1 class="nl-title font-bold text-center text-4xl">{this.titleModal}</h1>
|
||||
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">{this.description}</p> */}
|
||||
|
||||
<div class="mt-3 flex flex-col gap-2">
|
||||
{this.iframeUrl && (
|
||||
<iframe
|
||||
src={this.iframeUrl}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '600px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
></iframe>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
# nl-iframe
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------- | ------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `description` | `description` | | `string` | `'Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible.'` |
|
||||
| `iframeUrl` | `iframe-url` | | `string` | `''` |
|
||||
| `titleModal` | `title-modal` | | `string` | `'Confirm'` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| -------------- | ----------- | ------------------ |
|
||||
| `nlCloseModal` | | `CustomEvent<any>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-auth --> nl-iframe
|
||||
style nl-iframe fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { Component, h, Fragment, State, Prop, Event, EventEmitter } from '@stencil/core';
|
||||
import { state } from '@/store';
|
||||
import { ConnectionString } from '@/types';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-import-flow',
|
||||
styleUrl: 'nl-import-flow.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlImportFlow {
|
||||
@Prop({ mutable: true }) titleInfo = 'Back up your keys';
|
||||
@Prop() titleImport = 'Choose a service';
|
||||
@Prop() services: ConnectionString[] = [];
|
||||
|
||||
@State() isContinued = false;
|
||||
@State() isKeyBackup = false;
|
||||
|
||||
@State() isCopy = false;
|
||||
|
||||
@Event() nlImportAccount: EventEmitter<ConnectionString>;
|
||||
@Event() nlExportKeys: EventEmitter<void>;
|
||||
|
||||
handleDomainSelect(event: CustomEvent<string>) {
|
||||
const s = this.services.find(s => s.domain === event.detail);
|
||||
state.nlImport = s;
|
||||
}
|
||||
|
||||
handleCreateAccount(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
this.nlImportAccount.emit(state.nlImport);
|
||||
}
|
||||
|
||||
handleContinue() {
|
||||
this.isContinued = true;
|
||||
}
|
||||
|
||||
handleContinueKeyBackup() {
|
||||
this.isKeyBackup = true;
|
||||
}
|
||||
|
||||
async copyToClipboard() {
|
||||
this.nlExportKeys.emit();
|
||||
this.isCopy = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.isCopy = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isContinued && !this.isKeyBackup) {
|
||||
return (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<h1 class="nl-title font-bold text-center text-2xl">{this.titleInfo}</h1>
|
||||
<p class="nl-description font-light text-sm pt-2 pb-2 max-w-96 mx-auto">
|
||||
Nostr profiles are controlled by cryptographic keys.
|
||||
<br />
|
||||
<br />
|
||||
Your keys are currently only stored in this browser tab, and may be lost if you close it.
|
||||
<br />
|
||||
<br />
|
||||
You should backup your keys.
|
||||
<br />
|
||||
<br />
|
||||
We recommend to import your keys into a key store service, to protect them and to use with other apps.
|
||||
{/* <br />
|
||||
<br />
|
||||
You can also export your keys and save them in your password manager. */}
|
||||
</p>
|
||||
<div class="ml-auto mr-auto mb-2 w-72">
|
||||
<button-base onClick={() => this.handleContinue()} titleBtn="Import to key store" />
|
||||
</div>
|
||||
<div class="ml-auto mr-auto w-72">
|
||||
<button-base onClick={() => this.handleContinueKeyBackup()} titleBtn="Export keys" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isKeyBackup) {
|
||||
return (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<h1 class="nl-title font-bold text-center text-2xl">Key export</h1>
|
||||
<p class="nl-description font-light text-sm pt-2 pb-2 max-w-96 mx-auto">
|
||||
Copy your keys and store them in a safe place, like a password manager.
|
||||
<br />
|
||||
<br />
|
||||
You can sign into other Nostr apps by pasting your keys into them.
|
||||
<br />
|
||||
<br />
|
||||
Your keys must be kept secret, never share them with anyone.
|
||||
</p>
|
||||
<div class="max-w-72 mx-auto">
|
||||
<div class="ml-auto mr-auto mb-2 w-72">
|
||||
<button-base onClick={() => this.copyToClipboard()} titleBtn={this.isCopy ? 'Copied!' : 'Copy to clipboard'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const options = this.services.filter(s => s.canImport).map(s => ({ name: s.domain!, value: s.domain! }));
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<h1 class="nl-title font-bold text-center text-2xl">{this.titleImport}</h1>
|
||||
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">
|
||||
Your Nostr keys will be imported into the service you choose. You will manage your keys on their website.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-72 mx-auto mb-5">
|
||||
<div class="mb-0.5">
|
||||
<nl-select onSelectDomain={e => this.handleDomainSelect(e)} selected={0} options={options}></nl-select>
|
||||
</div>
|
||||
<p class="nl-title font-light text-sm mb-2">Default provider is a fine choice to start with.</p>
|
||||
|
||||
<div class="ps-4 pe-4 overflow-y-auto">
|
||||
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
|
||||
</div>
|
||||
|
||||
<button-base disabled={state.isLoading} onClick={e => this.handleCreateAccount(e)} titleBtn="Start importing">
|
||||
{state.isLoading ? (
|
||||
<span
|
||||
slot="icon-start"
|
||||
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
|
||||
role="status"
|
||||
aria-label="loading"
|
||||
></span>
|
||||
) : (
|
||||
<svg slot="icon-start" style={{ display: 'none' }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button-base>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# nl-import-flow
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------- | -------------- | ----------- | -------------------- | --------------------- |
|
||||
| `services` | -- | | `ConnectionString[]` | `[]` |
|
||||
| `titleImport` | `title-import` | | `string` | `'Choose a service'` |
|
||||
| `titleInfo` | `title-info` | | `string` | `'Back up your keys'` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| ----------------- | ----------- | ------------------------------- |
|
||||
| `nlExportKeys` | | `CustomEvent<void>` |
|
||||
| `nlImportAccount` | | `CustomEvent<ConnectionString>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Depends on
|
||||
|
||||
- [button-base](../button-base)
|
||||
- [nl-select](../nl-select)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-import-flow --> button-base
|
||||
nl-import-flow --> nl-select
|
||||
nl-auth --> nl-import-flow
|
||||
style nl-import-flow fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Component, h } from '@stencil/core';
|
||||
import { state } from '@/store';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-info-extension',
|
||||
styleUrl: 'nl-info-extension.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlInfoExtension {
|
||||
render() {
|
||||
return (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
{state.isLoadingExtension ? (
|
||||
<div>
|
||||
<h1 class="nl-title font-bold text-center text-4xl">Signing in...</h1>
|
||||
<div class="mt-10 mb-10 ml-auto mr-auto w-20">
|
||||
<span
|
||||
slot="icon-start"
|
||||
class="animate-spin-loading ml-auto mr-auto inline-block w-20 h-20 border-[4px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
|
||||
role="status"
|
||||
aria-label="loading"
|
||||
></span>
|
||||
</div>
|
||||
<div class="ps-4 pe-4 overflow-y-auto">
|
||||
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h1 class="nl-title font-bold text-center text-4xl">Install browser extension!</h1>
|
||||
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">
|
||||
Try{' '}
|
||||
<a href="https://getalby.com" target="_blank">
|
||||
Alby
|
||||
</a>
|
||||
,{' '}
|
||||
<a href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp" target="_blank">
|
||||
nos2x
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href="https://apps.apple.com/us/app/nostore/id1666553677" target="_blank">
|
||||
Nostore
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
# nl-info-extension
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-auth --> nl-info-extension
|
||||
style nl-info-extension fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Component, h, Prop } from '@stencil/core';
|
||||
import { NlTheme } from '@/types';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-info',
|
||||
styleUrl: 'nl-info.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlInfo {
|
||||
@Prop() theme: NlTheme = 'default';
|
||||
@Prop() darkMode: boolean = false;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<svg class="w-12 h-12 mx-auto mb-2" width="225" height="224" viewBox="0 0 225 224" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="224.047" height="224" rx="64" fill={this.theme === 'laan' ? (this.darkMode ? 'white' : 'black') : '#6951FA'} />
|
||||
<path
|
||||
d="M162.441 135.941V88.0593C170.359 85.1674 176 77.5348 176 68.6696C176 57.2919 166.708 48 155.33 48C143.953 48 134.661 57.2444 134.661 68.6696C134.661 77.5822 140.302 85.1674 148.219 88.0593V135.941C147.698 136.13 147.176 136.367 146.655 136.604L87.3956 77.3452C88.6282 74.6904 89.2919 71.7511 89.2919 68.6696C89.2919 57.2444 80.0474 48 68.6696 48C57.2919 48 48 57.2444 48 68.6696C48 77.5822 53.6415 85.1674 61.5585 88.0593V135.941C53.6415 138.833 48 146.465 48 155.33C48 166.708 57.2444 176 68.6696 176C80.0948 176 89.3393 166.708 89.3393 155.33C89.3393 146.418 83.6978 138.833 75.7807 135.941V88.0593C76.3022 87.8696 76.8237 87.6326 77.3452 87.3956L136.604 146.655C135.372 149.31 134.708 152.249 134.708 155.33C134.708 166.708 143.953 176 155.378 176C166.803 176 176.047 166.708 176.047 155.33C176.047 146.418 170.406 138.833 162.489 135.941H162.441Z"
|
||||
fill={this.theme === 'laan' ? (this.darkMode ? 'black' : 'white') : 'white'}
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="nl-title font-bold text-center text-4xl">
|
||||
Nostr <span class="font-light">Login</span>
|
||||
</h1>
|
||||
<p class="text-green-800 dark:text-green-200 font-light text-center text-lg pt-2 max-w-96 mx-auto">Version: 1.7.11</p>
|
||||
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">
|
||||
Learn more about Nostr{' '}
|
||||
<a target="_blank" href="https://nostr.how">
|
||||
here
|
||||
</a>
|
||||
.<br />
|
||||
This is an{' '}
|
||||
<a target="_blank" href="https://github.com/nostrband/nostr-login">
|
||||
open-source
|
||||
</a>{' '}
|
||||
tool by{' '}
|
||||
<a target="_blank" href="https://nostr.band">
|
||||
Nostr.Band
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
# nl-info
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------- | ----------- | ----------- | -------------------------------------------------------------------- | ----------- |
|
||||
| `darkMode` | `dark-mode` | | `boolean` | `false` |
|
||||
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-auth --> nl-info
|
||||
style nl-info fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
|
||||
import { state } from '@/store';
|
||||
import { CURRENT_MODULE } from '@/types';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-loading',
|
||||
styleUrl: 'nl-loading.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlLoading {
|
||||
@Event() stopFetchHandler: EventEmitter<boolean>;
|
||||
@Event() handleContinue: EventEmitter<boolean>;
|
||||
@Prop() path: string;
|
||||
|
||||
handleStop(e) {
|
||||
e.preventDefault();
|
||||
this.stopFetchHandler.emit();
|
||||
}
|
||||
|
||||
handleContinueClick(e) {
|
||||
e.preventDefault();
|
||||
// reset();
|
||||
this.handleContinue.emit();
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = 'Connecting...';
|
||||
let text = 'Establishing connection to your key storage.';
|
||||
if (state.njumpIframe) {
|
||||
title = '';
|
||||
text = '';
|
||||
} else if (this.path === CURRENT_MODULE.LOCAL_SIGNUP) {
|
||||
title = 'Creating...';
|
||||
text = 'Publishing your profile on Nostr.';
|
||||
} else if (state.authUrl) {
|
||||
if (state.isLoading) {
|
||||
title = 'Confirming...';
|
||||
text = 'Please confirm the connection in your key storage app.';
|
||||
} else {
|
||||
title = 'Almost ready!';
|
||||
text = 'Continue to confirm the connection to your key storage.';
|
||||
}
|
||||
}
|
||||
|
||||
const showButton = this.path !== CURRENT_MODULE.LOCAL_SIGNUP;
|
||||
const showIframe = !state.isLoading && state.iframeUrl && state.authUrl;
|
||||
const iframeUrl = state.iframeUrl ? `${state.iframeUrl}?connect=${encodeURIComponent(state.authUrl)}` : '';
|
||||
|
||||
return (
|
||||
<div class="p-4 overflow-y-auto">
|
||||
{title && (<h1 class="nl-title font-bold text-center text-4xl">{title}</h1>)}
|
||||
{text && (<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">{text}</p>)}
|
||||
{!state.njumpIframe && !state.authUrl && state.isLoading && (
|
||||
<div class="mt-10 mb-10 ml-auto mr-auto w-20">
|
||||
<span
|
||||
slot="icon-start"
|
||||
class="animate-spin-loading ml-auto mr-auto inline-block w-20 h-20 border-[4px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
|
||||
role="status"
|
||||
aria-label="loading"
|
||||
></span>
|
||||
</div>
|
||||
)}
|
||||
<div class="ps-4 pe-4 overflow-y-auto">
|
||||
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
|
||||
</div>
|
||||
{iframeUrl && (
|
||||
<div class="mt-3 ml-auto mr-auto w-72 flex justify-center">
|
||||
<iframe src={iframeUrl} width="180px" height="80px" style={{ display: showIframe ? 'block' : 'none', border: '0' }}></iframe>
|
||||
</div>
|
||||
)}
|
||||
{state.njumpIframe && (
|
||||
<div class="mt-3 ml-auto mr-auto flex justify-center">
|
||||
<iframe srcdoc={state.njumpIframe} width="600px" style={{ border: '0', height: "80vh", borderRadius: "8px" }}></iframe>
|
||||
</div>
|
||||
)}
|
||||
{!showIframe && showButton && (
|
||||
<div class="mt-3 ml-auto mr-auto w-72">
|
||||
<button-base
|
||||
onClick={e => {
|
||||
if (state.authUrl && !state.isLoading) {
|
||||
this.handleContinueClick(e);
|
||||
} else {
|
||||
this.handleStop(e);
|
||||
}
|
||||
}}
|
||||
titleBtn={!state.isLoading ? 'Continue' : 'Cancel'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
# nl-loading
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| -------- | --------- | ----------- | -------- | ----------- |
|
||||
| `path` | `path` | | `string` | `undefined` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| ------------------ | ----------- | ---------------------- |
|
||||
| `handleContinue` | | `CustomEvent<boolean>` |
|
||||
| `stopFetchHandler` | | `CustomEvent<boolean>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Depends on
|
||||
|
||||
- [button-base](../button-base)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-loading --> button-base
|
||||
nl-auth --> nl-loading
|
||||
style nl-loading fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Component, h, Fragment, State, Prop, Event, EventEmitter } from '@stencil/core';
|
||||
import { state } from '@/store';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-local-signup',
|
||||
styleUrl: 'nl-local-signup.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlLocalSignup {
|
||||
@Prop() titleSignup = 'Create Nostr profile';
|
||||
@Prop() description = 'Choose any username, you can always change it later.';
|
||||
@Prop() descriptionNjump = 'Proceed to creating your Nostr profile in a new tab.';
|
||||
@Prop() signupNjump = false;
|
||||
|
||||
@State() isAvailable = false;
|
||||
|
||||
@Event() nlLocalSignup: EventEmitter<string>;
|
||||
@Event() nlSignupNjump: EventEmitter<void>;
|
||||
// @Event() nlCheckSignup: EventEmitter<string>;
|
||||
@Event() fetchHandler: EventEmitter<boolean>;
|
||||
|
||||
handleInputChange(event: Event) {
|
||||
state.nlSignup.signupName = (event.target as HTMLInputElement).value;
|
||||
// this.nlCheckSignup.emit(`${(event.target as HTMLInputElement).value}@${state.nlSignup.domain}`);
|
||||
}
|
||||
|
||||
handleCreateAccount(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.signupNjump) {
|
||||
this.nlSignupNjump.emit();
|
||||
} else {
|
||||
this.nlLocalSignup.emit(`${state.nlSignup.signupName}`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<h1 class="nl-title font-bold text-center text-2xl">{this.titleSignup}</h1>
|
||||
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.signupNjump ? this.descriptionNjump : this.description}</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-72 mx-auto">
|
||||
{!this.signupNjump && (
|
||||
<div class="relative mb-2">
|
||||
<input
|
||||
onInput={e => this.handleInputChange(e)}
|
||||
type="text"
|
||||
class="nl-input peer py-3 px-4 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
|
||||
placeholder="Enter username"
|
||||
value={state.nlSignup.signupName}
|
||||
/>
|
||||
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke={this.isAvailable ? '#00cc00' : 'currentColor'}
|
||||
class="flex-shrink-0 w-4 h-4 text-gray-500"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="ps-4 pe-4 overflow-y-auto">
|
||||
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
|
||||
</div>
|
||||
|
||||
<button-base disabled={state.isLoading} onClick={e => this.handleCreateAccount(e)} titleBtn={this.signupNjump ? 'Get started' : 'Create profile'}>
|
||||
{state.isLoading ? (
|
||||
<span
|
||||
slot="icon-start"
|
||||
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
|
||||
role="status"
|
||||
aria-label="loading"
|
||||
></span>
|
||||
) : (
|
||||
<svg slot="icon-start" style={{ display: 'none' }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button-base>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# nl-local-signup
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------------ | ------------------- | ----------- | --------- | -------------------------------------------------------- |
|
||||
| `description` | `description` | | `string` | `'Choose any username, you can always change it later.'` |
|
||||
| `descriptionNjump` | `description-njump` | | `string` | `'Proceed to creating your Nostr profile in a new tab.'` |
|
||||
| `signupNjump` | `signup-njump` | | `boolean` | `false` |
|
||||
| `titleSignup` | `title-signup` | | `string` | `'Create Nostr profile'` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| --------------- | ----------- | ---------------------- |
|
||||
| `fetchHandler` | | `CustomEvent<boolean>` |
|
||||
| `nlLocalSignup` | | `CustomEvent<string>` |
|
||||
| `nlSignupNjump` | | `CustomEvent<void>` |
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Used by
|
||||
|
||||
- [nl-auth](../nl-auth)
|
||||
|
||||
### Depends on
|
||||
|
||||
- [button-base](../button-base)
|
||||
|
||||
### Graph
|
||||
```mermaid
|
||||
graph TD;
|
||||
nl-local-signup --> button-base
|
||||
nl-auth --> nl-local-signup
|
||||
style nl-local-signup fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Info, RecentType } from '@/types';
|
||||
import { Component, h, Prop } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'nl-login-status',
|
||||
// styleUrl: 'nl-login-status.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class NlLoginStatus {
|
||||
@Prop() info: RecentType | Info | undefined;
|
||||
|
||||
render() {
|
||||
let text = '';
|
||||
let color = '';
|
||||
if (this.info.authMethod === 'extension') {
|
||||
text = 'Extension';
|
||||
color = 'border-yellow-300 text-yellow-500 bg-yellow-100';
|
||||
} else if (this.info.authMethod === 'readOnly') {
|
||||
text = 'Read only';
|
||||
color = 'border-gray-300 text-gray-400 bg-gray-100';
|
||||
} else if (this.info.authMethod === 'connect') {
|
||||
text = 'Connect';
|
||||
color = 'border-teal-300 text-teal-600 bg-teal-100';
|
||||
} else if (this.info.authMethod === 'local') {
|
||||
text = 'Temporary';
|
||||
color = 'border-red-300 text-red-600 bg-red-100';
|
||||
} else if (this.info.authMethod === 'otp') {
|
||||
text = 'Delegated';
|
||||
color = 'border-orange-300 text-orange-600 bg-orange-100';
|
||||
} else {
|
||||
console.log('unknown auth method', this.info);
|
||||
throw new Error('Unknown auth method');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span class={`${color} rounded-xl border w-auto text-[10px] px-1 `}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user