From d0bf851e867aa042f914c657c25f8d4297cdb823 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 12 Dec 2025 13:12:46 -0400 Subject: [PATCH] v0.1.15 - Fix config page display and rebrand admin UI from relay to Blossom terminology --- Makefile | 20 +- api/embedded.html | 58 + api/index.css | 1310 + api/index.html | 424 + api/index.js | 5718 ++ api/nostr-lite.js | 4282 ++ api/nostr.bundle.js | 11534 +++ api/text_graph.js | 463 + build/admin_event.o | Bin 17552 -> 18640 bytes build/admin_interface.o | Bin 0 -> 760064 bytes build/ginxsom-fcgi | Bin 340928 -> 1099152 bytes build/main.o | Bin 109400 -> 111072 bytes config/local-nginx.conf | 58 + ...8e51ccf828a2f5a05227d7a768f33b5a198681a.db | Bin 73728 -> 73728 bytes debug.log | 14476 ++++ docs/WEBSOCKET_IMPLEMENTATION.md | 2285 +- restart-all.sh | 6 + scripts/embed_web_files.sh | 82 + src/admin_commands.h | 4 +- src/admin_event.c | 216 +- src/admin_interface.c | 62 + src/admin_interface_embedded.h | 62954 ++++++++++++++++ src/ginxsom.h | 9 +- src/main.c | 70 +- tests/admin_event_test.sh | 20 +- 25 files changed, 102416 insertions(+), 1635 deletions(-) create mode 100644 api/embedded.html create mode 100644 api/index.css create mode 100644 api/index.html create mode 100644 api/index.js create mode 100644 api/nostr-lite.js create mode 100644 api/nostr.bundle.js create mode 100644 api/text_graph.js create mode 100644 build/admin_interface.o create mode 100755 scripts/embed_web_files.sh create mode 100644 src/admin_interface.c create mode 100644 src/admin_interface_embedded.h diff --git a/Makefile b/Makefile index 3b4d71b..e5bd1ba 100644 --- a/Makefile +++ b/Makefile @@ -8,15 +8,24 @@ BUILDDIR = build TARGET = $(BUILDDIR)/ginxsom-fcgi # Source files -SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c $(SRCDIR)/relay_client.c $(SRCDIR)/admin_commands.c +SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/admin_interface.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c $(SRCDIR)/relay_client.c $(SRCDIR)/admin_commands.c OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o) +# Embedded web interface files +EMBEDDED_HEADER = $(SRCDIR)/admin_interface_embedded.h +EMBED_SCRIPT = scripts/embed_web_files.sh + # Add core_relay_pool.c from nostr_core_lib POOL_SRC = nostr_core_lib/nostr_core/core_relay_pool.c POOL_OBJ = $(BUILDDIR)/core_relay_pool.o # Default target -all: $(TARGET) +all: $(EMBEDDED_HEADER) $(TARGET) + +# Generate embedded web interface files +$(EMBEDDED_HEADER): $(EMBED_SCRIPT) api/*.html api/*.css api/*.js + @echo "Embedding web interface files..." + @$(EMBED_SCRIPT) # Create build directory $(BUILDDIR): @@ -37,6 +46,7 @@ $(TARGET): $(OBJECTS) $(POOL_OBJ) # Clean build files clean: rm -rf $(BUILDDIR) + rm -f $(EMBEDDED_HEADER) # Install (copy to system location) install: $(TARGET) @@ -55,4 +65,8 @@ run: $(TARGET) debug: CFLAGS += -g -DDEBUG debug: $(TARGET) -.PHONY: all clean install uninstall run debug +# Rebuild embedded files +embed: + @$(EMBED_SCRIPT) + +.PHONY: all clean install uninstall run debug embed diff --git a/api/embedded.html b/api/embedded.html new file mode 100644 index 0000000..7e7c47f --- /dev/null +++ b/api/embedded.html @@ -0,0 +1,58 @@ + + + + + + Embedded NOSTR_LOGIN_LITE + + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/api/index.css b/api/index.css new file mode 100644 index 0000000..4c4ae61 --- /dev/null +++ b/api/index.css @@ -0,0 +1,1310 @@ +:root { + /* Core Variables (7) */ + --primary-color: #000000; + --secondary-color: #ffffff; + --accent-color: #ff0000; + --muted-color: #dddddd; + --border-color: var(--muted-color); + --font-family: "Courier New", Courier, monospace; + --border-radius: 5px; + --border-width: 1px; + + /* Floating Tab Variables (8) */ + --tab-bg-logged-out: #ffffff; + --tab-bg-logged-in: #ffffff; + --tab-bg-opacity-logged-out: 0.9; + --tab-bg-opacity-logged-in: 0.2; + --tab-color-logged-out: #000000; + --tab-color-logged-in: #ffffff; + --tab-border-logged-out: #000000; + --tab-border-logged-in: #ff0000; + --tab-border-opacity-logged-out: 1.0; + --tab-border-opacity-logged-in: 0.1; +} + +/* Dark Mode Overrides */ +body.dark-mode { + --primary-color: #ffffff; + --secondary-color: #000000; + --accent-color: #ff0000; + --muted-color: #222222; + --border-color: var(--muted-color); + + + --tab-bg-logged-out: #000000; + --tab-color-logged-out: #ffffff; + --tab-border-logged-out: #ffffff; + --tab-bg-logged-in: #000000; + --tab-color-logged-in: #ffffff; + --tab-border-logged-in: #00ffff; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-family); + background-color: var(--secondary-color); + color: var(--primary-color); + /* line-height: 1.4; */ + padding: 0; + max-width: none; + margin: 0; +} + +/* Header Styles */ +.main-header { + background-color: var(--secondary-color); + + padding: 15px 20px; + z-index: 100; + max-width: 1200px; + margin: 0 auto; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} + +.header-title { + margin: 0; + font-size: 24px; + font-weight: normal; + color: var(--primary-color); + border: none; + padding: 0; + text-align: left; +} + +.relay-info { + text-align: center; + flex: 1; + max-width: 150px; + margin: 0 auto; +} + +.relay-name { + font-size: 14px; + font-weight: bold; + color: var(--primary-color); + margin-bottom: 2px; +} + +.relay-pubkey-container { + border: 1px solid transparent; + border-radius: var(--border-radius); + padding: 4px; + margin-top: 4px; + cursor: pointer; + transition: border-color 0.2s ease; + background-color: var(--secondary-color); + display: inline-block; + width: fit-content; +} + +.relay-pubkey-container:hover { + border-color: var(--border-color); +} + +.relay-pubkey-container.copied { + border-color: var(--accent-color); + animation: flash-accent 0.5s ease-in-out; +} + +.relay-pubkey { + font-size: 8px; + color: var(--primary-color); + font-family: "Courier New", Courier, monospace; + line-height: 1.2; + white-space: pre-line; + text-align: center; +} + +@keyframes flash-accent { + 0% { border-color: var(--accent-color); } + 50% { border-color: var(--accent-color); } + 100% { border-color: transparent; } +} + +.relay-description { + font-size: 10px; + color: var(--primary-color); + margin-bottom: 0; + display: inline-block; + width: fit-content; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.header-title { + margin: 0; + font-size: 24px; + font-weight: bolder; + color: var(--primary-color); + border: none; + padding: 0; + text-align: left; + display: flex; + gap: 2px; +} + +.relay-letter { + position: relative; + display: inline-block; + transition: all 0.05s ease; +} + +.relay-letter.underlined::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background-color: var(--accent-color); +} + +.header-user-name { + display: block; + font-weight: 500; + color: var(--primary-color); + font-size: 10px; + text-align: center; + margin-top: 4px; +} + +.profile-area { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + cursor: pointer; + padding: 8px 12px; + border-radius: var(--border-radius); + transition: background-color 0.2s ease; + /* margin-left: auto; */ +} + +.admin-label { + font-size: 10px; + color: var(--primary-color); + font-weight: normal; + margin-bottom: 4px; + text-align: center; +} + +.profile-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.profile-area:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.profile-info { + display: flex; + align-items: center; + gap: 10px; +} + +.header-user-image { + width: 48px; /* 50% larger than 32px */ + height: 48px; /* 50% larger than 32px */ + border-radius: var(--border-radius); /* Curved corners like other elements */ + object-fit: cover; + border: 2px solid transparent; /* Invisible border */ + background-color: var(--secondary-color); +} + + +.logout-dropdown { + position: absolute; + top: 100%; + right: 0; + background-color: var(--secondary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 120px; + z-index: 200; + margin-top: 4px; +} + +.logout-btn { + width: 100%; + padding: 5px 10px; + background: none; + border: none; + color: var(--primary-color); + text-align: left; + cursor: pointer; + font-size: 10px; + font-family: var(--font-family); + border-radius: var(--border-radius); + transition: background-color 0.2s ease; +} + +.logout-btn:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +/* Login Modal Styles */ +.login-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.login-modal-content { + background-color: var(--secondary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + padding: 30px; + max-width: 400px; + width: 90%; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +h1 { + border-bottom: var(--border-width) solid var(--border-color); + padding-bottom: 10px; + margin-bottom: 30px; + font-weight: bold; + font-size: 24px; + font-family: var(--font-family); + color: var(--primary-color); +} + +h2 { + font-weight: normal; + text-align: center; + font-size: 16px; + font-family: var(--font-family); + color: var(--primary-color); +} + +h3 { + font-weight: normal; + font-size: 12px; + font-family: var(--font-family); + color: var(--primary-color); + padding-bottom: 10px; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: lighter; + font-size: 10px; + font-family: var(--font-family); + color: var(--primary-color); +} + + + + +.section { + background: var(--secondary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + padding: 20px; + margin-bottom: 20px; + margin-left: 5px; + margin-right:5px; +} + +.section-header { + display: flex; + justify-content: center; + align-items: center; + padding-bottom: 15px; +} + + + + + +.input-group { + margin-bottom: 15px; +} + + +input, +textarea, +select { + width: 100%; + padding: 8px; + background: var(--secondary-color); + color: var(--primary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + font-size: 14px; + box-sizing: border-box; + transition: all 0.2s ease; +} + +input:focus, +textarea:focus, +select:focus { + border-color: var(--accent-color); + outline: none; +} + +button { + width: 100%; + padding: 8px; + background: var(--secondary-color); + color: var(--primary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + font-size: 14px; + cursor: pointer; + margin: 5px 0; + font-weight: bold; + transition: all 0.2s ease; +} + +button:hover { + border-color: var(--accent-color); +} + +button:active { + background: var(--accent-color); + color: var(--secondary-color); +} + +button:disabled { + background-color: var(--muted-color); + color: var(--primary-color); + cursor: not-allowed; + border-color: var(--muted-color); +} + +/* Flash animation for refresh button */ +@keyframes flash-red { + 0% { border-color: var(--border-color); } + 50% { border-color: var(--accent-color); } + 100% { border-color: var(--border-color); } +} + +.flash-red { + animation: flash-red 1s ease-in-out; +} + +/* Flash animation for updated statistics values */ +@keyframes flash-value { + 0% { color: var(--primary-color); } + 50% { color: var(--accent-color); } + 100% { color: var(--primary-color); } +} + +.flash-value { + animation: flash-value 1s ease-in-out; +} + +/* Npub links styling */ +.npub-link { + color: var(--primary-color); + text-decoration: none; + font-weight: normal; + transition: color 0.2s ease; +} + +.npub-link:hover { + color: var(--accent-color); +} + +.status { + padding: 10px; + margin: 10px 0; + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-weight: bold; + font-family: var(--font-family); + transition: all 0.2s ease; +} + +.status.connected { + background-color: var(--primary-color); + color: var(--secondary-color); +} + +.status.disconnected { + background-color: var(--secondary-color); + color: var(--primary-color); +} + +.status.authenticated { + background-color: var(--primary-color); + color: var(--secondary-color); +} + +.status.error { + background-color: var(--secondary-color); + color: var(--primary-color); + border-color: var(--accent-color); +} + + +.config-table { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: 10px 0; + overflow: hidden; +} + +.config-table th, +.config-table td { + border: 0.1px solid var(--muted-color); + padding: 4px; + text-align: left; + font-family: var(--font-family); + font-size: 10px; +} + +.config-table tbody tr:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.config-table-container { + overflow-x: auto; + max-width: 100%; +} + +.config-table th { + font-weight: bold; + height: 24px; /* Base height for tbody rows */ + line-height: 24px; /* Center text vertically */ +} + +.config-table td { + height: 16px; /* 50% taller than tbody rows would be */ + line-height: 16px; /* Center text vertically */ +} + +/* Inline config value inputs - remove borders and padding to fit seamlessly in table cells */ +.config-value-input { + border: none; + padding: 2px 4px; + background: transparent; + width: 100%; + min-height: auto; + font-family: inherit; + font-size: inherit; + color: inherit; + border-radius: 0; +} + +/* Relay Events Styles */ +.status-message { + margin-top: 10px; + padding: 8px; + border-radius: var(--border-radius); + font-size: 14px; + font-family: var(--font-family); + text-align: center; +} + +.relay-entry { + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + padding: 10px; + margin-bottom: 10px; + background: var(--secondary-color); +} + +.config-value-input:focus { + border: 1px solid var(--accent-color); + background: var(--secondary-color); + outline: none; +} + +/* Config actions cell - clickable for saving */ +.config-actions-cell { + cursor: pointer; + transition: all 0.2s ease; + text-align: center !important; + font-weight: bold; + vertical-align: middle; + width: 60px; + min-width: 60px; + max-width: 60px; + padding: 8px 4px; +} + +.config-actions-cell:hover { + border: 1px solid var(--accent-color); + background-color: var(--muted-color); +} + +.json-display { + background-color: var(--secondary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + padding: 10px; + font-family: var(--font-family); + font-size: 12px; + white-space: pre-wrap; + max-height: 300px; + overflow-y: auto; + margin: 10px 0; +} + +.log-panel { + height: 200px; + overflow-y: auto; + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + padding: 10px; + font-size: 12px; + background-color: var(--secondary-color); + font-family: var(--font-family); +} + +.log-entry { + margin-bottom: 5px; + border-bottom: 1px solid var(--muted-color); + padding-bottom: 5px; +} + +.log-timestamp { + font-weight: bold; + font-family: var(--font-family); +} + +.inline-buttons { + display: flex; + gap: 10px; + flex-wrap: nowrap; +} + +.inline-buttons button { + flex: 1; +} + +.user-info { + padding: 10px; + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + margin: 10px 0; + background-color: var(--secondary-color); +} + +.user-info-container { + display: flex; + flex-direction: column; + gap: 15px; +} + +.user-details { + order: -1; /* Show user details first when logged in */ +} + +.login-section { + text-align: center; +} + +.logout-section { + display: flex; + justify-content: flex-end; +} + +.login-logout-btn { + width: auto; + min-width: 120px; + padding: 12px 16px; + background: var(--secondary-color); + color: var(--primary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + margin: 0; + flex-shrink: 0; +} + +.login-logout-btn:hover { + border-color: var(--accent-color); +} + +.login-logout-btn:active { + background: var(--accent-color); + color: var(--secondary-color); +} + +.login-logout-btn.logout-state { + background: var(--accent-color); + color: var(--secondary-color); + border-color: var(--accent-color); +} + +.login-logout-btn.logout-state:hover { + background: var(--primary-color); + border-color: var(--border-color); +} + +.user-pubkey { + font-family: var(--font-family); + font-size: 12px; + word-break: break-all; + margin: 5px 0; +} + +/* User profile header with image */ +.user-profile-header { + display: flex; + align-items: flex-start; + gap: 15px; +} + +.user-image-container { + flex-shrink: 0; +} + +.user-profile-image { + width: 60px; + height: 60px; + border-radius: var(--border-radius); + object-fit: cover; + border: 2px solid var(--border-color); + background-color: var(--bg-color); +} + +.user-text-info { + flex: 1; + min-width: 0; /* Allow text to wrap */ +} + +.hidden { + display: none; +} + + + +.countdown-btn { + width: auto; + min-width: 40px; + padding: 8px 12px; + background: var(--secondary-color); + color: var(--primary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + font-size: 10px; + /* font-weight: bold; */ + cursor: pointer; + transition: all 0.2s ease; + margin-left: auto; + position: relative; +} + +.countdown-btn:hover::after { + content: "countdown"; + position: absolute; + top: -30px; + left: 50%; + transform: translateX(-50%); + background: var(--primary-color); + color: var(--secondary-color); + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: normal; + white-space: nowrap; + z-index: 1000; + border: 1px solid var(--border-color); +} + +.auth-rules-controls { + margin-bottom: 15px; +} + +.section-header .status { + margin: 0; + padding: 5px 10px; + min-width: auto; + font-size: 12px; +} + +/* Auth Rule Input Sections Styling */ +.auth-rule-section { + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + padding: 15px; + margin: 15px 0; + background-color: var(--secondary-color); +} + +.auth-rule-section h3 { + margin: 0 0 10px 0; + font-size: 14px; + font-weight: bold; + border-left: 4px solid var(--border-color); + padding-left: 8px; + font-family: var(--font-family); + color: var(--primary-color); +} + +.auth-rule-section p { + margin: 0 0 15px 0; + font-size: 13px; + color: var(--muted-color); + font-family: var(--font-family); +} + +.rule-status { + margin-top: 10px; + padding: 8px; + border: var(--border-width) solid var(--muted-color); + border-radius: var(--border-radius); + font-size: 12px; + min-height: 20px; + background-color: var(--secondary-color); + font-family: var(--font-family); + transition: all 0.2s ease; +} + +.rule-status.success { + border-color: #4CAF50; + background-color: #E8F5E8; + color: #2E7D32; +} + +.rule-status.error { + border-color: var(--accent-color); + background-color: #FFEBEE; + color: #C62828; +} + +.rule-status.warning { + border-color: #FF9800; + background-color: #FFF3E0; + color: #E65100; +} + +.warning-box { + border: var(--border-width) solid #FF9800; + border-radius: var(--border-radius); + background-color: #FFF3E0; + padding: 10px; + margin: 10px 0; + font-size: 13px; + color: #E65100; + font-family: var(--font-family); +} + +.warning-box strong { + color: #D84315; +} + +#login-section { + text-align: center; + padding: 20px; +} + +/* Floating tab styles */ +.floating-tab { + font-family: var(--font-family); + border-radius: var(--border-radius); + border: var(--border-width) solid; + transition: all 0.2s ease; +} + +.floating-tab--logged-out { + background: rgba(255, 255, 255, var(--tab-bg-opacity-logged-out)); + color: var(--tab-color-logged-out); + border-color: rgba(0, 0, 0, var(--tab-border-opacity-logged-out)); +} + +.floating-tab--logged-in { + background: rgba(0, 0, 0, var(--tab-bg-opacity-logged-in)); + color: var(--tab-color-logged-in); + border-color: rgba(255, 0, 0, var(--tab-border-opacity-logged-in)); +} + +.transition { + transition: all 0.2s ease; +} + +/* SQL Query Interface Styles */ +.query-selector { + margin-bottom: 15px; +} + +.query-selector select { + width: 100%; + padding: 8px; + background: var(--secondary-color); + color: var(--primary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + font-size: 14px; + cursor: pointer; +} + +.query-selector select:focus { + border-color: var(--accent-color); + outline: none; +} + +.query-selector optgroup { + font-weight: bold; + color: var(--primary-color); +} + +.query-selector option { + padding: 4px; + background: var(--secondary-color); + color: var(--primary-color); +} + +.query-editor textarea { + width: 100%; + min-height: 120px; + resize: vertical; + font-family: "Courier New", Courier, monospace; + font-size: 12px; + line-height: 1.4; + tab-size: 4; + white-space: pre; +} + +.query-actions { + display: flex; + gap: 10px; + margin-top: 10px; +} + +.query-actions button { + flex: 1; + min-width: 120px; +} + +.primary-button { + background: var(--primary-color); + color: var(--secondary-color); + border-color: var(--primary-color); +} + +.primary-button:hover { + background: var(--secondary-color); + color: var(--primary-color); + border-color: var(--accent-color); +} + +.danger-button { + background: var(--accent-color); + color: var(--secondary-color); + border-color: var(--accent-color); +} + +.danger-button:hover { + background: var(--secondary-color); + color: var(--primary-color); + border-color: var(--accent-color); +} + +.query-info { + padding: 10px; + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + margin: 10px 0; + font-family: var(--font-family); + font-size: 12px; + background-color: var(--secondary-color); +} + +.query-info-success { + border-color: #4CAF50; + background-color: #E8F5E8; + color: #2E7D32; +} + +.query-info-success span { + display: inline-block; + margin-right: 15px; +} + +.request-id { + font-family: "Courier New", Courier, monospace; + font-size: 10px; + opacity: 0.7; +} + +.error-message { + border-color: var(--accent-color); + background-color: #FFEBEE; + color: #C62828; + padding: 10px; + border-radius: var(--border-radius); + margin: 10px 0; + font-family: var(--font-family); + font-size: 12px; +} + +.sql-results-table { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: 10px 0; + overflow: hidden; + font-size: 11px; +} + +.sql-results-table th, +.sql-results-table td { + border: 0.1px solid var(--muted-color); + padding: 6px 8px; + text-align: left; + font-family: var(--font-family); + white-space: nowrap; + min-width: 100px; +} + +.sql-results-table th { + font-weight: bold; + background-color: rgba(0, 0, 0, 0.05); + position: sticky; + top: 0; + z-index: 10; +} + +.sql-results-table tbody tr:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.sql-results-table tbody tr:nth-child(even) { + background-color: rgba(0, 0, 0, 0.02); +} + +.no-results { + text-align: center; + font-style: italic; + color: var(--muted-color); + padding: 20px; + font-family: var(--font-family); +} + +.loading { + text-align: center; + font-style: italic; + color: var(--muted-color); + padding: 20px; + font-family: var(--font-family); +} + +/* Dark mode adjustments for SQL interface */ +body.dark-mode .query-info-success { + border-color: #4CAF50; + background-color: rgba(76, 175, 80, 0.1); + color: #81C784; +} + +body.dark-mode .error-message { + border-color: var(--accent-color); + background-color: rgba(244, 67, 54, 0.1); + color: #EF5350; +} + +body.dark-mode .sql-results-table th { + background-color: rgba(255, 255, 255, 0.05); +} + +body.dark-mode .sql-results-table tbody tr:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +body.dark-mode .sql-results-table tbody tr:nth-child(even) { + background-color: rgba(255, 255, 255, 0.02); +} + + +/* Config Toggle Button Styles */ +.config-toggle-btn { + width: 24px; + height: 24px; + padding: 0; + background: var(--secondary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + font-size: 14px; + cursor: pointer; + margin-left: 10px; + font-weight: bold; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +/* Toggle Button Styles */ +.toggle-btn { + width: auto; + min-width: 120px; + padding: 8px 12px; + background: var(--secondary-color); + color: var(--primary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; + margin-left: auto; +} + +.toggle-btn:hover { + border-color: var(--accent-color); +} + +.toggle-btn:active { + background: var(--accent-color); + color: var(--secondary-color); +} + +.config-toggle-btn:hover { + border-color: var(--accent-color); +} + +.config-toggle-btn:active { + background: var(--accent-color); + color: var(--secondary-color); +} + +.config-toggle-btn[data-state="true"] { + color: var(--accent-color); +} + +.config-toggle-btn[data-state="false"] { + color: var(--primary-color); +} + +.config-toggle-btn[data-state="indeterminate"] { + background-color: var(--muted-color); + color: var(--primary-color); + cursor: not-allowed; + border-color: var(--muted-color); + +} + + +/* ================================ + REAL-TIME EVENT RATE CHART + ================================ */ + +.chart-container { + margin: 20px 0; + padding: 15px; + background: var(--secondary-color); + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); +} + +#event-rate-chart { + font-family: var(--font-family); + font-size: 12px; + line-height: 1.2; + color: var(--primary-color); + background: var(--secondary-color); + padding: 20px; + overflow: hidden; + white-space: pre; + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + box-sizing: border-box; +} + +/* ================================ + SIDE NAVIGATION MENU + ================================ */ + +.side-nav { + position: fixed; + top: 0; + left: -300px; + width: 280px; + height: 100vh; + background: var(--secondary-color); + border-right: var(--border-width) solid var(--border-color); + z-index: 1000; + transition: left 0.3s ease; + overflow-y: auto; + padding-top: 80px; +} + +.side-nav.open { + left: 0; +} + +.side-nav-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 999; + display: none; +} + +.side-nav-overlay.show { + display: block; +} + +.nav-menu { + list-style: none; + padding: 0; + margin: 0; +} + +.nav-menu li { + border-bottom: var(--border-width) solid var(--muted-color); +} + +.nav-menu li:last-child { + border-bottom: none; +} + +.nav-item { + display: block; + padding: 15px 20px; + color: var(--primary-color); + text-decoration: none; + font-family: var(--font-family); + font-size: 16px; + font-weight: bold; + transition: all 0.2s ease; + cursor: pointer; + border: 2px solid var(--secondary-color); + background: none; + width: 100%; + text-align: left; +} + +.nav-item:hover { + border: 2px solid var(--secondary-color); + background:var(--muted-color); + color: var(--accent-color); +} + +.nav-item.active { + text-decoration: underline; + padding-left: 16px; + } + +.nav-footer { + position: absolute; + bottom: 20px; + left: 0; + right: 0; + padding: 0 20px; + } + +.nav-footer-btn { + display: block; + width: 100%; + padding: 12px 20px; + margin-bottom: 8px; + color: var(--primary-color); + + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: var(--font-family); + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + } + +.nav-footer-btn:hover { + background:var(--muted-color); + border-color: var(--accent-color); + } + +.nav-footer-btn:last-child { + margin-bottom: 0; + } + +.header-title.clickable { + cursor: pointer; + transition: all 0.2s ease; +} + +.header-title.clickable:hover { + opacity: 0.8; +} + +/* ================================ + SUBSCRIPTION TABLE COLLAPSIBLE GROUPS + ================================ */ + +/* Subscription group header styles */ +.subscription-group-header { + + font-weight: 500; + + cursor: pointer; + user-select: none; +} + +.subscription-group-header:hover { + background-color: var(--secondary-color); +} + +.expand-icon { + display: inline-block; + width: 20px; + transition: transform 0.2s ease; + font-size: 12px; +} + +/* Detail row styles */ +.subscription-detail-row { + /* background-color: var(--secondary-color); */ +} + +.subscription-detail-row:hover { + background-color: var(--muted-color); +} + +/* Detail row cell styles */ +.subscription-detail-prefix { + padding-left: 30px; + font-family: 'Courier New', monospace; + font-size: 11px; + color: var(--muted-color); +} + +.subscription-detail-id { + font-family: 'Courier New', monospace; + font-size: 12px; +} + diff --git a/api/index.html b/api/index.html new file mode 100644 index 0000000..b2c2a2c --- /dev/null +++ b/api/index.html @@ -0,0 +1,424 @@ + + + + + + + Blossom Admin + + + + + + + + +
+ + +
+ +
+
+ B + L + O + S + S + O + M +
+
+
Blossom
+
Loading...
+
+
Loading...
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..2d6802d --- /dev/null +++ b/api/index.js @@ -0,0 +1,5718 @@ +// Global error handler to prevent page refreshes +window.addEventListener('error', function (e) { + console.error('Global error caught:', e.error); + console.error('Error message:', e.message); + console.error('Error filename:', e.filename); + console.error('Error line:', e.lineno); + e.preventDefault(); // Prevent default browser error handling + return true; // Prevent page refresh +}); + +window.addEventListener('unhandledrejection', function (e) { + console.error('Unhandled promise rejection:', e.reason); + e.preventDefault(); // Prevent default browser error handling + return true; // Prevent page refresh +}); + +// Global state +let nlLite = null; +let userPubkey = null; +let isLoggedIn = false; +let currentConfig = null; +// Global subscription state +let relayPool = null; +let subscriptionId = null; +let isSubscribed = false; // Flag to prevent multiple simultaneous subscriptions +let isSubscribing = false; // Flag to prevent re-entry during subscription setup +// Relay connection state +let relayInfo = null; +let isRelayConnected = false; +let relayPubkey = null; +// Simple relay URL object (replaces DOM element) +let relayConnectionUrl = { value: '' }; +// Database statistics auto-refresh +let statsAutoRefreshInterval = null; +let countdownInterval = null; +let countdownSeconds = 10; + +// Side navigation state +let currentPage = 'statistics'; // Default page +let sideNavOpen = false; + +// SQL Query state +let pendingSqlQueries = new Map(); + +// Real-time event rate chart +let eventRateChart = null; +let previousTotalEvents = 0; // Track previous total for rate calculation + +// Relay Events state - now handled by main subscription + +// DOM elements +const loginModal = document.getElementById('login-modal'); +const loginModalContainer = document.getElementById('login-modal-container'); +const profileArea = document.getElementById('profile-area'); +const headerUserImage = document.getElementById('header-user-image'); +const headerUserName = document.getElementById('header-user-name'); + +// Legacy elements (kept for backward compatibility) +const persistentUserName = document.getElementById('persistent-user-name'); +const persistentUserPubkey = document.getElementById('persistent-user-pubkey'); +const persistentUserAbout = document.getElementById('persistent-user-about'); +const persistentUserDetails = document.getElementById('persistent-user-details'); +const fetchConfigBtn = document.getElementById('fetch-config-btn'); +const configDisplay = document.getElementById('config-display'); +const configTableBody = document.getElementById('config-table-body'); + +// NIP-17 DM elements +const dmOutbox = document.getElementById('dm-outbox'); +const dmInbox = document.getElementById('dm-inbox'); +const sendDmBtn = document.getElementById('send-dm-btn'); + +// Utility functions +function log(message, type = 'INFO') { + const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; + const logMessage = `${timestamp} [${type}]: ${message}`; + + // Always log to browser console so we don't lose logs on refresh + console.log(logMessage); + + // UI logging removed - using console only +} + +// Utility functions +function log(message, type = 'INFO') { + const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; + const logMessage = `${timestamp} [${type}]: ${message}`; + + // Always log to browser console so we don't lose logs on refresh + console.log(logMessage); + + // UI logging removed - using console only +} + + +// NIP-59 helper: randomize created_at to thwart time-analysis (past 2 days) +// TEMPORARILY DISABLED: Using current timestamp for debugging +function randomNow() { + // const TWO_DAYS = 2 * 24 * 60 * 60; // 172800 seconds + const now = Math.round(Date.now() / 1000); + return now; // Math.round(now - Math.random() * TWO_DAYS); +} + +// Safe JSON parse with error handling +function safeJsonParse(jsonString) { + try { + return JSON.parse(jsonString); + } catch (error) { + console.error('JSON parse error:', error); + return null; + } +} +// ================================ +// NIP-11 RELAY CONNECTION FUNCTIONS +// ================================ + +// Convert WebSocket URL to HTTP URL for NIP-11 +function wsToHttpUrl(wsUrl) { + if (wsUrl.startsWith('ws://')) { + return wsUrl.replace('ws://', 'http://'); + } else if (wsUrl.startsWith('wss://')) { + return wsUrl.replace('wss://', 'https://'); + } + return wsUrl; +} + +// Fetch relay information using NIP-11 +async function fetchRelayInfo(relayUrl) { + try { + log(`Fetching NIP-11 relay info from: ${relayUrl}`, 'INFO'); + + // Convert WebSocket URL to HTTP URL + const httpUrl = wsToHttpUrl(relayUrl); + + // Make HTTP request with NIP-11 headers + const response = await fetch(httpUrl, { + method: 'GET', + headers: { + 'Accept': 'application/nostr+json', + 'User-Agent': 'C-Relay-Admin-API/1.0' + }, + timeout: 10000 // 10 second timeout + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/nostr+json')) { + throw new Error(`Invalid content type: ${contentType}. Expected application/nostr+json`); + } + + const relayInfo = await response.json(); + + // Log if relay info is empty (not configured yet) but don't throw error + if (!relayInfo || Object.keys(relayInfo).length === 0) { + log('Relay returned empty NIP-11 info - relay not configured yet, will use manual pubkey if provided', 'INFO'); + // Return empty object - this is valid, caller will handle manual pubkey fallback + return {}; + } + + // Validate pubkey if present + if (relayInfo.pubkey && !/^[0-9a-fA-F]{64}$/.test(relayInfo.pubkey)) { + throw new Error(`Invalid relay pubkey format: ${relayInfo.pubkey}`); + } + + log(`Successfully fetched relay info. Pubkey: ${relayInfo.pubkey ? relayInfo.pubkey.substring(0, 16) + '...' : 'not set'}`, 'INFO'); + return relayInfo; + + } catch (error) { + log(`Failed to fetch relay info: ${error.message}`, 'ERROR'); + throw error; + } +} + +// Test WebSocket connection to relay +async function testWebSocketConnection(wsUrl) { + return new Promise((resolve, reject) => { + try { + log(`Testing WebSocket connection to: ${wsUrl}`, 'INFO'); + + const ws = new WebSocket(wsUrl); + const timeout = setTimeout(() => { + ws.close(); + reject(new Error('WebSocket connection timeout (10s)')); + }, 10000); + + ws.onopen = () => { + clearTimeout(timeout); + log('WebSocket connection successful', 'INFO'); + ws.close(); + resolve(true); + }; + + ws.onerror = (error) => { + clearTimeout(timeout); + log(`WebSocket connection failed: ${error.message || 'Unknown error'}`, 'ERROR'); + reject(new Error('WebSocket connection failed')); + }; + + ws.onclose = (event) => { + if (event.code !== 1000) { // 1000 = normal closure + clearTimeout(timeout); + reject(new Error(`WebSocket closed unexpectedly: ${event.code} ${event.reason}`)); + } + }; + + } catch (error) { + log(`WebSocket test error: ${error.message}`, 'ERROR'); + reject(error); + } + }); +} + + + + + +// Check for existing authentication state with multiple API methods and retry logic +async function checkExistingAuthWithRetries() { + console.log('Starting authentication state detection with retry logic...'); + + const maxAttempts = 3; + const delay = 200; // ms between attempts (reduced from 500ms) + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + console.log(`Authentication detection attempt ${attempt}/${maxAttempts}`); + + try { + // Method 1: Try window.NOSTR_LOGIN_LITE.getAuthState() + if (window.NOSTR_LOGIN_LITE && typeof window.NOSTR_LOGIN_LITE.getAuthState === 'function') { + console.log('Trying window.NOSTR_LOGIN_LITE.getAuthState()...'); + const authState = window.NOSTR_LOGIN_LITE.getAuthState(); + if (authState && authState.pubkey) { + console.log('✅ Auth state found via NOSTR_LOGIN_LITE.getAuthState():', authState.pubkey); + await restoreAuthenticationState(authState.pubkey); + return true; + } + } + + // Method 2: Try nlLite.getPublicKey() + if (nlLite && typeof nlLite.getPublicKey === 'function') { + console.log('Trying nlLite.getPublicKey()...'); + const pubkey = await nlLite.getPublicKey(); + if (pubkey && pubkey.length === 64) { + console.log('✅ Pubkey found via nlLite.getPublicKey():', pubkey); + await restoreAuthenticationState(pubkey); + return true; + } + } + + // Method 3: Try window.nostr.getPublicKey() (NIP-07) + if (window.nostr && typeof window.nostr.getPublicKey === 'function') { + console.log('Trying window.nostr.getPublicKey()...'); + const pubkey = await window.nostr.getPublicKey(); + if (pubkey && pubkey.length === 64) { + console.log('✅ Pubkey found via window.nostr.getPublicKey():', pubkey); + await restoreAuthenticationState(pubkey); + return true; + } + } + + // Method 4: Check localStorage directly for NOSTR_LOGIN_LITE data + const localStorageData = localStorage.getItem('NOSTR_LOGIN_LITE_DATA'); + if (localStorageData) { + try { + const parsedData = JSON.parse(localStorageData); + if (parsedData.pubkey) { + console.log('✅ Pubkey found in localStorage:', parsedData.pubkey); + await restoreAuthenticationState(parsedData.pubkey); + return true; + } + } catch (parseError) { + console.log('Failed to parse localStorage data:', parseError.message); + } + } + + console.log(`❌ Attempt ${attempt}: No authentication found via any method`); + + // Wait before next attempt (except for last attempt) + if (attempt < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + + } catch (error) { + console.log(`❌ Attempt ${attempt} failed:`, error.message); + if (attempt < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + console.log('🔍 Authentication detection completed - no existing auth found after all attempts'); + return false; +} + +// Helper function to restore authentication state +async function restoreAuthenticationState(pubkey) { + console.log('🔄 Restoring authentication state for pubkey:', pubkey); + + userPubkey = pubkey; + isLoggedIn = true; + + // Show main interface and profile in header + showProfileInHeader(); + loadUserProfile(); + + // Automatically set up relay connection (but don't show admin sections yet) + await setupAutomaticRelayConnection(); + + console.log('✅ Authentication state restored successfully'); +} + +// Automatically set up relay connection based on current page URL +async function setupAutomaticRelayConnection(showSections = false) { + console.log('=== SETUP AUTOMATIC RELAY CONNECTION (HTTP MODE) ==='); + console.log('showSections:', showSections); + + try { + // Get the current page URL and convert to HTTP URL for NIP-11 + const currentUrl = window.location.href; + let httpUrl = ''; + + if (currentUrl.startsWith('https://')) { + // Extract protocol and host only (remove /api or any path) + const url = new URL(currentUrl); + httpUrl = `${url.protocol}//${url.host}`; + } else if (currentUrl.startsWith('http://')) { + // Extract protocol and host only (remove /api or any path) + const url = new URL(currentUrl); + httpUrl = `${url.protocol}//${url.host}`; + } else { + // Fallback for development + httpUrl = 'http://localhost:9443'; + } + + console.log('🔗 Using base URL for NIP-11:', httpUrl); + + // Fetch relay info to get pubkey via NIP-11 (from root, not /api) + try { + const relayInfo = await fetchRelayInfo(httpUrl); + + if (relayInfo && relayInfo.pubkey) { + relayPubkey = relayInfo.pubkey; + console.log('✅ Fetched relay pubkey from NIP-11:', relayPubkey.substring(0, 16) + '...'); + } else { + throw new Error('NIP-11 response missing pubkey field'); + } + } catch (error) { + console.error('❌ Failed to fetch relay pubkey from NIP-11:', error.message); + throw new Error(`Cannot connect to server: ${error.message}`); + } + + // Mark as connected (no actual relay connection needed for HTTP mode) + isRelayConnected = true; + + // Update relay info in header + updateRelayInfoInHeader(); + + // Only show admin sections if explicitly requested + if (showSections) { + updateAdminSectionsVisibility(); + } + + console.log('✅ Automatic relay connection setup complete (HTTP mode)'); + + } catch (error) { + console.error('❌ Failed to setup automatic relay connection:', error); + // Still mark as connected to allow basic functionality + isRelayConnected = true; + if (showSections) { + updateAdminSectionsVisibility(); + } + } +} + +// Legacy function for backward compatibility +async function checkExistingAuth() { + return await checkExistingAuthWithRetries(); +} + +// Initialize NOSTR_LOGIN_LITE +async function initializeApp() { + try { + await window.NOSTR_LOGIN_LITE.init({ + theme: 'default', + methods: { + extension: true, + local: true, + seedphrase: true, + readonly: true, + connect: true, + remote: true, + otp: false + }, + floatingTab: { + enabled: false + } + }); + + nlLite = window.NOSTR_LOGIN_LITE; + console.log('Nostr login system initialized'); + + // Check for existing authentication state after initialization + const wasAlreadyLoggedIn = await checkExistingAuth(); + if (wasAlreadyLoggedIn) { + console.log('User was already logged in, showing profile in header'); + showProfileInHeader(); + // Show admin sections since user is already authenticated and relay is connected + updateAdminSectionsVisibility(); + } else { + console.log('No existing authentication found, showing login modal'); + showLoginModal(); + } + + // Listen for authentication events + window.addEventListener('nlMethodSelected', handleAuthEvent); + window.addEventListener('nlLogout', handleLogoutEvent); + + } catch (error) { + console.log('Failed to initialize Nostr login: ' + error.message); + } +} + +// Handle authentication events +function handleAuthEvent(event) { + const { pubkey, method, error } = event.detail; + + if (method && pubkey) { + userPubkey = pubkey; + isLoggedIn = true; + console.log(`Login successful! Method: ${method}`); + console.log(`Public key: ${pubkey}`); + + // Hide login modal and show profile in header + hideLoginModal(); + showProfileInHeader(); + loadUserProfile(); + + // Automatically set up relay connection and show admin sections + setupAutomaticRelayConnection(true); + + // Auto-enable monitoring when admin logs in + autoEnableMonitoring(); + + } else if (error) { + console.log(`Authentication error: ${error}`); + } +} + +// Handle logout events +function handleLogoutEvent() { + console.log('Logout event received'); + + userPubkey = null; + isLoggedIn = false; + currentConfig = null; + + // Reset relay connection state + isRelayConnected = false; + relayPubkey = null; + + // Reset UI - hide profile and show login modal + hideProfileFromHeader(); + showLoginModal(); + + updateConfigStatus(false); + updateAdminSectionsVisibility(); + + console.log('Logout event handled successfully'); +} + + +// Update visibility of admin sections based on login and relay connection status +function updateAdminSectionsVisibility() { + const shouldShow = isLoggedIn && isRelayConnected; + + // If logged in and connected, show the current page, otherwise hide all sections + if (shouldShow) { + // Show the current page + switchPage(currentPage); + + // Load data for the current page + loadCurrentPageData(); + } else { + // Hide all sections when not logged in or not connected + const sections = [ + 'databaseStatisticsSection', + 'subscriptionDetailsSection', + 'div_config', + 'authRulesSection', + 'nip17DMSection', + 'sqlQuerySection' + ]; + + sections.forEach(sectionId => { + const section = document.getElementById(sectionId); + if (section) { + section.style.display = 'none'; + } + }); + + stopStatsAutoRefresh(); + } + + // Update countdown display when visibility changes + updateCountdownDisplay(); +} + +// Load data for the current page +function loadCurrentPageData() { + switch (currentPage) { + case 'statistics': + // Load statistics immediately (no auto-refresh - using real-time monitoring events) + sendStatsQuery().catch(error => { + console.log('Auto-fetch statistics failed: ' + error.message); + }); + break; + case 'configuration': + // Load configuration + fetchConfiguration().catch(error => { + console.log('Auto-fetch configuration failed: ' + error.message); + }); + break; + case 'authorization': + // Load auth rules + loadAuthRules().catch(error => { + console.log('Auto-load auth rules failed: ' + error.message); + }); + break; + // Other pages don't need initial data loading + } +} + +// Show login modal +function showLoginModal() { + if (loginModal && loginModalContainer) { + // Initialize the login UI in the modal + if (window.NOSTR_LOGIN_LITE && typeof window.NOSTR_LOGIN_LITE.embed === 'function') { + window.NOSTR_LOGIN_LITE.embed('#login-modal-container', { + seamless: true + }); + } + loginModal.style.display = 'flex'; + } +} + +// Hide login modal +function hideLoginModal() { + if (loginModal) { + loginModal.style.display = 'none'; + } +} + +// Show profile in header +function showProfileInHeader() { + if (profileArea) { + profileArea.style.display = 'flex'; + } +} + +// Hide profile from header +function hideProfileFromHeader() { + if (profileArea) { + profileArea.style.display = 'none'; + } +} + +// Update login/logout UI visibility (legacy function - kept for backward compatibility) +function updateLoginLogoutUI() { + // This function is now handled by showProfileInHeader() and hideProfileFromHeader() + // Kept for backward compatibility with any existing code that might call it +} + +// Show main interface after login (legacy function - kept for backward compatibility) +function showMainInterface() { + // This function is now handled by showProfileInHeader() and updateAdminSectionsVisibility() + // Kept for backward compatibility with any existing code that might call it + updateAdminSectionsVisibility(); +} + +// Load user profile using nostr-tools pool +async function loadUserProfile() { + if (!userPubkey) return; + + console.log('Loading user profile...'); + + // Update header display (new system) + if (headerUserName) { + headerUserName.textContent = 'Loading...'; + } + + // Update legacy elements if they exist (backward compatibility) + if (persistentUserName) { + persistentUserName.textContent = 'Loading...'; + } + if (persistentUserAbout) { + persistentUserAbout.textContent = 'Loading...'; + } + + // Convert hex pubkey to npub for initial display + let displayPubkey = userPubkey; + let npubLink = ''; + try { + if (userPubkey && userPubkey.length === 64 && /^[0-9a-fA-F]+$/.test(userPubkey)) { + const npub = window.NostrTools.nip19.npubEncode(userPubkey); + displayPubkey = npub; + npubLink = `${npub}`; + } + } catch (error) { + console.log('Failed to encode user pubkey to npub:', error.message); + } + + if (persistentUserPubkey) { + if (npubLink) { + persistentUserPubkey.innerHTML = npubLink; + } else { + persistentUserPubkey.textContent = displayPubkey; + } + } + + try { + // Create a SimplePool instance for profile loading + const profilePool = new window.NostrTools.SimplePool(); + const relays = ['wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol', + 'wss://relay.primal.net', + 'wss://relay.snort.social' + ]; + + // Get profile event (kind 0) for the user with timeout + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Profile query timeout')), 5000) + ); + + const queryPromise = profilePool.querySync(relays, { + kinds: [0], + authors: [userPubkey], + limit: 1 + }); + + const events = await Promise.race([queryPromise, timeoutPromise]); + + if (events.length > 0) { + console.log('Profile event found:', events[0]); + const profile = JSON.parse(events[0].content); + console.log('Parsed profile:', profile); + displayProfile(profile); + } else { + console.log('No profile events found for pubkey:', userPubkey); + + // Update header display (new system) + if (headerUserName) { + headerUserName.textContent = 'Anonymous User'; + } + + // Update legacy elements if they exist (backward compatibility) + if (persistentUserName) { + persistentUserName.textContent = 'Anonymous User'; + } + if (persistentUserAbout) { + persistentUserAbout.textContent = 'No profile found'; + } + // Keep the npub display + } + + // Properly close the profile pool with error handling + try { + await profilePool.close(relays); + // Give time for cleanup + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (closeError) { + console.log('Profile pool close error (non-critical):', closeError.message); + } + + } catch (error) { + console.log('Profile loading failed: ' + error.message); + + // Update header display (new system) + if (headerUserName) { + headerUserName.textContent = 'Error loading profile'; + } + + // Update legacy elements if they exist (backward compatibility) + if (persistentUserName) { + persistentUserName.textContent = 'Error loading profile'; + } + if (persistentUserAbout) { + persistentUserAbout.textContent = error.message; + } + // Keep the npub display + } +} + +// Display profile data +function displayProfile(profile) { + const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User'; + const about = profile.about || 'No description provided'; + const picture = profile.picture || profile.image || null; + + // Convert hex pubkey to npub for display + let displayPubkey = userPubkey; + let npubLink = ''; + try { + if (userPubkey && userPubkey.length === 64 && /^[0-9a-fA-F]+$/.test(userPubkey)) { + const npub = window.NostrTools.nip19.npubEncode(userPubkey); + displayPubkey = npub; + npubLink = `${npub}`; + } + } catch (error) { + console.log('Failed to encode user pubkey to npub:', error.message); + } + + // Update header profile display + if (headerUserName) { + headerUserName.textContent = name; + } + + // Handle header profile picture + if (headerUserImage) { + if (picture && typeof picture === 'string' && (picture.startsWith('http') || picture.startsWith('https'))) { + headerUserImage.src = picture; + headerUserImage.style.display = 'block'; + headerUserImage.onerror = function() { + // Hide image on error + this.style.display = 'none'; + console.log('Profile image failed to load:', picture); + }; + } else { + headerUserImage.style.display = 'none'; + } + } + + // Update legacy persistent user details (kept for backward compatibility) + if (persistentUserName) persistentUserName.textContent = name; + if (persistentUserPubkey && npubLink) { + persistentUserPubkey.innerHTML = npubLink; + } else if (persistentUserPubkey) { + persistentUserPubkey.textContent = displayPubkey; + } + if (persistentUserAbout) persistentUserAbout.textContent = about; + + // Handle legacy profile picture + const userImageContainer = document.getElementById('persistent-user-image'); + if (userImageContainer) { + if (picture && typeof picture === 'string' && picture.startsWith('http')) { + // Create or update image element + let img = userImageContainer.querySelector('img'); + if (!img) { + img = document.createElement('img'); + img.className = 'user-profile-image'; + img.alt = `${name}'s profile picture`; + img.onerror = function() { + // Hide image on error + this.style.display = 'none'; + }; + userImageContainer.appendChild(img); + } + img.src = picture; + img.style.display = 'block'; + } else { + // Hide image if no valid picture + const img = userImageContainer.querySelector('img'); + if (img) { + img.style.display = 'none'; + } + } + } + + console.log(`Profile loaded for: ${name} with pubkey: ${userPubkey}`); +} + +// Logout function +async function logout() { + log('Logging out...', 'INFO'); + try { + // Stop auto-refresh before disconnecting + stopStatsAutoRefresh(); + + // Clean up relay pool + if (relayPool) { + log('Closing relay pool...', 'INFO'); + const url = relayConnectionUrl.value.trim(); + if (url) { + try { + await relayPool.close([url]); + } catch (e) { + console.log('Pool close error (non-critical):', e.message); + } + } + relayPool = null; + subscriptionId = null; + } + + // Reset subscription flags + isSubscribed = false; + isSubscribing = false; + + await nlLite.logout(); + + userPubkey = null; + isLoggedIn = false; + currentConfig = null; + + // Reset relay connection state + isRelayConnected = false; + relayPubkey = null; + + // Reset UI - hide profile and show login modal + hideProfileFromHeader(); + + updateConfigStatus(false); + updateAdminSectionsVisibility(); + + log('Logged out successfully', 'INFO'); + } catch (error) { + log('Logout failed: ' + error.message, 'ERROR'); + } +} + +function updateConfigStatus(loaded) { + if (loaded) { + configDisplay.classList.remove('hidden'); + } else { + configDisplay.classList.add('hidden'); + } +} + + + +// Generate random subscription ID (avoiding colons which are rejected by relay) +function generateSubId() { + // Use only alphanumeric characters, underscores, hyphens, and commas + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-,'; + let result = ''; + for (let i = 0; i < 12; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +// WebSocket monitoring function to attach to SimplePool connections +function attachWebSocketMonitoring(relayPool, url) { + console.log('🔍 Attaching WebSocket monitoring to SimplePool...'); + + // SimplePool stores connections in _conn object + if (relayPool && relayPool._conn) { + // Monitor when connections are created + const originalGetConnection = relayPool._conn[url]; + if (originalGetConnection) { + console.log('📡 Found existing connection for URL:', url); + + // Try to access the WebSocket if it's available + const conn = relayPool._conn[url]; + if (conn && conn.ws) { + attachWebSocketEventListeners(conn.ws, url); + } + } + + // Override the connection getter to monitor new connections + const originalConn = relayPool._conn; + relayPool._conn = new Proxy(originalConn, { + get(target, prop) { + const conn = target[prop]; + if (conn && conn.ws && !conn.ws._monitored) { + console.log('🔗 New WebSocket connection detected for:', prop); + attachWebSocketEventListeners(conn.ws, prop); + conn.ws._monitored = true; + } + return conn; + }, + set(target, prop, value) { + if (value && value.ws && !value.ws._monitored) { + console.log('🔗 WebSocket connection being set for:', prop); + attachWebSocketEventListeners(value.ws, prop); + value.ws._monitored = true; + } + target[prop] = value; + return true; + } + }); + } + + console.log('✅ WebSocket monitoring attached'); +} + +function attachWebSocketEventListeners(ws, url) { + console.log(`🎯 Attaching event listeners to WebSocket for ${url}`); + + // Log connection open + ws.addEventListener('open', (event) => { + console.log(`🔓 WebSocket OPEN for ${url}:`, { + readyState: ws.readyState, + url: ws.url, + protocol: ws.protocol, + extensions: ws.extensions + }); + }); + + // Log incoming messages with full details + ws.addEventListener('message', (event) => { + try { + const data = event.data; + console.log(`📨 WebSocket MESSAGE from ${url}:`, { + type: event.type, + data: data, + dataLength: data.length, + timestamp: new Date().toISOString() + }); + + // Try to parse as JSON for Nostr messages + try { + const parsed = JSON.parse(data); + if (Array.isArray(parsed)) { + const [type, ...args] = parsed; + console.log(`📨 Parsed Nostr message [${type}]:`, args); + } else { + console.log(`📨 Parsed JSON:`, parsed); + } + } catch (parseError) { + console.log(`📨 Raw message (not JSON):`, data); + } + } catch (error) { + console.error(`❌ Error processing WebSocket message from ${url}:`, error); + } + }); + + // Log connection close with details + ws.addEventListener('close', (event) => { + console.log(`🔒 WebSocket CLOSE for ${url}:`, { + code: event.code, + reason: event.reason, + wasClean: event.wasClean, + readyState: ws.readyState, + timestamp: new Date().toISOString() + }); + }); + + // Log errors with full details + ws.addEventListener('error', (event) => { + console.error(`❌ WebSocket ERROR for ${url}:`, { + type: event.type, + target: event.target, + readyState: ws.readyState, + url: ws.url, + timestamp: new Date().toISOString() + }); + + // Log additional WebSocket state + console.error(`❌ WebSocket state details:`, { + readyState: ws.readyState, + bufferedAmount: ws.bufferedAmount, + protocol: ws.protocol, + extensions: ws.extensions, + binaryType: ws.binaryType + }); + }); + + // Override send method to log outgoing messages + const originalSend = ws.send; + ws.send = function(data) { + console.log(`📤 WebSocket SEND to ${url}:`, { + data: data, + dataLength: data.length, + readyState: ws.readyState, + timestamp: new Date().toISOString() + }); + + // Try to parse outgoing Nostr messages + try { + const parsed = JSON.parse(data); + if (Array.isArray(parsed)) { + const [type, ...args] = parsed; + console.log(`📤 Outgoing Nostr message [${type}]:`, args); + } else { + console.log(`📤 Outgoing JSON:`, parsed); + } + } catch (parseError) { + console.log(`📤 Outgoing raw message (not JSON):`, data); + } + + return originalSend.call(this, data); + }; + + console.log(`✅ Event listeners attached to WebSocket for ${url}`); +} + +// Configuration subscription using nostr-tools SimplePool +async function subscribeToConfiguration() { + try { + console.log('=== SUBSCRIBE TO CONFIGURATION ==='); + console.log('Call stack:', new Error().stack); + + // If pool already exists and subscribed, we're done + if (relayPool && isSubscribed) { + console.log('✅ Already subscribed, reusing existing pool'); + return true; + } + + // Prevent concurrent subscription attempts + if (isSubscribing) { + console.log('⚠️ Subscription already in progress'); + return false; + } + + isSubscribing = true; + + const url = relayConnectionUrl.value.trim(); + if (!url) { + console.error('No relay URL configured'); + isSubscribing = false; + return false; + } + + console.log(`🔌 Connecting to relay: ${url}`); + + // Create pool ONLY if it doesn't exist + if (!relayPool) { + console.log('✨ Creating NEW SimplePool for admin operations'); + relayPool = new window.NostrTools.SimplePool(); + + // Attach WebSocket monitoring to the new pool + attachWebSocketMonitoring(relayPool, url); + } else { + console.log('♻️ Reusing existing SimplePool'); + } + + subscriptionId = generateSubId(); + + console.log(`📝 Generated subscription ID: ${subscriptionId}`); + console.log(`👤 User pubkey: ${userPubkey}`); + console.log(`🎯 About to call relayPool.subscribeMany with URL: ${url}`); + console.log(`📊 relayPool._conn before subscribeMany:`, Object.keys(relayPool._conn || {})); + + // Mark as subscribed BEFORE calling subscribeMany to prevent race conditions + isSubscribed = true; + + // Subscribe to kind 23457 events (admin response events), kind 4 (NIP-04 DMs), kind 1059 (NIP-17 GiftWrap), kind 24567 (ephemeral monitoring events), and relay events (kinds 0, 10050, 10002) + console.log('🔔 Calling relayPool.subscribeMany with all filters...'); + const subscription = relayPool.subscribeMany([url], [{ + since: Math.floor(Date.now() / 1000) - 5, // Look back 5 seconds to avoid race condition + kinds: [23457], + authors: [getRelayPubkey()], // Only listen to responses from the relay + "#p": [userPubkey], // Only responses directed to this user + limit: 50 + }, { + since: Math.floor(Date.now() / 1000), + kinds: [4], // NIP-04 Direct Messages + authors: [getRelayPubkey()], // Only listen to DMs from the relay + "#p": [userPubkey], // Only DMs directed to this user + limit: 50 + }, { + since: Math.floor(Date.now() / 1000) - (2 * 24 * 60 * 60), // Look back 2 days for NIP-59 randomized timestamps + kinds: [1059], // NIP-17 GiftWrap events + "#p": [userPubkey], // Only GiftWrap events addressed to this user + limit: 50 + }, { + since: Math.floor(Date.now() / 1000), // Start from current time + kinds: [24567], // Real-time ephemeral monitoring events + authors: [getRelayPubkey()], // Only listen to monitoring events from the relay + "#d": isLoggedIn ? ["event_kinds", "time_stats", "top_pubkeys", "subscription_details", "cpu_metrics"] : ["event_kinds", "time_stats", "top_pubkeys", "cpu_metrics"], // Include subscription_details only when authenticated, cpu_metrics available to all + limit: 50 + }, { + since: Math.floor(Date.now() / 1000) - (24 * 60 * 60), // Look back 24 hours for relay events + kinds: [0, 10050, 10002], // Relay events: metadata, DM relays, relay list + authors: [getRelayPubkey()], // Only listen to relay's own events + limit: 10 + }], { + async onevent(event) { + // Simplified logging - one line per event + if (event.kind === 24567) { + const dTag = event.tags.find(tag => tag[0] === 'd'); + const dataType = dTag ? dTag[1] : 'unknown'; + // console.log(`📊 Monitoring event: ${dataType}`); + } else { + console.log(`📨 Event received: kind ${event.kind}`); + } + + // Handle NIP-04 DMs + if (event.kind === 4) { + try { + // Decrypt the DM content + const decryptedContent = await window.nostr.nip04.decrypt(event.pubkey, event.content); + log(`Received NIP-04 DM from relay: ${decryptedContent.substring(0, 50)}...`, 'INFO'); + + // Add to inbox + const timestamp = new Date(event.created_at * 1000).toLocaleString(); + addMessageToInbox('received', decryptedContent, timestamp, event.pubkey); + + // Log for testing + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `NIP-04 DM: ${decryptedContent}`, 'DM'); + } + } catch (decryptError) { + log(`Failed to decrypt NIP-04 DM: ${decryptError.message}`, 'ERROR'); + if (typeof logTestEvent === 'function') { + logTestEvent('ERROR', `Failed to decrypt DM: ${decryptError.message}`, 'DM'); + } + } + return; + } + + // Handle NIP-17 GiftWrap DMs + if (event.kind === 1059) { + console.log(`📨 RECEIVED KIND 1059 EVENT:`, { + id: event.id, + pubkey: event.pubkey, + created_at: event.created_at, + content: event.content.substring(0, 100) + '...', + tags: event.tags + }); + + try { + // Step 1: Unwrap gift wrap to get seal + const sealJson = await window.nostr.nip44.decrypt(event.pubkey, event.content); + console.log(`🔓 STEP 1 - Unwrapped gift wrap:`, sealJson.substring(0, 100) + '...'); + const seal = safeJsonParse(sealJson); + if (!seal || seal.kind !== 13) { + throw new Error('Unwrapped content is not a valid seal (kind 13)'); + } + console.log(`✅ Seal validated:`, { kind: seal.kind, pubkey: seal.pubkey.substring(0, 16) + '...' }); + + // Step 2: Unseal to get rumor + const rumorJson = await window.nostr.nip44.decrypt(seal.pubkey, seal.content); + console.log(`🔓 STEP 2 - Unsealed rumor:`, rumorJson.substring(0, 100) + '...'); + const rumor = safeJsonParse(rumorJson); + if (!rumor || rumor.kind !== 14) { + throw new Error('Unsealed content is not a valid rumor (kind 14)'); + } + console.log(`✅ Rumor validated:`, { kind: rumor.kind, pubkey: rumor.pubkey.substring(0, 16) + '...', content: rumor.content.substring(0, 50) + '...' }); + + log(`Received NIP-17 DM from relay: ${rumor.content.substring(0, 50)}...`, 'INFO'); + + // Add to inbox + const timestamp = new Date(event.created_at * 1000).toLocaleString(); + addMessageToInbox('received', rumor.content, timestamp, rumor.pubkey); + + // Log for testing + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `NIP-17 DM: ${rumor.content}`, 'DM'); + } + } catch (unwrapError) { + console.error(`❌ NIP-17 DM UNWRAP FAILED:`, unwrapError); + log(`Failed to unwrap NIP-17 DM: ${unwrapError.message}`, 'ERROR'); + if (typeof logTestEvent === 'function') { + logTestEvent('ERROR', `Failed to unwrap DM: ${unwrapError.message}`, 'DM'); + } + } + return; + } + + // Handle admin response events (kind 23457) + if (event.kind === 23457) { + // Log all received messages for testing + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `Admin response event: ${JSON.stringify(event)}`, 'EVENT'); + } + + // Process admin response event + processAdminResponse(event); + } + + // Handle monitoring events (kind 24567 - ephemeral) + if (event.kind === 24567) { + // Process monitoring event (logging done above) + processMonitoringEvent(event); + } + + // Handle relay events (kinds 0, 10050, 10002) + if ([0, 10050, 10002].includes(event.kind)) { + handleRelayEventReceived(event); + } + }, + oneose() { + console.log('EOSE received - End of stored events'); + console.log('Current config after EOSE:', currentConfig); + + if (!currentConfig) { + console.log('No configuration events were received'); + } + }, + onclose(reason) { + console.log('Subscription closed:', reason); + // Reset subscription state to allow re-subscription + isSubscribed = false; + isSubscribing = false; + isRelayConnected = false; + updateConfigStatus(false); + log('WebSocket connection closed - subscription state reset', 'WARNING'); + } + }); + + // Store subscription for cleanup + relayPool.currentSubscription = subscription; + + // Mark as subscribed + isSubscribed = true; + isSubscribing = false; + + console.log('✅ Subscription established successfully'); + return true; + + } catch (error) { + console.error('Configuration subscription failed:', error.message); + console.error('Configuration subscription failed:', error); + console.error('Error stack:', error.stack); + isSubscribing = false; + return false; + } +} + +// Process admin response events (kind 23457) +async function processAdminResponse(event) { + try { + console.log('=== PROCESSING ADMIN RESPONSE ==='); + console.log('Response event:', event); + + // Verify this is a kind 23457 admin response event + if (event.kind !== 23457) { + console.log('Ignoring non-admin response event, kind:', event.kind); + return; + } + + // Verify the event is from the relay + const expectedRelayPubkey = getRelayPubkey(); + if (event.pubkey !== expectedRelayPubkey) { + console.log('Ignoring response from unknown pubkey:', event.pubkey); + return; + } + + // Decrypt the NIP-44 encrypted content + const decryptedContent = await decryptFromRelay(event.content); + if (!decryptedContent) { + throw new Error('Failed to decrypt admin response content'); + } + + // console.log('Decrypted admin response:', decryptedContent); + + // Try to parse as JSON first, if it fails treat as plain text + let responseData; + try { + responseData = JSON.parse(decryptedContent); + console.log('Parsed response data:', responseData); + } catch (parseError) { + // Not JSON - treat as plain text response + console.log('Response is plain text, not JSON'); + responseData = { + plain_text: true, + message: decryptedContent + }; + } + + // Log the response for testing + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `Decrypted response: ${JSON.stringify(responseData)}`, 'RESPONSE'); + } + + // Handle different types of admin responses + handleAdminResponseData(responseData); + + } catch (error) { + console.error('Error processing admin response:', error); + if (typeof logTestEvent === 'function') { + logTestEvent('ERROR', `Failed to process admin response: ${error.message}`, 'ERROR'); + } + } +} + +// Initialize real-time event rate chart +function initializeEventRateChart() { + try { + console.log('=== INITIALIZING EVENT RATE CHART ==='); + + const chartContainer = document.getElementById('event-rate-chart'); + console.log('Chart container found:', chartContainer); + + if (!chartContainer) { + console.log('Event rate chart container not found'); + return; + } + + // Show immediate placeholder content + chartContainer.textContent = 'Initializing event rate chart...'; + console.log('Set placeholder content'); + + // Check if ASCIIBarChart is available + console.log('Checking ASCIIBarChart availability...'); + console.log('typeof ASCIIBarChart:', typeof ASCIIBarChart); + console.log('window.ASCIIBarChart:', window.ASCIIBarChart); + + if (typeof ASCIIBarChart === 'undefined') { + console.log('ASCIIBarChart not available - text_graph.js may not be loaded'); + // Show a more detailed error message + chartContainer.innerHTML = ` +
+ ⚠️ Chart library not loaded
+ Check: /text_graph/text_graph.js
+ Real-time event visualization unavailable +
+ `; + return; + } + + // Create stub elements that the chart expects for info display + createChartStubElements(); + + console.log('Creating ASCIIBarChart instance...'); + + // Initialize the chart with correct parameters based on text_graph.js API + eventRateChart = new ASCIIBarChart('event-rate-chart', { + maxHeight: 11, // Chart height in lines + maxDataPoints: 76, // Show last 76 bins (5+ minutes of history) + title: 'New Events', // Chart title + xAxisLabel: '', // No X-axis label + yAxisLabel: '', // No Y-axis label + autoFitWidth: true, // Enable responsive font sizing + useBinMode: true, // Enable time bin aggregation + binDuration: 4000, // 4-second time bins + xAxisLabelFormat: 'elapsed', // Show elapsed time labels + debug: false // Disable debug logging + }); + + console.log('ASCIIBarChart instance created:', eventRateChart); + console.log('Chart container content after init:', chartContainer.textContent); + console.log('Chart container innerHTML after init:', chartContainer.innerHTML); + + // Force an initial render + if (eventRateChart && typeof eventRateChart.render === 'function') { + console.log('Forcing initial render...'); + eventRateChart.render(); + console.log('Chart container content after render:', chartContainer.textContent); + } + + console.log('Event rate chart initialized successfully'); + log('Real-time event rate chart initialized', 'INFO'); + + } catch (error) { + console.error('Failed to initialize event rate chart:', error); + console.error('Error stack:', error.stack); + log(`Failed to initialize event rate chart: ${error.message}`, 'ERROR'); + + // Show detailed error message in the container + const chartContainer = document.getElementById('event-rate-chart'); + if (chartContainer) { + chartContainer.innerHTML = ` +
+ ❌ Chart initialization failed
+ ${error.message}
+ Check browser console for details +
+ `; + } + } +} + +// Create stub elements that the ASCIIBarChart expects for info display +function createChartStubElements() { + const stubIds = ['values', 'max-value', 'scale', 'count']; + + stubIds.forEach(id => { + if (!document.getElementById(id)) { + const stubElement = document.createElement('div'); + stubElement.id = id; + stubElement.style.display = 'none'; // Hide stub elements + document.body.appendChild(stubElement); + } + }); + + console.log('Chart stub elements created'); +} + +// Handle monitoring events (kind 24567 - ephemeral) +async function processMonitoringEvent(event) { + try { + // Verify this is a kind 24567 ephemeral monitoring event + if (event.kind !== 24567) { + return; + } + + // Verify the event is from the relay + const expectedRelayPubkey = getRelayPubkey(); + if (event.pubkey !== expectedRelayPubkey) { + return; + } + + // Check the d-tag to determine which type of monitoring event this is + const dTag = event.tags.find(tag => tag[0] === 'd'); + if (!dTag) { + return; + } + + // Parse the monitoring data (content is JSON, not encrypted for monitoring events) + const monitoringData = JSON.parse(event.content); + + // Route to appropriate handler based on d-tag (no verbose logging) + switch (dTag[1]) { + case 'event_kinds': + updateStatsFromMonitoringEvent(monitoringData); + break; + + case 'time_stats': + updateStatsFromTimeMonitoringEvent(monitoringData); + break; + + case 'top_pubkeys': + updateStatsFromTopPubkeysMonitoringEvent(monitoringData); + break; + + + case 'subscription_details': + // Only process subscription details if user is authenticated + if (isLoggedIn) { + updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData); + // Also update the active subscriptions count from this data + if (monitoringData.data && monitoringData.data.subscriptions) { + updateStatsCell('active-subscriptions', monitoringData.data.subscriptions.length.toString()); + } + } + break; + + case 'cpu_metrics': + updateStatsFromCpuMonitoringEvent(monitoringData); + break; + + default: + return; + } + + } catch (error) { + console.error('Error processing monitoring event:', error); + log(`Failed to process monitoring event: ${error.message}`, 'ERROR'); + } +} + +// Handle different types of admin response data +function handleAdminResponseData(responseData) { + try { + console.log('=== HANDLING ADMIN RESPONSE DATA ==='); + console.log('Response data:', responseData); + console.log('Response query_type:', responseData.query_type); + + // Handle plain text responses (from create_relay_event and other commands) + if (responseData.plain_text) { + console.log('Handling plain text response'); + log(responseData.message, 'INFO'); + + // Show the message in relay events status if we're on that page + if (currentPage === 'relay-events') { + // Try to determine which kind based on message content + if (responseData.message.includes('Kind: 0')) { + showStatus('kind0-status', responseData.message, 'success'); + } else if (responseData.message.includes('Kind: 10050')) { + showStatus('kind10050-status', responseData.message, 'success'); + } else if (responseData.message.includes('Kind: 10002')) { + showStatus('kind10002-status', responseData.message, 'success'); + } + } + return; + } + + // Handle auth query responses - updated to match backend response types + if (responseData.query_type && + (responseData.query_type.includes('auth_rules') || + responseData.query_type.includes('auth'))) { + console.log('Routing to auth query handler'); + handleAuthQueryResponse(responseData); + return; + } + + // Handle config update responses specifically + if (responseData.query_type === 'config_update') { + console.log('Routing to config update handler'); + handleConfigUpdateResponse(responseData); + return; + } + + // Handle config query responses - updated to match backend response types + if (responseData.query_type && + (responseData.query_type.includes('config') || + responseData.query_type.startsWith('config_'))) { + console.log('Routing to config query handler'); + handleConfigQueryResponse(responseData); + return; + } + + // Handle system command responses + if (responseData.command) { + console.log('Routing to system command handler'); + handleSystemCommandResponse(responseData); + return; + } + + // Handle auth rule modification responses + if (responseData.operation || responseData.rules_processed !== undefined) { + console.log('Routing to auth rule modification handler'); + handleAuthRuleResponse(responseData); + return; + } + + // Handle stats query responses + if (responseData.query_type === 'stats_query') { + console.log('Routing to stats query handler'); + handleStatsQueryResponse(responseData); + return; + } + + // Handle SQL query responses + if (responseData.query_type === 'sql_query') { + console.log('Routing to SQL query handler'); + handleSqlQueryResponse(responseData); + return; + } + + // Generic response handling + console.log('Using generic response handler'); + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `Generic admin response: ${JSON.stringify(responseData)}`, 'RESPONSE'); + } + + } catch (error) { + console.error('Error handling admin response data:', error); + if (typeof logTestEvent === 'function') { + logTestEvent('ERROR', `Failed to handle response data: ${error.message}`, 'ERROR'); + } + } +} + +// Handle config query responses +function handleConfigQueryResponse(responseData) { + console.log('=== CONFIG QUERY RESPONSE ==='); + console.log('Query type:', responseData.query_type); + console.log('Total results:', responseData.total_results); + console.log('Data:', responseData.data); + console.log('Data type:', typeof responseData.data); + console.log('Is array:', Array.isArray(responseData.data)); + + // Check if data exists and has content (handle both object and array formats) + const hasData = responseData.data && ( + (Array.isArray(responseData.data) && responseData.data.length > 0) || + (!Array.isArray(responseData.data) && Object.keys(responseData.data).length > 0) + ); + + if (hasData) { + console.log('Converting config response to display format...'); + + // Create a synthetic event structure for displayConfiguration + const syntheticEvent = { + id: 'config_response_' + Date.now(), + pubkey: getRelayPubkey(), + created_at: Math.floor(Date.now() / 1000), + kind: 'config_response', + content: 'Configuration from admin API', + tags: [] + }; + + // Convert config data to tags format - handle both array and object formats + if (Array.isArray(responseData.data)) { + // Array format: [{key: 'x', value: 'y'}, ...] + responseData.data.forEach(config => { + const key = config.key || config.config_key; + const value = config.value || config.config_value; + if (key && value !== undefined) { + syntheticEvent.tags.push([key, value]); + } + }); + } else { + // Object format: {key1: 'value1', key2: 'value2', ...} + Object.entries(responseData.data).forEach(([key, value]) => { + if (key && value !== undefined) { + syntheticEvent.tags.push([key, value]); + } + }); + } + + console.log('Synthetic event created:', syntheticEvent); + console.log('Calling displayConfiguration with synthetic event...'); + + // Display the configuration using the original display function + displayConfiguration(syntheticEvent); + + // Update relay info in header with config data + updateStoredRelayInfo(responseData); + + // Initialize toggle buttons with config data + initializeToggleButtonsFromConfig(responseData); + + const configCount = Array.isArray(responseData.data) ? + responseData.data.length : + Object.keys(responseData.data).length; + log(`Configuration loaded: ${configCount} parameters`, 'INFO'); + } else { + console.log('No configuration data received'); + updateConfigStatus(false); + } + + // Also log to test interface for debugging + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `Config query response: ${responseData.query_type}, ${responseData.total_results} results`, 'CONFIG_QUERY'); + + if (responseData.data && responseData.data.length > 0) { + logTestEvent('RECV', '=== CONFIGURATION VALUES ===', 'CONFIG'); + responseData.data.forEach((config, index) => { + const key = config.key || config.config_key || `config_${index}`; + const value = config.value || config.config_value || 'undefined'; + const category = config.category || 'general'; + const dataType = config.data_type || 'string'; + + logTestEvent('RECV', `${key}: ${value} (${dataType}, ${category})`, 'CONFIG'); + }); + logTestEvent('RECV', '=== END CONFIGURATION VALUES ===', 'CONFIG'); + } else { + logTestEvent('RECV', 'No configuration values found', 'CONFIG_QUERY'); + } + } +} + +// Handle config update responses +function handleConfigUpdateResponse(responseData) { + console.log('=== CONFIG UPDATE RESPONSE ==='); + console.log('Query type:', responseData.query_type); + console.log('Status:', responseData.status); + console.log('Data:', responseData.data); + + if (responseData.status === 'success') { + const updatesApplied = responseData.updates_applied || 0; + log(`Configuration updated successfully: ${updatesApplied} parameters changed`, 'INFO'); + + // Show success message with details + if (responseData.data && Array.isArray(responseData.data)) { + responseData.data.forEach((config, index) => { + if (config.status === 'success') { + log(`✓ ${config.key}: ${config.value} (${config.data_type})`, 'INFO'); + } else { + log(`✗ ${config.key}: ${config.error || 'Failed to update'}`, 'ERROR'); + } + }); + } + + // Configuration updated successfully - user can manually refresh using Fetch Config button + log('Configuration updated successfully. Click "Fetch Config" to refresh the display.', 'INFO'); + + } else { + const errorMessage = responseData.message || responseData.error || 'Unknown error'; + log(`Configuration update failed: ${errorMessage}`, 'ERROR'); + + // Show detailed error information if available + if (responseData.data && Array.isArray(responseData.data)) { + responseData.data.forEach((config, index) => { + if (config.status === 'error') { + log(`✗ ${config.key}: ${config.error || 'Failed to update'}`, 'ERROR'); + } + }); + } + } + + // Log to test interface for debugging + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `Config update response: ${responseData.status}`, 'CONFIG_UPDATE'); + + if (responseData.data && responseData.data.length > 0) { + responseData.data.forEach((config, index) => { + const status = config.status === 'success' ? '✓' : '✗'; + const message = config.status === 'success' ? + `${config.key} = ${config.value}` : + `${config.key}: ${config.error || 'Failed'}`; + logTestEvent('RECV', `${status} ${message}`, 'CONFIG_UPDATE'); + }); + } else { + logTestEvent('RECV', 'No configuration update details received', 'CONFIG_UPDATE'); + } + } +} + +// Handle auth query responses +function handleAuthQueryResponse(responseData) { + console.log('=== AUTH QUERY RESPONSE ==='); + console.log('Query type:', responseData.query_type); + console.log('Total results:', responseData.total_results); + console.log('Data:', responseData.data); + + // Update the current auth rules with the response data + if (responseData.data && Array.isArray(responseData.data)) { + currentAuthRules = responseData.data; + console.log('Updated currentAuthRules with', currentAuthRules.length, 'rules'); + + // Always show the auth rules table when we receive data (no VIEW RULES button anymore) + console.log('Auto-showing auth rules table since we received data...'); + showAuthRulesTable(); + + updateAuthRulesStatus('loaded'); + log(`Loaded ${responseData.total_results} auth rules from relay`, 'INFO'); + } else { + currentAuthRules = []; + console.log('No auth rules data received, cleared currentAuthRules'); + + // Show empty table (no VIEW RULES button anymore) + console.log('Auto-showing auth rules table with empty data...'); + showAuthRulesTable(); + + updateAuthRulesStatus('loaded'); + log('No auth rules found on relay', 'INFO'); + } + + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `Auth query response: ${responseData.query_type}, ${responseData.total_results} results`, 'AUTH_QUERY'); + + if (responseData.data && responseData.data.length > 0) { + responseData.data.forEach((rule, index) => { + logTestEvent('RECV', `Rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE'); + }); + } else { + logTestEvent('RECV', 'No auth rules found', 'AUTH_QUERY'); + } + } +} + +// Handle system command responses +function handleSystemCommandResponse(responseData) { + console.log('=== SYSTEM COMMAND RESPONSE ==='); + console.log('Command:', responseData.command); + console.log('Status:', responseData.status); + + // Handle delete auth rule responses + if (responseData.command === 'delete_auth_rule') { + if (responseData.status === 'success') { + log('Auth rule deleted successfully', 'INFO'); + // Refresh the auth rules display + loadAuthRules(); + } else { + log(`Failed to delete auth rule: ${responseData.message || 'Unknown error'}`, 'ERROR'); + } + } + + // Handle clear all auth rules responses + if (responseData.command === 'clear_all_auth_rules') { + if (responseData.status === 'success') { + const rulesCleared = responseData.rules_cleared || 0; + log(`Successfully cleared ${rulesCleared} auth rules`, 'INFO'); + // Clear local auth rules and refresh display + currentAuthRules = []; + displayAuthRules(currentAuthRules); + } else { + log(`Failed to clear auth rules: ${responseData.message || 'Unknown error'}`, 'ERROR'); + } + } + + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `System command response: ${responseData.command} - ${responseData.status}`, 'SYSTEM_CMD'); + } +} + +// Handle auth rule modification responses +function handleAuthRuleResponse(responseData) { + console.log('=== AUTH RULE MODIFICATION RESPONSE ==='); + console.log('Operation:', responseData.operation); + console.log('Status:', responseData.status); + + // Handle auth rule addition/modification responses + if (responseData.status === 'success') { + const rulesProcessed = responseData.rules_processed || 0; + log(`Successfully processed ${rulesProcessed} auth rule modifications`, 'INFO'); + + // Refresh the auth rules display to show the new rules + if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { + loadAuthRules(); + } + } else { + log(`Failed to process auth rule modifications: ${responseData.message || 'Unknown error'}`, 'ERROR'); + } + + if (typeof logTestEvent === 'function') { + logTestEvent('RECV', `Auth rule response: ${responseData.operation} - ${responseData.status}`, 'AUTH_RULE'); + + if (responseData.processed_rules) { + responseData.processed_rules.forEach((rule, index) => { + logTestEvent('RECV', `Processed rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE'); + }); + } + } +} + +// Helper function to decrypt content from relay using NIP-44 +async function decryptFromRelay(encryptedContent) { + try { + console.log('Decrypting content from relay...'); + + // Get the relay public key for decryption + const relayPubkey = getRelayPubkey(); + + // Use NIP-07 extension's NIP-44 decrypt method + if (!window.nostr || !window.nostr.nip44) { + throw new Error('NIP-44 decryption not available via NIP-07 extension'); + } + + const decryptedContent = await window.nostr.nip44.decrypt(relayPubkey, encryptedContent); + + if (!decryptedContent) { + throw new Error('NIP-44 decryption returned empty result'); + } + + console.log('Successfully decrypted content from relay'); + return decryptedContent; + + } catch (error) { + console.error('NIP-44 decryption failed:', error); + throw error; + } +} + +// Fetch configuration using admin API via HTTP POST +async function fetchConfiguration() { + try { + console.log('=== FETCHING CONFIGURATION VIA HTTP POST ==='); + + // Require login + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to fetch configuration'); + } + + // Ensure relay pubkey is available + if (!relayPubkey) { + throw new Error('Relay pubkey not available'); + } + + console.log('Sending config query command via HTTP POST...'); + + // Send config_query command via HTTP POST + const responseData = await sendAdminCommandHTTP(['config_query', 'all']); + + // Process the response + handleConfigQueryResponse(responseData); + + console.log('Configuration fetched successfully'); + return true; + + } catch (error) { + console.error('Failed to fetch configuration:', error); + log(`Failed to fetch configuration: ${error.message}`, 'ERROR'); + return false; + } +} + +function displayConfiguration(event) { + try { + console.log('=== DISPLAYING CONFIGURATION EVENT ==='); + console.log('Event received for display:', event); + + currentConfig = event; + + // Clear existing table + configTableBody.innerHTML = ''; + + // Display tags (editable configuration parameters only) + console.log(`Processing ${event.tags.length} configuration parameters`); + event.tags.forEach((tag, index) => { + if (tag.length >= 2) { + const row = document.createElement('tr'); + const key = tag[0]; + const value = tag[1]; + + // Create editable input for value + const valueInput = document.createElement('input'); + valueInput.type = 'text'; + valueInput.value = value; + valueInput.className = 'config-value-input'; + valueInput.dataset.key = key; + valueInput.dataset.originalValue = value; + valueInput.dataset.rowIndex = index; + + // Create clickable Actions cell + const actionsCell = document.createElement('td'); + actionsCell.className = 'config-actions-cell'; + actionsCell.textContent = 'SAVE'; + actionsCell.dataset.key = key; + actionsCell.dataset.originalValue = value; + actionsCell.dataset.rowIndex = index; + + // Initially hide the SAVE text + actionsCell.style.color = 'transparent'; + + // Show SAVE text and make clickable when value changes + valueInput.addEventListener('input', function () { + if (this.value !== this.dataset.originalValue) { + actionsCell.style.color = 'var(--primary-color)'; + actionsCell.style.cursor = 'pointer'; + actionsCell.onclick = () => saveIndividualConfig(key, valueInput.value, valueInput.dataset.originalValue, actionsCell); + } else { + actionsCell.style.color = 'transparent'; + actionsCell.style.cursor = 'default'; + actionsCell.onclick = null; + } + }); + + row.innerHTML = `${key}`; + row.cells[1].appendChild(valueInput); + row.appendChild(actionsCell); + configTableBody.appendChild(row); + } + }); + + // Show message if no configuration parameters found + if (event.tags.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = `No configuration parameters found`; + configTableBody.appendChild(row); + } + + console.log('Configuration display completed successfully'); + updateConfigStatus(true); + + } catch (error) { + console.error('Error in displayConfiguration:', error.message); + console.error('Display configuration error:', error); + } +} + +// Save individual configuration parameter +async function saveIndividualConfig(key, newValue, originalValue, actionsCell) { + if (!isLoggedIn || !userPubkey) { + log('Must be logged in to save configuration', 'ERROR'); + return; + } + + if (!currentConfig) { + log('No current configuration to update', 'ERROR'); + return; + } + + // Don't save if value hasn't changed + if (newValue === originalValue) { + return; + } + + try { + log(`Saving individual config: ${key} = ${newValue}`, 'INFO'); + + // Determine data type based on key name + let dataType = 'string'; + if (['max_connections', 'pow_min_difficulty', 'nip42_challenge_timeout', 'max_subscriptions_per_client', 'max_event_tags', 'max_content_length'].includes(key)) { + dataType = 'integer'; + } else if (['auth_enabled', 'nip42_auth_required', 'nip40_expiration_enabled'].includes(key)) { + dataType = 'boolean'; + } + + // Determine category based on key name + let category = 'general'; + if (key.startsWith('relay_')) { + category = 'relay'; + } else if (key.startsWith('nip40_')) { + category = 'expiration'; + } else if (key.startsWith('nip42_') || key.startsWith('auth_')) { + category = 'authentication'; + } else if (key.startsWith('pow_')) { + category = 'proof_of_work'; + } else if (key.startsWith('max_')) { + category = 'limits'; + } + + const configObj = { + key: key, + value: newValue, + data_type: dataType, + category: category + }; + + // Update cell during save + actionsCell.textContent = 'SAVING...'; + actionsCell.style.color = 'var(--accent-color)'; + actionsCell.style.cursor = 'not-allowed'; + actionsCell.onclick = null; + + // Send single config update + await sendConfigUpdateCommand([configObj]); + + // Update the original value on success + const input = actionsCell.parentElement.cells[1].querySelector('input'); + if (input) { + input.dataset.originalValue = newValue; + // Hide SAVE text since value now matches original + actionsCell.style.color = 'transparent'; + actionsCell.style.cursor = 'default'; + actionsCell.onclick = null; + } + + actionsCell.textContent = 'SAVED'; + actionsCell.style.color = 'var(--accent-color)'; + setTimeout(() => { + actionsCell.textContent = 'SAVE'; + // Keep transparent if value matches original + if (input && input.value === input.dataset.originalValue) { + actionsCell.style.color = 'transparent'; + } + }, 2000); + + log(`Successfully saved config: ${key} = ${newValue}`, 'INFO'); + + } catch (error) { + log(`Failed to save individual config ${key}: ${error.message}`, 'ERROR'); + actionsCell.textContent = 'SAVE'; + actionsCell.style.color = 'var(--primary-color)'; + actionsCell.style.cursor = 'pointer'; + actionsCell.onclick = () => saveIndividualConfig(key, actionsCell.parentElement.cells[1].querySelector('input').value, originalValue, actionsCell); + } +} + + + + +// Send config update command using kind 23456 with Administrator API (inner events) +async function sendConfigUpdateCommand(configObjects) { + try { + if (!relayPool) { + throw new Error('SimplePool connection not available'); + } + + console.log(`Sending config_update command with ${configObjects.length} configuration object(s)`); + + // Create command array for config update + const command_array = ["config_update", configObjects]; + + // Encrypt the command array directly using NIP-44 + const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); + if (!encrypted_content) { + throw new Error('Failed to encrypt command array'); + } + + // Create single kind 23456 admin event + const configEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", getRelayPubkey()]], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(configEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + console.log(`Config update event signed with ${configObjects.length} object(s)`); + + // Publish via SimplePool with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + console.log(`✅ Config Update Relay ${index} (${url}): Event published successfully`); + if (typeof logTestEvent === 'function') { + logTestEvent('INFO', `Config update relay ${index} publish success`, 'PUBLISH'); + } + } else { + console.error(`❌ Config Update Relay ${index} (${url}): Publish failed:`, result.reason); + if (typeof logTestEvent === 'function') { + logTestEvent('ERROR', `Config update relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected config update event. Details: ${errorDetails}`); + } + + console.log(`Config update command sent successfully with ${configObjects.length} configuration object(s)`); + + // Log for testing + if (typeof logTestEvent === 'function') { + logTestEvent('SENT', `Config update command: ${configObjects.length} object(s)`, 'CONFIG_UPDATE'); + configObjects.forEach((config, index) => { + logTestEvent('SENT', `Config ${index + 1}: ${config.key} = ${config.value} (${config.data_type})`, 'CONFIG'); + }); + } + + } catch (error) { + console.error(`Failed to send config_update command:`, error); + throw error; + } +} + + + +// Profile area click handler removed - dropdown moved to sidebar +// Logout and dark mode buttons are now in the sidebar footer + +// Initialize relay pubkey container click handler for clipboard copy +const relayPubkeyContainer = document.getElementById('relay-pubkey-container'); +if (relayPubkeyContainer) { + relayPubkeyContainer.addEventListener('click', async function() { + const relayPubkeyElement = document.getElementById('relay-pubkey'); + if (relayPubkeyElement && relayPubkeyElement.textContent !== 'Loading...') { + try { + // Get the full npub (remove all whitespace for continuous string) + const fullNpub = relayPubkeyElement.textContent.replace(/\s/g, ''); + + await navigator.clipboard.writeText(fullNpub); + + // Add copied class for visual feedback + relayPubkeyContainer.classList.add('copied'); + + // Remove the class after animation completes + setTimeout(() => { + relayPubkeyContainer.classList.remove('copied'); + }, 500); + + log('Relay npub copied to clipboard', 'INFO'); + } catch (error) { + log('Failed to copy relay npub to clipboard', 'ERROR'); + } + } + }); +} + +// Event handlers +fetchConfigBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + fetchConfiguration().catch(error => { + console.log('Manual fetch configuration failed: ' + error.message); + }); +}); + + + + + +// ================================ +// AUTH RULES MANAGEMENT FUNCTIONS +// ================================ + +// Global auth rules state +let currentAuthRules = []; +let editingAuthRule = null; + +// DOM elements for auth rules +const authRulesSection = document.getElementById('authRulesSection'); +const refreshAuthRulesBtn = document.getElementById('refreshAuthRulesBtn'); +const authRulesTableContainer = document.getElementById('authRulesTableContainer'); +const authRulesTableBody = document.getElementById('authRulesTableBody'); +const authRuleFormContainer = document.getElementById('authRuleFormContainer'); +const authRuleForm = document.getElementById('authRuleForm'); +const authRuleFormTitle = document.getElementById('authRuleFormTitle'); +const saveAuthRuleBtn = document.getElementById('saveAuthRuleBtn'); +const cancelAuthRuleBtn = document.getElementById('cancelAuthRuleBtn'); + +// Show auth rules section after login +function showAuthRulesSection() { + if (authRulesSection) { + authRulesSection.style.display = 'block'; + updateAuthRulesStatus('ready'); + log('Auth rules section is now available', 'INFO'); + } +} + +// Hide auth rules section on logout +function hideAuthRulesSection() { + if (authRulesSection) { + authRulesSection.style.display = 'none'; + + // Add null checks for all elements + if (authRulesTableContainer) { + authRulesTableContainer.style.display = 'none'; + } + if (authRuleFormContainer) { + authRuleFormContainer.style.display = 'none'; + } + + currentAuthRules = []; + editingAuthRule = null; + log('Auth rules section hidden', 'INFO'); + } +} + +// Update auth rules status indicator (removed - no status element) +function updateAuthRulesStatus(status) { + // Status element removed - no-op +} + +// Load auth rules from relay using admin API via HTTP POST +async function loadAuthRules() { + try { + log('Loading auth rules via HTTP POST...', 'INFO'); + updateAuthRulesStatus('loading'); + + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to load auth rules'); + } + + if (!relayPubkey) { + throw new Error('Relay pubkey not available'); + } + + log('Sending auth rules query via HTTP POST...', 'INFO'); + + // Send auth_query command via HTTP POST + const responseData = await sendAdminCommandHTTP(['auth_query', 'all']); + + // Process the response + handleAuthQueryResponse(responseData); + + log('Auth rules loaded successfully', 'INFO'); + updateAuthRulesStatus('loaded'); + + } catch (error) { + log(`Failed to load auth rules: ${error.message}`, 'ERROR'); + updateAuthRulesStatus('error'); + currentAuthRules = []; + displayAuthRules(currentAuthRules); + } +} + +// Display auth rules in the table +function displayAuthRules(rules) { + console.log('=== DISPLAY AUTH RULES DEBUG ==='); + console.log('authRulesTableBody element:', authRulesTableBody); + console.log('Rules to display:', rules); + console.log('Rules length:', rules ? rules.length : 'undefined'); + console.log('authRulesTableContainer display:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found'); + + if (!authRulesTableBody) { + console.log('ERROR: authRulesTableBody element not found'); + return; + } + + authRulesTableBody.innerHTML = ''; + console.log('Cleared existing table content'); + + if (!rules || rules.length === 0) { + console.log('No rules to display, showing empty message'); + const row = document.createElement('tr'); + row.innerHTML = `No auth rules configured`; + authRulesTableBody.appendChild(row); + console.log('Added empty rules message row'); + return; + } + + console.log(`Displaying ${rules.length} auth rules`); + rules.forEach((rule, index) => { + console.log(`Adding rule ${index + 1}:`, rule); + const row = document.createElement('tr'); + + // Convert hex pubkey to npub for display in pattern_value + let displayPatternValue = rule.pattern_value || rule.rule_target || '-'; + let patternValueLink = displayPatternValue; + try { + if (rule.pattern_value && rule.pattern_value.length === 64 && /^[0-9a-fA-F]+$/.test(rule.pattern_value)) { + const npub = window.NostrTools.nip19.npubEncode(rule.pattern_value); + displayPatternValue = npub; + patternValueLink = `${npub}`; + } + } catch (error) { + console.log('Failed to encode pattern_value to npub:', error.message); + } + + row.innerHTML = ` + ${rule.rule_type} + ${rule.pattern_type || rule.operation || '-'} + ${patternValueLink} + ${rule.enabled !== false ? 'Active' : 'Inactive'} + +
+ + +
+ + `; + authRulesTableBody.appendChild(row); + }); + + // Update status display + console.log(`Total Rules: ${rules.length}, Active Rules: ${rules.filter(r => r.enabled !== false).length}`); + + console.log('=== END DISPLAY AUTH RULES DEBUG ==='); +} + +// Show auth rules table (automatically called when auth rules are loaded) +function showAuthRulesTable() { + console.log('=== SHOW AUTH RULES TABLE DEBUG ==='); + console.log('authRulesTableContainer element:', authRulesTableContainer); + console.log('Current display style:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found'); + + if (authRulesTableContainer) { + authRulesTableContainer.style.display = 'block'; + console.log('Set authRulesTableContainer display to block'); + + // If we already have cached auth rules, display them immediately + if (currentAuthRules && currentAuthRules.length >= 0) { + console.log('Displaying cached auth rules:', currentAuthRules.length, 'rules'); + displayAuthRules(currentAuthRules); + updateAuthRulesStatus('loaded'); + log(`Auth rules table displayed with ${currentAuthRules.length} cached rules`, 'INFO'); + } else { + // No cached rules, load from relay + console.log('No cached auth rules, loading from relay...'); + loadAuthRules(); + log('Auth rules table displayed - loading from relay', 'INFO'); + } + } else { + console.log('ERROR: authRulesTableContainer element not found'); + } + console.log('=== END SHOW AUTH RULES TABLE DEBUG ==='); +} + +// Show add auth rule form +function showAddAuthRuleForm() { + if (authRuleFormContainer && authRuleFormTitle) { + editingAuthRule = null; + authRuleFormTitle.textContent = 'Add Auth Rule'; + authRuleForm.reset(); + authRuleFormContainer.style.display = 'block'; + log('Opened add auth rule form', 'INFO'); + } +} + +// Show edit auth rule form +function editAuthRule(index) { + if (index < 0 || index >= currentAuthRules.length) return; + + const rule = currentAuthRules[index]; + editingAuthRule = { ...rule, index: index }; + + if (authRuleFormTitle && authRuleForm) { + authRuleFormTitle.textContent = 'Edit Auth Rule'; + + // Populate form fields + document.getElementById('authRuleType').value = rule.rule_type || ''; + document.getElementById('authPatternType').value = rule.pattern_type || rule.operation || ''; + document.getElementById('authPatternValue').value = rule.pattern_value || rule.rule_target || ''; + document.getElementById('authRuleAction').value = rule.action || 'allow'; + document.getElementById('authRuleDescription').value = rule.description || ''; + + authRuleFormContainer.style.display = 'block'; + log(`Editing auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); + } +} + +// Delete auth rule using Administrator API (inner events) +async function deleteAuthRule(index) { + if (index < 0 || index >= currentAuthRules.length) return; + + const rule = currentAuthRules[index]; + const confirmMsg = `Delete auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}?`; + + if (!confirm(confirmMsg)) return; + + try { + log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); + + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to delete auth rules'); + } + + if (!relayPool) { + throw new Error('SimplePool connection not available'); + } + + // Create command array for deleting auth rule + // Format: ["system_command", "delete_auth_rule", rule_type, pattern_type, pattern_value] + const rule_type = rule.rule_type; + const pattern_type = rule.pattern_type || 'pubkey'; + const pattern_value = rule.pattern_value || rule.rule_target; + + const command_array = ["system_command", "delete_auth_rule", rule_type, pattern_type, pattern_value]; + + // Encrypt the command array directly using NIP-44 + const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); + if (!encrypted_content) { + throw new Error('Failed to encrypt command array'); + } + + // Create single kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", getRelayPubkey()]], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + log('Sending delete auth rule command to relay...', 'INFO'); + + // Publish via SimplePool with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + console.log(`✅ Delete Auth Rule Relay ${index} (${url}): Event published successfully`); + if (typeof logTestEvent === 'function') { + logTestEvent('INFO', `Delete auth rule relay ${index} publish success`, 'PUBLISH'); + } + } else { + console.error(`❌ Delete Auth Rule Relay ${index} (${url}): Publish failed:`, result.reason); + if (typeof logTestEvent === 'function') { + logTestEvent('ERROR', `Delete auth rule relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected delete auth rule event. Details: ${errorDetails}`); + } + + log('Delete auth rule command sent successfully - waiting for response...', 'INFO'); + + // Remove from local array immediately for UI responsiveness + currentAuthRules.splice(index, 1); + displayAuthRules(currentAuthRules); + + } catch (error) { + log(`Failed to delete auth rule: ${error.message}`, 'ERROR'); + } +} + +// Hide auth rule form +function hideAuthRuleForm() { + if (authRuleFormContainer) { + authRuleFormContainer.style.display = 'none'; + editingAuthRule = null; + log('Auth rule form hidden', 'INFO'); + } +} + +// Validate auth rule form +function validateAuthRuleForm() { + const ruleType = document.getElementById('authRuleType').value; + const patternType = document.getElementById('authPatternType').value; + const patternValue = document.getElementById('authPatternValue').value.trim(); + const action = document.getElementById('authRuleAction').value; + + if (!ruleType) { + alert('Please select a rule type'); + return false; + } + + if (!patternType) { + alert('Please select a pattern type'); + return false; + } + + if (!patternValue) { + alert('Please enter a pattern value'); + return false; + } + + if (!action) { + alert('Please select an action'); + return false; + } + + // Validate pubkey format for pubkey rules + if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist') && + patternValue.length !== 64) { + alert('Pubkey must be exactly 64 hex characters'); + return false; + } + + // Validate hex format for pubkey rules + if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist')) { + const hexPattern = /^[0-9a-fA-F]+$/; + if (!hexPattern.test(patternValue)) { + alert('Pubkey must contain only hex characters (0-9, a-f, A-F)'); + return false; + } + } + + return true; +} + +// Save auth rule (add or update) +async function saveAuthRule(event) { + event.preventDefault(); + + if (!validateAuthRuleForm()) return; + + try { + const ruleData = { + rule_type: document.getElementById('authRuleType').value, + pattern_type: document.getElementById('authPatternType').value, + pattern_value: document.getElementById('authPatternValue').value.trim(), + action: document.getElementById('authRuleAction').value, + description: document.getElementById('authRuleDescription').value.trim() || null, + enabled: true + }; + + if (editingAuthRule) { + log(`Updating auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); + + // TODO: Implement actual rule update via WebSocket kind 23456 event + // For now, just update local array + currentAuthRules[editingAuthRule.index] = { ...ruleData, id: editingAuthRule.id || Date.now() }; + + log('Auth rule updated (placeholder implementation)', 'INFO'); + } else { + log(`Adding new auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); + + // TODO: Implement actual rule creation via WebSocket kind 23456 event + // For now, just add to local array + currentAuthRules.push({ ...ruleData, id: Date.now() }); + + log('Auth rule added (placeholder implementation)', 'INFO'); + } + + displayAuthRules(currentAuthRules); + hideAuthRuleForm(); + + } catch (error) { + log(`Failed to save auth rule: ${error.message}`, 'ERROR'); + } +} + +// Monitoring is now subscription-based - no auto-enable needed +// Monitoring automatically activates when someone subscribes to kind 24567 events +async function autoEnableMonitoring() { + log('Monitoring system is subscription-based - no manual enable needed', 'INFO'); + log('Subscribe to kind 24567 events to receive real-time monitoring data', 'INFO'); +} + +// Update existing logout and showMainInterface functions to handle auth rules and NIP-17 DMs +const originalLogout = logout; +logout = async function () { + hideAuthRulesSection(); + // Clear DM inbox and outbox on logout + if (dmInbox) { + dmInbox.innerHTML = '
No messages received yet.
'; + } + if (dmOutbox) { + dmOutbox.value = ''; + } + await originalLogout(); +}; + +const originalShowMainInterface = showMainInterface; +showMainInterface = function () { + originalShowMainInterface(); + // Removed showAuthRulesSection() call - visibility now handled by updateAdminSectionsVisibility() +}; + +// Auth rules event handlers +if (refreshAuthRulesBtn) { + refreshAuthRulesBtn.addEventListener('click', function (e) { + e.preventDefault(); + loadAuthRules(); + }); +} + +if (authRuleForm) { + authRuleForm.addEventListener('submit', saveAuthRule); +} + +if (cancelAuthRuleBtn) { + cancelAuthRuleBtn.addEventListener('click', function (e) { + e.preventDefault(); + hideAuthRuleForm(); + }); +} + +// ================================ +// STREAMLINED AUTH RULE FUNCTIONS +// ================================ + +// Utility function to convert nsec to hex pubkey or npub to hex pubkey +function nsecToHex(input) { + if (!input || input.trim().length === 0) { + return null; + } + + const trimmed = input.trim(); + + // If it's already 64-char hex, return as-is + if (/^[0-9a-fA-F]{64}$/.test(trimmed)) { + return trimmed; + } + + // If it starts with nsec1, try to decode + if (trimmed.startsWith('nsec1')) { + try { + if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.decode) { + const decoded = window.NostrTools.nip19.decode(trimmed); + if (decoded.type === 'nsec') { + // Handle different versions of nostr-tools + if (typeof decoded.data === 'string') { + // v1 style - data is already hex + return decoded.data; + } else { + // v2 style - data is Uint8Array + const hexPubkey = Array.from(decoded.data) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + return hexPubkey; + } + } + } + } catch (error) { + console.error('Failed to decode nsec:', error); + return null; + } + } + + // If it starts with npub1, try to decode to hex + if (trimmed.startsWith('npub1')) { + try { + if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.decode) { + const decoded = window.NostrTools.nip19.decode(trimmed); + if (decoded.type === 'npub') { + // Handle different versions of nostr-tools + if (typeof decoded.data === 'string') { + // v1 style - data is already hex + return decoded.data; + } else { + // v2 style - data is Uint8Array + const hexPubkey = Array.from(decoded.data) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + return hexPubkey; + } + } + } + } catch (error) { + console.error('Failed to decode npub:', error); + return null; + } + } + + return null; // Invalid format +} + +// Add blacklist rule (updated to use combined input) +function addBlacklistRule() { + const input = document.getElementById('authRulePubkey'); + + if (!input) return; + + const inputValue = input.value.trim(); + if (!inputValue) { + log('Please enter a pubkey or nsec', 'ERROR'); + return; + } + + // Convert nsec or npub to hex if needed + const hexPubkey = nsecToHex(inputValue); + if (!hexPubkey) { + log('Invalid pubkey format. Please enter nsec1..., npub1..., or 64-character hex', 'ERROR'); + return; + } + + // Validate hex length + if (hexPubkey.length !== 64) { + log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR'); + return; + } + + log('Adding to blacklist...', 'INFO'); + + // Create auth rule data + const ruleData = { + rule_type: 'pubkey_blacklist', + pattern_type: 'Global', + pattern_value: hexPubkey, + action: 'deny' + }; + + // Add to WebSocket queue for processing + addAuthRuleViaWebSocket(ruleData) + .then(() => { + log(`Pubkey ${hexPubkey.substring(0, 16)}... added to blacklist`, 'INFO'); + input.value = ''; + + // Refresh auth rules display if visible + if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { + loadAuthRules(); + } + }) + .catch(error => { + log(`Failed to add rule: ${error.message}`, 'ERROR'); + }); +} + +// Add whitelist rule (updated to use combined input) +function addWhitelistRule() { + const input = document.getElementById('authRulePubkey'); + const warningDiv = document.getElementById('whitelistWarning'); + + if (!input) return; + + const inputValue = input.value.trim(); + if (!inputValue) { + log('Please enter a pubkey or nsec', 'ERROR'); + return; + } + + // Convert nsec or npub to hex if needed + const hexPubkey = nsecToHex(inputValue); + if (!hexPubkey) { + log('Invalid pubkey format. Please enter nsec1..., npub1..., or 64-character hex', 'ERROR'); + return; + } + + // Validate hex length + if (hexPubkey.length !== 64) { + log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR'); + return; + } + + // Show whitelist warning + if (warningDiv) { + warningDiv.style.display = 'block'; + } + + log('Adding to whitelist...', 'INFO'); + + // Create auth rule data + const ruleData = { + rule_type: 'pubkey_whitelist', + pattern_type: 'Global', + pattern_value: hexPubkey, + action: 'allow' + }; + + // Add to WebSocket queue for processing + addAuthRuleViaWebSocket(ruleData) + .then(() => { + log(`Pubkey ${hexPubkey.substring(0, 16)}... added to whitelist`, 'INFO'); + input.value = ''; + + // Refresh auth rules display if visible + if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { + loadAuthRules(); + } + }) + .catch(error => { + log(`Failed to add rule: ${error.message}`, 'ERROR'); + }); +} + +// Add auth rule via SimplePool (kind 23456 event) - FIXED to match working test pattern +async function addAuthRuleViaWebSocket(ruleData) { + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to add auth rules'); + } + + if (!relayPool) { + throw new Error('SimplePool connection not available'); + } + + try { + log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO'); + + // Map client-side rule types to command array format (matching working tests) + let commandRuleType, commandPatternType; + + switch (ruleData.rule_type) { + case 'pubkey_blacklist': + commandRuleType = 'blacklist'; + commandPatternType = 'pubkey'; + break; + case 'pubkey_whitelist': + commandRuleType = 'whitelist'; + commandPatternType = 'pubkey'; + break; + case 'hash_blacklist': + commandRuleType = 'blacklist'; + commandPatternType = 'hash'; + break; + default: + throw new Error(`Unknown rule type: ${ruleData.rule_type}`); + } + + // Create command array in the same format as working tests + // Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."] + const command_array = [commandRuleType, commandPatternType, ruleData.pattern_value]; + + // Encrypt the command array directly using NIP-44 + const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); + if (!encrypted_content) { + throw new Error('Failed to encrypt command array'); + } + + // Create single kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", getRelayPubkey()]], + content: encrypted_content + }; + + // DEBUG: Log the complete event structure being sent + console.log('=== AUTH RULE EVENT DEBUG (Administrator API) ==='); + console.log('Original Rule Data:', ruleData); + console.log('Command Array:', command_array); + console.log('Encrypted Content:', encrypted_content.substring(0, 50) + '...'); + console.log('Auth Event (before signing):', JSON.stringify(authEvent, null, 2)); + console.log('=== END AUTH RULE EVENT DEBUG ==='); + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + // Publish via SimplePool with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + console.log(`✅ Add Auth Rule Relay ${index} (${url}): Event published successfully`); + if (typeof logTestEvent === 'function') { + logTestEvent('INFO', `Add auth rule relay ${index} publish success`, 'PUBLISH'); + } + } else { + console.error(`❌ Add Auth Rule Relay ${index} (${url}): Publish failed:`, result.reason); + if (typeof logTestEvent === 'function') { + logTestEvent('ERROR', `Add auth rule relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected add auth rule event. Details: ${errorDetails}`); + } + + log('Auth rule added successfully', 'INFO'); + + } catch (error) { + log(`Failed to add auth rule: ${error.message}`, 'ERROR'); + throw error; + } +} + +// ================================ +// TEST FUNCTIONS FOR ADMIN API +// ================================ + +// Test event logging function +function logTestEvent(direction, message, type = 'INFO') { + const testLog = document.getElementById('test-event-log'); + if (!testLog) return; + + const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; + const logEntry = document.createElement('div'); + logEntry.className = 'log-entry'; + + const directionColor = direction === 'SENT' ? '#007bff' : '#28a745'; + logEntry.innerHTML = ` + ${timestamp} + [${direction}] + [${type}] + ${message} + `; + + testLog.appendChild(logEntry); + testLog.scrollTop = testLog.scrollHeight; +} + +// Test: Get Auth Rules +async function testGetAuthRules() { + if (!isLoggedIn || !userPubkey) { + logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); + return; + } + + if (!relayPool) { + logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); + return; + } + + try { + logTestEvent('INFO', 'Testing Get Auth Rules command...', 'TEST'); + + // Create command array for getting auth rules + const command_array = '["auth_query", "all"]'; + + // Encrypt the command content using NIP-44 + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt auth query command'); + } + + // Create kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["p", getRelayPubkey()] + ], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + logTestEvent('SENT', `Get Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT'); + + // Publish via SimplePool with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + logTestEvent('INFO', `Test Add Blacklist relay ${index} publish success`, 'PUBLISH'); + } else { + logTestEvent('ERROR', `Test Add Blacklist relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected test add blacklist event. Details: ${errorDetails}`); + } + + logTestEvent('INFO', 'Get Auth Rules command sent successfully', 'SUCCESS'); + + } catch (error) { + logTestEvent('ERROR', `Get Auth Rules test failed: ${error.message}`, 'ERROR'); + } +} + +// Test: Clear All Auth Rules +async function testClearAuthRules() { + if (!isLoggedIn || !userPubkey) { + logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); + return; + } + + if (!relayPool) { + logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); + return; + } + + try { + logTestEvent('INFO', 'Testing Clear All Auth Rules command...', 'TEST'); + + // Create command array for clearing auth rules + const command_array = '["system_command", "clear_all_auth_rules"]'; + + // Encrypt the command content using NIP-44 + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt clear auth rules command'); + } + + // Create kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["p", getRelayPubkey()] + ], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + logTestEvent('SENT', `Clear Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT'); + + // Publish via SimplePool with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + logTestEvent('INFO', `Test Add Whitelist relay ${index} publish success`, 'PUBLISH'); + } else { + logTestEvent('ERROR', `Test Add Whitelist relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected test add whitelist event. Details: ${errorDetails}`); + } + + logTestEvent('INFO', 'Clear Auth Rules command sent successfully', 'SUCCESS'); + + } catch (error) { + logTestEvent('ERROR', `Clear Auth Rules test failed: ${error.message}`, 'ERROR'); + } +} + +// Test: Add Blacklist +async function testAddBlacklist() { + const testPubkeyInput = document.getElementById('test-pubkey-input'); + let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : ''; + + // Use a default test pubkey if none provided + if (!testPubkey) { + testPubkey = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO'); + } + + if (!isLoggedIn || !userPubkey) { + logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); + return; + } + + if (!relayPool) { + logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); + return; + } + + try { + logTestEvent('INFO', `Testing Add Blacklist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST'); + + // Create command array for adding blacklist rule + const command_array = `["blacklist", "pubkey", "${testPubkey}"]`; + + // Encrypt the command content using NIP-44 + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt blacklist command'); + } + + // Create kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["p", getRelayPubkey()] + ], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + logTestEvent('SENT', `Add Blacklist event: ${JSON.stringify(signedEvent)}`, 'EVENT'); + + // Publish via SimplePool with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + logTestEvent('INFO', `Test Config Query relay ${index} publish success`, 'PUBLISH'); + } else { + logTestEvent('ERROR', `Test Config Query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected test config query event. Details: ${errorDetails}`); + } + + logTestEvent('INFO', 'Add Blacklist command sent successfully', 'SUCCESS'); + + } catch (error) { + logTestEvent('ERROR', `Add Blacklist test failed: ${error.message}`, 'ERROR'); + } +} + +// Test: Add Whitelist +async function testAddWhitelist() { + const testPubkeyInput = document.getElementById('test-pubkey-input'); + let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : ''; + + // Use a default test pubkey if none provided + if (!testPubkey) { + testPubkey = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO'); + } + + if (!isLoggedIn || !userPubkey) { + logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); + return; + } + + if (!relayPool) { + logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); + return; + } + + try { + logTestEvent('INFO', `Testing Add Whitelist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST'); + + // Create command array for adding whitelist rule + const command_array = `["whitelist", "pubkey", "${testPubkey}"]`; + + // Encrypt the command content using NIP-44 + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt whitelist command'); + } + + // Create kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["p", getRelayPubkey()] + ], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT'); + + // Publish via SimplePool + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + logTestEvent('INFO', `Test Post Event relay ${index} publish success`, 'PUBLISH'); + } else { + logTestEvent('ERROR', `Test Post Event relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected test post event. Details: ${errorDetails}`); + } + + logTestEvent('INFO', 'Add Whitelist command sent successfully', 'SUCCESS'); + + } catch (error) { + logTestEvent('ERROR', `Add Whitelist test failed: ${error.message}`, 'ERROR'); + } +} + +// Test: Config Query +async function testConfigQuery() { + if (!isLoggedIn || !userPubkey) { + logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); + return; + } + + if (!relayPool) { + logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); + return; + } + + try { + logTestEvent('INFO', 'Testing Config Query command...', 'TEST'); + + // Create command array for getting configuration + const command_array = '["config_query", "all"]'; + + // Encrypt the command content using NIP-44 + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt config query command'); + } + + // Create kind 23456 admin event + const configEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["p", getRelayPubkey()] + ], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(configEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + logTestEvent('SENT', `Config Query event: ${JSON.stringify(signedEvent)}`, 'EVENT'); + + // Publish via SimplePool with detailed error diagnostics + const url = relayUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + logTestEvent('INFO', `Test Config Query relay ${index} publish success`, 'PUBLISH'); + } else { + logTestEvent('ERROR', `Test Config Query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected test config query event. Details: ${errorDetails}`); + } + + logTestEvent('INFO', 'Config Query command sent successfully', 'SUCCESS'); + + } catch (error) { + logTestEvent('ERROR', `Config Query test failed: ${error.message}`, 'ERROR'); + } +} + +// Test: Post Basic Event +async function testPostEvent() { + if (!isLoggedIn || !userPubkey) { + logTestEvent('ERROR', 'Must be logged in to test event posting', 'ERROR'); + return; + } + + if (!relayPool) { + logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); + return; + } + + try { + logTestEvent('INFO', 'Testing basic event posting...', 'TEST'); + + // Create a simple kind 1 text note event + const testEvent = { + kind: 1, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["t", "test"], + ["client", "c-relay-admin-api"] + ], + content: `Test event from C-Relay Admin API at ${new Date().toISOString()}` + }; + + logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT'); + + // Sign the event using NIP-07 + const signedEvent = await window.nostr.signEvent(testEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT'); + + // Publish via SimplePool to the same relay with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO'); + + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + logTestEvent('INFO', `Test Post Event relay ${index} publish success`, 'PUBLISH'); + } else { + logTestEvent('ERROR', `Test Post Event relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); + } + }); + + // Throw error if all relays failed + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected test post event. Details: ${errorDetails}`); + } + + logTestEvent('INFO', 'Test event published successfully!', 'SUCCESS'); + logTestEvent('INFO', 'Check if the event appears in the subscription above...', 'INFO'); + + } catch (error) { + logTestEvent('ERROR', `Post Event test failed: ${error.message}`, 'ERROR'); + console.error('Post Event test error:', error); + } +} + +// Helper function to encrypt content for relay using NIP-44 +async function encryptForRelay(content) { + try { + console.log('Encrypting content for relay:', content.substring(0, 50) + '...'); + + // Get the relay public key for encryption + const relayPubkey = getRelayPubkey(); + + // Use NIP-07 extension's NIP-44 encrypt method + if (!window.nostr || !window.nostr.nip44) { + throw new Error('NIP-44 encryption not available via NIP-07 extension'); + } + + const encrypted_content = await window.nostr.nip44.encrypt(relayPubkey, content); + + if (!encrypted_content) { + throw new Error('NIP-44 encryption returned empty result'); + } + + console.log('Successfully encrypted content using NIP-44'); + return encrypted_content; + } catch (error) { + console.error('NIP-44 encryption failed:', error); + throw error; + } +} + +// Create Kind 23458 admin command event +async function createAdminCommandEvent(commandArray) { + try { + console.log('Creating Kind 23458 event for command:', commandArray); + + // Encrypt command array as JSON + const content = JSON.stringify(commandArray); + const encryptedContent = await encryptForRelay(content); + + // Create Kind 23458 event + const event = { + kind: 23458, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', getRelayPubkey()]], + content: encryptedContent + }; + + // Sign event using NIP-07 + if (!window.nostr || !window.nostr.signEvent) { + throw new Error('NIP-07 signing not available'); + } + + const signedEvent = await window.nostr.signEvent(event); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + console.log('Kind 23458 event created and signed'); + return signedEvent; + } catch (error) { + console.error('Failed to create admin command event:', error); + throw error; + } +} + +// Send admin command via HTTP POST to /api/admin +async function sendAdminCommandHTTP(commandArray) { + try { + console.log('Sending admin command via HTTP POST:', commandArray); + + // Create and sign Kind 23458 event + const event = await createAdminCommandEvent(commandArray); + + // Send via HTTP POST to /api/admin + const response = await fetch('/api/admin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(event) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + // Parse Kind 23459 response event + const responseEvent = await response.json(); + + // Verify it's a Kind 23459 response + if (responseEvent.kind !== 23459) { + throw new Error(`Expected Kind 23459 response, got Kind ${responseEvent.kind}`); + } + + // Decrypt response content + if (!window.nostr || !window.nostr.nip44) { + throw new Error('NIP-44 decryption not available'); + } + + const decryptedContent = await window.nostr.nip44.decrypt( + responseEvent.pubkey, + responseEvent.content + ); + + // Parse decrypted JSON response + const responseData = JSON.parse(decryptedContent); + console.log('Received and decrypted response:', responseData); + + return responseData; + } catch (error) { + console.error('Failed to send admin command via HTTP:', error); + throw error; + } +} + +// Send NIP-17 Direct Message to relay using NIP-59 layering +async function sendNIP17DM() { + if (!isLoggedIn || !userPubkey) { + log('Must be logged in to send DM', 'ERROR'); + return; + } + + if (!isRelayConnected || !relayPubkey) { + log('Must be connected to relay to send DM', 'ERROR'); + return; + } + + const message = dmOutbox.value.trim(); + if (!message) { + log('Please enter a message to send', 'ERROR'); + return; + } + + // Capability checks + if (!window.nostr || !window.nostr.nip44 || !window.nostr.signEvent) { + log('NIP-17 DMs require a NIP-07 extension with NIP-44 support', 'ERROR'); + alert('NIP-17 DMs require a NIP-07 extension with NIP-44 support. Please install and configure a compatible extension.'); + return; + } + + if (!window.NostrTools || !window.NostrTools.generateSecretKey || !window.NostrTools.getPublicKey || !window.NostrTools.finalizeEvent) { + log('NostrTools library not available for ephemeral key operations', 'ERROR'); + alert('NostrTools library not available. Please ensure nostr.bundle.js is loaded.'); + return; + } + + try { + log(`Sending NIP-17 DM to relay: ${message.substring(0, 50)}...`, 'INFO'); + + // Step 1: Build unsigned rumor (kind 14) + const rumor = { + kind: 14, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), // Canonical time for rumor + tags: [["p", relayPubkey]], + content: message + }; + // NOTE: Rumor remains unsigned per NIP-59 + + log('Rumor built (unsigned), creating seal...', 'INFO'); + + // Step 2: Create seal (kind 13) + const seal = { + kind: 13, + pubkey: userPubkey, + created_at: randomNow(), // Randomized to past for metadata protection + tags: [], // Empty tags per NIP-59 + content: await window.nostr.nip44.encrypt(relayPubkey, JSON.stringify(rumor)) + }; + + // Sign seal with long-term key + const signedSeal = await window.nostr.signEvent(seal); + if (!signedSeal || !signedSeal.sig) { + throw new Error('Failed to sign seal event'); + } + + log('Seal created and signed, creating gift wrap...', 'INFO'); + + // Step 3: Create gift wrap (kind 1059) with ephemeral key + const ephemeralPriv = window.NostrTools.generateSecretKey(); + const ephemeralPub = window.NostrTools.getPublicKey(ephemeralPriv); + + const giftWrap = { + kind: 1059, + pubkey: ephemeralPub, + created_at: randomNow(), // Randomized to past for metadata protection + tags: [["p", relayPubkey]], + content: await window.NostrTools.nip44.encrypt( + JSON.stringify(signedSeal), + window.NostrTools.nip44.getConversationKey(ephemeralPriv, relayPubkey) + ) + }; + + // Sign gift wrap with ephemeral key using finalizeEvent + const signedGiftWrap = window.NostrTools.finalizeEvent(giftWrap, ephemeralPriv); + if (!signedGiftWrap || !signedGiftWrap.sig) { + throw new Error('Failed to sign gift wrap event'); + } + + // DEBUG: Log NIP-17 event details when created + console.log('=== NIP-17 EVENT CREATED ==='); + console.log('Full event:', JSON.stringify(signedGiftWrap, null, 2)); + console.log('Timestamp:', signedGiftWrap.created_at); + console.log('Local date time:', new Date(signedGiftWrap.created_at * 1000).toLocaleString()); + console.log('=== END NIP-17 EVENT DEBUG ==='); + + log('NIP-17 DM event created and signed with ephemeral key, publishing...', 'INFO'); + + // Publish via SimplePool + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedGiftWrap); + + // Use Promise.allSettled to capture per-relay outcomes + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + log(`✅ NIP-17 DM published successfully to relay ${index}`, 'INFO'); + } else { + log(`❌ NIP-17 DM failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR'); + } + }); + + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected NIP-17 DM event. Details: ${errorDetails}`); + } + + // Clear the outbox and show success + dmOutbox.value = ''; + log('NIP-17 DM sent successfully', 'INFO'); + + // Add to inbox for display + addMessageToInbox('sent', message, new Date().toLocaleString()); + + } catch (error) { + log(`Failed to send NIP-17 DM: ${error.message}`, 'ERROR'); + } +} + +// Add message to inbox display +function addMessageToInbox(direction, message, timestamp, pubkey = null) { + if (!dmInbox) return; + + const messageDiv = document.createElement('div'); + messageDiv.className = 'log-entry'; + + const directionColor = direction === 'sent' ? '#007bff' : '#28a745'; + + // Convert newlines to
tags for proper HTML display + const formattedMessage = message.replace(/\n/g, '
'); + + // Add pubkey display for received messages + let pubkeyDisplay = ''; + if (pubkey && direction === 'received') { + try { + const npub = window.NostrTools.nip19.npubEncode(pubkey); + pubkeyDisplay = ` (${npub})`; + } catch (error) { + console.error('Failed to encode pubkey to npub:', error); + } + } + + messageDiv.innerHTML = ` + ${timestamp} + [${direction.toUpperCase()}] + ${formattedMessage}${pubkeyDisplay} + `; + + // Remove the "No messages received yet" placeholder if it exists + const placeholder = dmInbox.querySelector('.log-entry'); + if (placeholder && placeholder.textContent === 'No messages received yet.') { + dmInbox.innerHTML = ''; + } + + // Add new message at the top + dmInbox.insertBefore(messageDiv, dmInbox.firstChild); + + // Limit to last 50 messages + while (dmInbox.children.length > 50) { + dmInbox.removeChild(dmInbox.lastChild); + } +} + +// Update relay info in header +function updateRelayInfoInHeader() { + const relayNameElement = document.getElementById('relay-name'); + const relayPubkeyElement = document.getElementById('relay-pubkey'); + const relayDescriptionElement = document.getElementById('relay-description'); + + if (!relayNameElement || !relayPubkeyElement || !relayDescriptionElement) { + return; + } + + // Get relay info from NIP-11 data or use defaults + const relayInfo = getRelayInfo(); + const relayName = relayInfo.name || 'C-Relay'; + const relayDescription = relayInfo.description || 'Nostr Relay'; + + // Convert relay pubkey to npub + let relayNpub = 'Loading...'; + if (relayPubkey) { + try { + relayNpub = window.NostrTools.nip19.npubEncode(relayPubkey); + } catch (error) { + console.log('Failed to encode relay pubkey to npub:', error.message); + relayNpub = relayPubkey.substring(0, 16) + '...'; + } + } + + // Format npub into 3 lines of 21 characters each, with spaces dividing each line into 3 groups of 7 characters + let formattedNpub = relayNpub; + if (relayNpub.length === 63) { + const line1 = relayNpub.substring(0, 7) + ' ' + relayNpub.substring(7, 14) + ' ' + relayNpub.substring(14, 21); + const line2 = relayNpub.substring(21, 28) + ' ' + relayNpub.substring(28, 35) + ' ' + relayNpub.substring(35, 42); + const line3 = relayNpub.substring(42, 49) + ' ' + relayNpub.substring(49, 56) + ' ' + relayNpub.substring(56, 63); + formattedNpub = line1 + '\n' + line2 + '\n' + line3; + } + + relayNameElement.textContent = relayName; + relayPubkeyElement.textContent = formattedNpub; + relayDescriptionElement.textContent = relayDescription; +} + +// Global variable to store relay info from NIP-11 or config +let relayInfoData = null; + +// Helper function to get relay info from stored data +function getRelayInfo() { + // Return stored relay info if available, otherwise defaults + if (relayInfoData) { + return relayInfoData; + } + + // Default values + return { + name: 'C-Relay', + description: 'Nostr Relay', + pubkey: relayPubkey + }; +} + +// Update stored relay info when config is loaded +function updateStoredRelayInfo(configData) { + if (configData && configData.data) { + // Extract relay info from config data - handle both object and array formats + let relayName = 'C-Relay'; + let relayDescription = 'Nostr Relay'; + + if (Array.isArray(configData.data)) { + // Array format: [{key: 'x', value: 'y'}, ...] + relayName = configData.data.find(item => item.key === 'relay_name')?.value || 'C-Relay'; + relayDescription = configData.data.find(item => item.key === 'relay_description')?.value || 'Nostr Relay'; + } else { + // Object format: {key1: 'value1', key2: 'value2', ...} + relayName = configData.data.relay_name || 'C-Relay'; + relayDescription = configData.data.relay_description || 'Nostr Relay'; + } + + relayInfoData = { + name: relayName, + description: relayDescription, + pubkey: relayPubkey + }; + + // Update header immediately + updateRelayInfoInHeader(); + } +} + +// Helper function to get relay pubkey +function getRelayPubkey() { + // Use the dynamically fetched relay pubkey if available + if (relayPubkey) { + return relayPubkey; + } + + // No fallback - throw error if relay pubkey not available + throw new Error('Relay pubkey not available. Please connect to relay first.'); +} + +// Enhanced SimplePool message handler to capture test responses +function enhancePoolForTesting() { + // SimplePool handles message parsing automatically, so we just need to + // ensure our event handlers log appropriately. This is already done + // in the subscription onevent callback. + console.log('SimplePool enhanced for testing - automatic message handling enabled'); +} + +// Generate random test pubkey function +function generateRandomTestKey() { + // Generate 32 random bytes (64 hex characters) for a valid pubkey + const randomBytes = new Uint8Array(32); + crypto.getRandomValues(randomBytes); + + // Convert to hex string + const hexPubkey = Array.from(randomBytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + // Set the generated key in the input field + const testPubkeyInput = document.getElementById('test-pubkey-input'); + if (testPubkeyInput) { + testPubkeyInput.value = hexPubkey; + logTestEvent('INFO', `Generated random test pubkey: ${hexPubkey.substring(0, 16)}...`, 'KEYGEN'); + } + + return hexPubkey; +} + +// ================================ +// DATABASE STATISTICS FUNCTIONS +// ================================ + +// Send restart command to restart the relay using Administrator API +async function sendRestartCommand() { + if (!isLoggedIn || !userPubkey) { + log('Must be logged in to restart relay', 'ERROR'); + return; + } + + if (!relayPool) { + log('SimplePool connection not available', 'ERROR'); + return; + } + + try { + log('Sending restart command to relay...', 'INFO'); + + // Create command array for restart + const command_array = ["system_command", "restart"]; + + // Encrypt the command array directly using NIP-44 + const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); + if (!encrypted_content) { + throw new Error('Failed to encrypt command array'); + } + + // Create single kind 23456 admin event + const restartEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", getRelayPubkey()]], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(restartEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + // Publish via SimplePool + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes + const results = await Promise.allSettled(publishPromises); + + // Check if any relay accepted the event + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + log(`Restart command published successfully to relay ${index}`, 'INFO'); + } else { + log(`Restart command failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR'); + } + }); + + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected restart command. Details: ${errorDetails}`); + } + + log('Restart command sent successfully - relay should restart shortly...', 'INFO'); + + // Update connection status to indicate restart is in progress + updateRelayConnectionStatus('connecting'); + relayConnectionStatus.textContent = 'RESTARTING...'; + + // The relay will disconnect and need to be reconnected after restart + // This will be handled by the WebSocket disconnection event + + } catch (error) { + log(`Failed to send restart command: ${error.message}`, 'ERROR'); + updateRelayConnectionStatus('error'); + } +} + +// Send stats_query command to get database statistics using Administrator API (inner events) +async function sendStatsQuery() { + if (!isLoggedIn || !userPubkey) { + log('Must be logged in to query database statistics', 'ERROR'); + updateStatsStatus('error', 'Not logged in'); + return; + } + + if (!relayPool) { + log('SimplePool connection not available', 'ERROR'); + updateStatsStatus('error', 'No relay connection'); + return; + } + + try { + updateStatsStatus('loading', 'Querying database...'); + + // Create command array for stats query + const command_array = ["stats_query", "all"]; + + // Encrypt the command array directly using NIP-44 + const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); + if (!encrypted_content) { + throw new Error('Failed to encrypt command array'); + } + + // Create single kind 23456 admin event + const statsEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", getRelayPubkey()]], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(statsEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + log('Sending stats query command...', 'INFO'); + + // Publish via SimplePool + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes + const results = await Promise.allSettled(publishPromises); + + // Check if any relay accepted the event + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + log(`Stats query published successfully to relay ${index}`, 'INFO'); + } else { + log(`Stats query failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR'); + } + }); + + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected stats query event. Details: ${errorDetails}`); + } + + log('Stats query command sent successfully - waiting for response...', 'INFO'); + updateStatsStatus('waiting', 'Waiting for response...'); + + } catch (error) { + log(`Failed to send stats query: ${error.message}`, 'ERROR'); + updateStatsStatus('error', error.message); + } +} + +// Handle stats_query response and populate tables +function handleStatsQueryResponse(responseData) { + try { + log('Processing stats query response...', 'INFO'); + console.log('Stats response data:', responseData); + + if (responseData.query_type !== 'stats_query') { + log('Ignoring non-stats response', 'WARNING'); + return; + } + + // Populate overview table + populateStatsOverview(responseData); + + // Populate event kinds table + populateStatsKinds(responseData); + + // Populate time-based statistics + populateStatsTime(responseData); + + // Populate top pubkeys table + populateStatsPubkeys(responseData); + + updateStatsStatus('loaded'); + log('Database statistics updated successfully', 'INFO'); + + } catch (error) { + log(`Error processing stats response: ${error.message}`, 'ERROR'); + updateStatsStatus('error', 'Failed to process response'); + } +} + +// Update statistics display from real-time monitoring event +function updateStatsFromMonitoringEvent(monitoringData) { + try { + + if (monitoringData.data_type !== 'event_kinds') { + return; + } + + // Update total events count and track rate for chart + if (monitoringData.total_events !== undefined) { + const currentTotal = monitoringData.total_events; + updateStatsCell('total-events', currentTotal.toString()); + + // Calculate new events since last update for chart + if (previousTotalEvents > 0) { + const newEvents = currentTotal - previousTotalEvents; + if (newEvents > 0 && eventRateChart) { + console.log(`Adding ${newEvents} new events to rate chart (${currentTotal} - ${previousTotalEvents})`); + eventRateChart.addValue(newEvents); + } + } + + // Update previous total for next calculation + previousTotalEvents = currentTotal; + } + + // Update event kinds table with real-time data + if (monitoringData.kinds && Array.isArray(monitoringData.kinds)) { + populateStatsKindsFromMonitoring(monitoringData.kinds, monitoringData.total_events); + } + + } catch (error) { + log(`Error updating stats from monitoring event: ${error.message}`, 'ERROR'); + } +} + +// Update statistics display from time_stats monitoring event +function updateStatsFromTimeMonitoringEvent(monitoringData) { + try { + if (monitoringData.data_type !== 'time_stats') { + return; + } + + // Update time-based statistics table with real-time data + if (monitoringData.periods && Array.isArray(monitoringData.periods)) { + // Use the existing populateStatsTime function which expects the nested time_stats object + const timeStats = { last_24h: 0, last_7d: 0, last_30d: 0 }; + + // Extract values from periods array + monitoringData.periods.forEach(period => { + if (period.period === 'last_24h') timeStats.last_24h = period.count; + else if (period.period === 'last_7d') timeStats.last_7d = period.count; + else if (period.period === 'last_30d') timeStats.last_30d = period.count; + }); + + populateStatsTime({ time_stats: timeStats }); + } + + } catch (error) { + log(`Error updating time stats from monitoring event: ${error.message}`, 'ERROR'); + } +} + +// Update statistics display from top_pubkeys monitoring event +function updateStatsFromTopPubkeysMonitoringEvent(monitoringData) { + try { + if (monitoringData.data_type !== 'top_pubkeys') { + return; + } + + // Update top pubkeys table with real-time data + if (monitoringData.pubkeys && Array.isArray(monitoringData.pubkeys)) { + // Pass total_events from monitoring data to the function + populateStatsPubkeysFromMonitoring(monitoringData.pubkeys, monitoringData.total_events || 0); + } + + } catch (error) { + log(`Error updating top pubkeys from monitoring event: ${error.message}`, 'ERROR'); + } +} + + +// Update statistics display from subscription_details monitoring event +function updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData) { + try { + // DEBUG: Log every subscription_details event that arrives at the webpage + // console.log('subscription_details', JSON.stringify(monitoringData, null, 2)); + console.log('subscription_details decoded:', monitoringData); + + if (monitoringData.data_type !== 'subscription_details') { + return; + } + + // Update subscription details table with real-time data + if (monitoringData.data && Array.isArray(monitoringData.data.subscriptions)) { + populateSubscriptionDetailsTable(monitoringData.data.subscriptions); + } + + } catch (error) { + log(`Error updating subscription details from monitoring event: ${error.message}`, 'ERROR'); + } +} + +// Update statistics display from CPU metrics monitoring event +function updateStatsFromCpuMonitoringEvent(monitoringData) { + try { + if (monitoringData.data_type !== 'cpu_metrics') { + return; + } + + // Update CPU metrics in the database statistics table + if (monitoringData.process_id !== undefined) { + updateStatsCell('process-id', monitoringData.process_id.toString()); + } + + if (monitoringData.memory_usage_mb !== undefined) { + updateStatsCell('memory-usage', monitoringData.memory_usage_mb.toFixed(1) + ' MB'); + } + + if (monitoringData.current_cpu_core !== undefined) { + updateStatsCell('cpu-core', 'Core ' + monitoringData.current_cpu_core); + } + + // Calculate CPU usage percentage if we have the data + if (monitoringData.process_cpu_time !== undefined && monitoringData.system_cpu_time !== undefined) { + // For now, just show the raw process CPU time (simplified) + // In a real implementation, you'd calculate deltas over time + updateStatsCell('cpu-usage', monitoringData.process_cpu_time + ' ticks'); + } + + } catch (error) { + log(`Error updating CPU metrics from monitoring event: ${error.message}`, 'ERROR'); + } +} + +// Populate event kinds table from monitoring data +function populateStatsKindsFromMonitoring(kindsData, totalEvents) { + const tableBody = document.getElementById('stats-kinds-table-body'); + if (!tableBody) return; + + tableBody.innerHTML = ''; + + if (kindsData.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = 'No event data'; + tableBody.appendChild(row); + return; + } + + kindsData.forEach(kind => { + const row = document.createElement('tr'); + const percentage = totalEvents > 0 ? ((kind.count / totalEvents) * 100).toFixed(1) : '0.0'; + row.innerHTML = ` + ${kind.kind} + ${kind.count} + ${percentage}% + `; + tableBody.appendChild(row); + }); +} + +// Populate database overview table +function populateStatsOverview(data) { + if (!data) return; + + // Update individual cells with flash animation for changed values + updateStatsCell('db-size', data.database_size_bytes ? formatFileSize(data.database_size_bytes) : '-'); + updateStatsCell('total-events', data.total_events || '-'); + updateStatsCell('oldest-event', data.database_created_at ? formatTimestamp(data.database_created_at) : '-'); + updateStatsCell('newest-event', data.latest_event_at ? formatTimestamp(data.latest_event_at) : '-'); +} + +// Populate event kinds distribution table +function populateStatsKinds(data) { + const tableBody = document.getElementById('stats-kinds-table-body'); + if (!tableBody || !data.event_kinds) return; + + tableBody.innerHTML = ''; + + if (data.event_kinds.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = 'No event data'; + tableBody.appendChild(row); + return; + } + + data.event_kinds.forEach(kind => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${kind.kind} + ${kind.count} + ${kind.percentage}% + `; + tableBody.appendChild(row); + }); +} + +// Populate time-based statistics table +function populateStatsTime(data) { + if (!data) return; + + // Access the nested time_stats object from backend response + const timeStats = data.time_stats || {}; + + // Update cells with flash animation for changed values + updateStatsCell('events-24h', timeStats.last_24h || '0'); + updateStatsCell('events-7d', timeStats.last_7d || '0'); + updateStatsCell('events-30d', timeStats.last_30d || '0'); +} + +// Populate top pubkeys table +function populateStatsPubkeys(data) { + const tableBody = document.getElementById('stats-pubkeys-table-body'); + if (!tableBody || !data.top_pubkeys) return; + + tableBody.innerHTML = ''; + + if (data.top_pubkeys.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = 'No pubkey data'; + tableBody.appendChild(row); + return; + } + + data.top_pubkeys.forEach((pubkey, index) => { + const row = document.createElement('tr'); + // Convert hex pubkey to npub for display + let displayPubkey = pubkey.pubkey || '-'; + let npubLink = displayPubkey; + try { + if (pubkey.pubkey && pubkey.pubkey.length === 64 && /^[0-9a-fA-F]+$/.test(pubkey.pubkey)) { + const npub = window.NostrTools.nip19.npubEncode(pubkey.pubkey); + displayPubkey = npub; + npubLink = `${npub}`; + } + } catch (error) { + console.log('Failed to encode pubkey to npub:', error.message); + } + row.innerHTML = ` + ${index + 1} + ${npubLink} + ${pubkey.event_count} + ${pubkey.percentage}% + `; + tableBody.appendChild(row); + }); +} + +// Populate top pubkeys table from monitoring data +function populateStatsPubkeysFromMonitoring(pubkeysData, totalEvents) { + const tableBody = document.getElementById('stats-pubkeys-table-body'); + if (!tableBody || !pubkeysData || !Array.isArray(pubkeysData)) return; + + tableBody.innerHTML = ''; + + if (pubkeysData.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = 'No pubkey data'; + tableBody.appendChild(row); + return; + } + + pubkeysData.forEach((pubkey, index) => { + const row = document.createElement('tr'); + // Convert hex pubkey to npub for display + let displayPubkey = pubkey.pubkey || '-'; + let npubLink = displayPubkey; + try { + if (pubkey.pubkey && pubkey.pubkey.length === 64 && /^[0-9a-fA-F]+$/.test(pubkey.pubkey)) { + const npub = window.NostrTools.nip19.npubEncode(pubkey.pubkey); + displayPubkey = npub; + npubLink = `${npub}`; + } + } catch (error) { + console.log('Failed to encode pubkey to npub:', error.message); + } + + // Calculate percentage using totalEvents parameter + const percentage = totalEvents > 0 ? ((pubkey.event_count / totalEvents) * 100).toFixed(1) : '0.0'; + + row.innerHTML = ` + ${index + 1} + ${npubLink} + ${pubkey.event_count} + ${percentage}% + `; + tableBody.appendChild(row); + }); +} + +// Populate subscription details table from monitoring data +function populateSubscriptionDetailsTable(subscriptionsData) { + const tableBody = document.getElementById('subscription-details-table-body'); + if (!tableBody || !subscriptionsData || !Array.isArray(subscriptionsData)) return; + + // Store current expand/collapse state before rebuilding + const expandedGroups = new Set(); + const headerRows = tableBody.querySelectorAll('.subscription-group-header'); + headerRows.forEach(header => { + const wsiPointer = header.getAttribute('data-wsi-pointer'); + const isExpanded = header.getAttribute('data-expanded') === 'true'; + if (isExpanded) { + expandedGroups.add(wsiPointer); + } + }); + + tableBody.innerHTML = ''; + + if (subscriptionsData.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = 'No active subscriptions'; + tableBody.appendChild(row); + return; + } + + // Sort subscriptions by wsi_pointer to group them together + subscriptionsData.sort((a, b) => { + const wsiA = a.wsi_pointer || ''; + const wsiB = b.wsi_pointer || ''; + return wsiA.localeCompare(wsiB); + }); + + // Group subscriptions by wsi_pointer + const groupedSubscriptions = {}; + subscriptionsData.forEach(sub => { + const wsiKey = sub.wsi_pointer || 'N/A'; + if (!groupedSubscriptions[wsiKey]) { + groupedSubscriptions[wsiKey] = []; + } + groupedSubscriptions[wsiKey].push(sub); + }); + + // Create rows for each group + Object.entries(groupedSubscriptions).forEach(([wsiPointer, subscriptions]) => { + // Calculate group summary + const subCount = subscriptions.length; + const now = Math.floor(Date.now() / 1000); + const oldestDuration = Math.max(...subscriptions.map(s => now - s.created_at)); + const oldestDurationStr = formatDuration(oldestDuration); + + // Create header row (summary) + const headerRow = document.createElement('tr'); + headerRow.className = 'subscription-group-header'; + headerRow.setAttribute('data-wsi-pointer', wsiPointer); + const wasExpanded = expandedGroups.has(wsiPointer); + headerRow.setAttribute('data-expanded', wasExpanded ? 'true' : 'false'); + + headerRow.innerHTML = ` + + + Websocket: ${wsiPointer} + + Subscriptions: ${subCount} | Oldest: ${oldestDurationStr} + + + `; + + // Add click handler to toggle expansion + headerRow.addEventListener('click', () => toggleSubscriptionGroup(wsiPointer)); + + tableBody.appendChild(headerRow); + + // Create detail rows (initially hidden) + subscriptions.forEach((subscription, index) => { + const detailRow = document.createElement('tr'); + detailRow.className = 'subscription-detail-row'; + detailRow.setAttribute('data-wsi-group', wsiPointer); + detailRow.style.display = 'none'; + + // Calculate duration + const duration = now - subscription.created_at; + const durationStr = formatDuration(duration); + + // Format filters + let filtersDisplay = 'None'; + if (subscription.filters && subscription.filters.length > 0) { + const filterDetails = []; + subscription.filters.forEach((filter) => { + const parts = []; + + if (filter.kinds && Array.isArray(filter.kinds) && filter.kinds.length > 0) { + parts.push(`kinds:[${filter.kinds.join(',')}]`); + } + + if (filter.authors && Array.isArray(filter.authors) && filter.authors.length > 0) { + const authorCount = filter.authors.length; + if (authorCount === 1) { + const shortPubkey = filter.authors[0].substring(0, 8) + '...'; + parts.push(`authors:[${shortPubkey}]`); + } else { + parts.push(`authors:[${authorCount} pubkeys]`); + } + } + + if (filter.ids && Array.isArray(filter.ids) && filter.ids.length > 0) { + const idCount = filter.ids.length; + parts.push(`ids:[${idCount} event${idCount > 1 ? 's' : ''}]`); + } + + const timeParts = []; + if (filter.since && filter.since > 0) { + const sinceDate = new Date(filter.since * 1000).toLocaleString(); + timeParts.push(`since:${sinceDate}`); + } + if (filter.until && filter.until > 0) { + const untilDate = new Date(filter.until * 1000).toLocaleString(); + timeParts.push(`until:${untilDate}`); + } + if (timeParts.length > 0) { + parts.push(timeParts.join(', ')); + } + + if (filter.limit && filter.limit > 0) { + parts.push(`limit:${filter.limit}`); + } + + if (filter.tag_filters && Array.isArray(filter.tag_filters) && filter.tag_filters.length > 0) { + parts.push(`tags:[${filter.tag_filters.length} filter${filter.tag_filters.length > 1 ? 's' : ''}]`); + } + + if (parts.length > 0) { + filterDetails.push(parts.join(', ')); + } else { + filterDetails.push('empty filter'); + } + }); + + filtersDisplay = filterDetails.join(' | '); + } + + detailRow.innerHTML = ` + └─ + ${subscription.id || 'N/A'} + ${durationStr} + ${filtersDisplay} + `; + + tableBody.appendChild(detailRow); + + // Restore expand/collapse state after adding all rows + if (wasExpanded) { + const detailRows = tableBody.querySelectorAll(`.subscription-detail-row[data-wsi-group="${wsiPointer}"]`); + detailRows.forEach(row => row.style.display = 'table-row'); + const expandIcon = headerRow.querySelector('.expand-icon'); + if (expandIcon) { + expandIcon.textContent = '▼'; + expandIcon.style.transform = 'rotate(90deg)'; + } + } + }); + }); +} + +// Toggle function for expanding/collapsing groups +function toggleSubscriptionGroup(wsiPointer) { + const headerRow = document.querySelector(`.subscription-group-header[data-wsi-pointer="${wsiPointer}"]`); + const detailRows = document.querySelectorAll(`.subscription-detail-row[data-wsi-group="${wsiPointer}"]`); + const expandIcon = headerRow.querySelector('.expand-icon'); + + const isExpanded = headerRow.getAttribute('data-expanded') === 'true'; + + if (isExpanded) { + // Collapse + detailRows.forEach(row => row.style.display = 'none'); + expandIcon.textContent = '▶'; + expandIcon.style.transform = 'rotate(0deg)'; + headerRow.setAttribute('data-expanded', 'false'); + } else { + // Expand + detailRows.forEach(row => row.style.display = 'table-row'); + expandIcon.textContent = '▼'; + expandIcon.style.transform = 'rotate(90deg)'; + headerRow.setAttribute('data-expanded', 'true'); + } +} + +// Helper function to format duration in human-readable format +function formatDuration(seconds) { + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; + return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`; +} + +// Update statistics status indicator (disabled - status display removed) +function updateStatsStatus(status, message = '') { + // Status display has been removed from the UI + return; +} + +// Utility function to format file size +function formatFileSize(bytes) { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + +// Utility function to format timestamp +function formatTimestamp(timestamp) { + if (!timestamp) return '-'; + const date = new Date(timestamp * 1000); + return date.toLocaleString(); +} + +// Update statistics cell with flash animation if value changed +function updateStatsCell(cellId, newValue) { + const cell = document.getElementById(cellId); + if (!cell) return; + + const currentValue = cell.textContent; + cell.textContent = newValue; + + // Flash if value changed + if (currentValue !== newValue && currentValue !== '-') { + cell.classList.add('flash-value'); + setTimeout(() => { + cell.classList.remove('flash-value'); + }, 500); + } +} + +// Start auto-refreshing database statistics every 10 seconds +function startStatsAutoRefresh() { + // DISABLED - Using real-time monitoring events instead of polling + // This function is kept for backward compatibility but no longer starts auto-refresh + log('Database statistics auto-refresh DISABLED - using real-time monitoring events', 'INFO'); +} + +// Stop auto-refreshing database statistics +function stopStatsAutoRefresh() { + if (statsAutoRefreshInterval) { + clearInterval(statsAutoRefreshInterval); + statsAutoRefreshInterval = null; + } + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + // Reset countdown display + updateCountdownDisplay(); + log('Database statistics auto-refresh stopped', 'INFO'); +} + +// Update countdown display in refresh button +function updateCountdownDisplay() { + const refreshBtn = document.getElementById('refresh-stats-btn'); + if (!refreshBtn) return; + + // DISABLED - No countdown display when using real-time monitoring + // Show empty button text + refreshBtn.textContent = ''; +} + +// Flash refresh button red on successful refresh +function flashRefreshButton() { + const refreshBtn = document.getElementById('refresh-stats-btn'); + if (!refreshBtn) return; + + // DISABLED - No flashing when using real-time monitoring + // This function is kept for backward compatibility +} + +// Event handlers for test buttons +document.addEventListener('DOMContentLoaded', () => { + // Test button event handlers + const testGetAuthRulesBtn = document.getElementById('test-get-auth-rules-btn'); + const testClearAuthRulesBtn = document.getElementById('test-clear-auth-rules-btn'); + const testAddBlacklistBtn = document.getElementById('test-add-blacklist-btn'); + const testAddWhitelistBtn = document.getElementById('test-add-whitelist-btn'); + const testConfigQueryBtn = document.getElementById('test-config-query-btn'); + const testPostEventBtn = document.getElementById('test-post-event-btn'); + const clearTestLogBtn = document.getElementById('clear-test-log-btn'); + const generateTestKeyBtn = document.getElementById('generate-test-key-btn'); + + if (testGetAuthRulesBtn) { + testGetAuthRulesBtn.addEventListener('click', testGetAuthRules); + } + + if (testClearAuthRulesBtn) { + testClearAuthRulesBtn.addEventListener('click', testClearAuthRules); + } + + if (testAddBlacklistBtn) { + testAddBlacklistBtn.addEventListener('click', testAddBlacklist); + } + + if (testAddWhitelistBtn) { + testAddWhitelistBtn.addEventListener('click', testAddWhitelist); + } + + if (testConfigQueryBtn) { + testConfigQueryBtn.addEventListener('click', testConfigQuery); + } + + if (testPostEventBtn) { + testPostEventBtn.addEventListener('click', testPostEvent); + } + + if (clearTestLogBtn) { + clearTestLogBtn.addEventListener('click', () => { + const testLog = document.getElementById('test-event-log'); + if (testLog) { + testLog.innerHTML = '
SYSTEM: Test log cleared.
'; + } + }); + } + + if (generateTestKeyBtn) { + generateTestKeyBtn.addEventListener('click', generateRandomTestKey); + } + + // Show test input section when needed + const testInputSection = document.getElementById('test-input-section'); + if (testInputSection) { + testInputSection.style.display = 'block'; + } + + // Database statistics event handlers + const refreshStatsBtn = document.getElementById('refresh-stats-btn'); + if (refreshStatsBtn) { + refreshStatsBtn.addEventListener('click', sendStatsQuery); + } + + // Subscription details section is always visible when authenticated + + // NIP-17 DM event handlers + if (sendDmBtn) { + sendDmBtn.addEventListener('click', sendNIP17DM); + } + + // SQL Query event handlers + const executeSqlBtn = document.getElementById('execute-sql-btn'); + const clearSqlBtn = document.getElementById('clear-sql-btn'); + const clearHistoryBtn = document.getElementById('clear-history-btn'); + + if (executeSqlBtn) { + executeSqlBtn.addEventListener('click', executeSqlQuery); + } + + if (clearSqlBtn) { + clearSqlBtn.addEventListener('click', clearSqlQuery); + } + + if (clearHistoryBtn) { + clearHistoryBtn.addEventListener('click', clearQueryHistory); + } +}); + + +// Dark mode functionality +function toggleDarkMode() { + const body = document.body; + const isDarkMode = body.classList.contains('dark-mode'); + + if (isDarkMode) { + body.classList.remove('dark-mode'); + localStorage.setItem('darkMode', 'false'); + updateDarkModeButton(false); + log('Switched to light mode', 'INFO'); + } else { + body.classList.add('dark-mode'); + localStorage.setItem('darkMode', 'true'); + updateDarkModeButton(true); + log('Switched to dark mode', 'INFO'); + } +} + +function updateDarkModeButton(isDarkMode) { + const navDarkModeBtn = document.getElementById('nav-dark-mode-btn'); + if (navDarkModeBtn) { + navDarkModeBtn.textContent = isDarkMode ? 'LIGHT MODE' : 'DARK MODE'; + } +} + +function initializeDarkMode() { + const savedDarkMode = localStorage.getItem('darkMode'); + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + const shouldBeDark = savedDarkMode === 'true' || (savedDarkMode === null && prefersDark); + + if (shouldBeDark) { + document.body.classList.add('dark-mode'); + updateDarkModeButton(true); + } else { + updateDarkModeButton(false); + } +} + +// Side navigation functions +function toggleSideNav() { + const sideNav = document.getElementById('side-nav'); + const overlay = document.getElementById('side-nav-overlay'); + + if (sideNavOpen) { + sideNav.classList.remove('open'); + overlay.classList.remove('show'); + sideNavOpen = false; + } else { + sideNav.classList.add('open'); + overlay.classList.add('show'); + sideNavOpen = true; + } +} + +function closeSideNav() { + const sideNav = document.getElementById('side-nav'); + const overlay = document.getElementById('side-nav-overlay'); + + sideNav.classList.remove('open'); + overlay.classList.remove('show'); + sideNavOpen = false; +} + +function switchPage(pageName) { + // Update current page + currentPage = pageName; + + // Update navigation active state + const navItems = document.querySelectorAll('.nav-item'); + navItems.forEach(item => { + item.classList.remove('active'); + if (item.getAttribute('data-page') === pageName) { + item.classList.add('active'); + } + }); + + // Hide all sections + const sections = [ + 'databaseStatisticsSection', + 'subscriptionDetailsSection', + 'div_config', + 'authRulesSection', + 'relayEventsSection', + 'nip17DMSection', + 'sqlQuerySection' + ]; + + sections.forEach(sectionId => { + const section = document.getElementById(sectionId); + if (section) { + section.style.display = 'none'; + } + }); + + // Show selected section + const pageMap = { + 'statistics': 'databaseStatisticsSection', + 'subscriptions': 'subscriptionDetailsSection', + 'configuration': 'div_config', + 'authorization': 'authRulesSection', + 'relay-events': 'relayEventsSection', + 'dm': 'nip17DMSection', + 'database': 'sqlQuerySection' + }; + + const targetSectionId = pageMap[pageName]; + if (targetSectionId) { + const targetSection = document.getElementById(targetSectionId); + if (targetSection) { + targetSection.style.display = 'block'; + } + } + + // Special handling for configuration page - ensure config-display is visible and refresh data + if (pageName === 'configuration') { + const configDisplay = document.getElementById('config-display'); + if (configDisplay) { + configDisplay.classList.remove('hidden'); + } + // Always refresh configuration data when navigating to config page + fetchConfiguration().catch(error => { + console.log('Failed to refresh configuration on page switch: ' + error.message); + }); + } + + // Close side navigation + closeSideNav(); + + log(`Switched to page: ${pageName}`, 'INFO'); +} + +// Initialize the app +document.addEventListener('DOMContentLoaded', () => { + console.log('C-Relay Admin API interface loaded'); + + // Initialize dark mode + initializeDarkMode(); + + // Initialize sidebar button text + const navDarkModeBtn = document.getElementById('nav-dark-mode-btn'); + if (navDarkModeBtn) { + navDarkModeBtn.textContent = document.body.classList.contains('dark-mode') ? 'LIGHT MODE' : 'DARK MODE'; + } + + // Start RELAY letter animation + startRelayAnimation(); + + // Initialize real-time event rate chart + setTimeout(() => { + initializeEventRateChart(); + }, 1000); // Delay to ensure text_graph.js is loaded + + // Initialize side navigation + initializeSideNavigation(); + + // Ensure admin sections are hidden by default on page load + updateAdminSectionsVisibility(); + + setTimeout(() => { + initializeApp(); + // Enhance SimplePool for testing after initialization + setTimeout(enhancePoolForTesting, 2000); + }, 100); +}); + +// Initialize side navigation event handlers +function initializeSideNavigation() { + // Header title click handler + const headerTitle = document.getElementById('header-title'); + if (headerTitle) { + headerTitle.addEventListener('click', toggleSideNav); + } + + // Overlay click handler + const overlay = document.getElementById('side-nav-overlay'); + if (overlay) { + overlay.addEventListener('click', closeSideNav); + } + + // Navigation item click handlers + const navItems = document.querySelectorAll('.nav-item'); + navItems.forEach(item => { + item.addEventListener('click', (e) => { + const pageName = e.target.getAttribute('data-page'); + if (pageName) { + switchPage(pageName); + } + }); + }); + + // Footer button handlers + const navDarkModeBtn = document.getElementById('nav-dark-mode-btn'); + const navLogoutBtn = document.getElementById('nav-logout-btn'); + + if (navDarkModeBtn) { + navDarkModeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + toggleDarkMode(); + // Update button text after toggle + setTimeout(() => { + navDarkModeBtn.textContent = document.body.classList.contains('dark-mode') ? 'LIGHT MODE' : 'DARK MODE'; + }, 10); + closeSideNav(); + }); + } + + if (navLogoutBtn) { + navLogoutBtn.addEventListener('click', (e) => { + e.stopPropagation(); + logout(); + closeSideNav(); + }); + } + + // Set initial page + switchPage(currentPage); +} + +// ================================ +// SQL QUERY FUNCTIONS +// ================================ + +// Predefined query templates +const SQL_QUERY_TEMPLATES = { + recent_events: "SELECT id, pubkey, created_at, kind, substr(content, 1, 50) as content FROM events ORDER BY created_at DESC LIMIT 20", + event_stats: "SELECT * FROM event_stats", + subscriptions: "SELECT * FROM active_subscriptions_log ORDER BY created_at DESC", + top_pubkeys: "SELECT * FROM top_pubkeys_view", + event_kinds: "SELECT * FROM event_kinds_view ORDER BY count DESC", + time_stats: "SELECT 'total' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events UNION ALL SELECT '24h' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events WHERE created_at >= (strftime('%s', 'now') - 86400) UNION ALL SELECT '7d' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events WHERE created_at >= (strftime('%s', 'now') - 604800) UNION ALL SELECT '30d' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events WHERE created_at >= (strftime('%s', 'now') - 2592000)" +}; + +// Query history management (localStorage) +const QUERY_HISTORY_KEY = 'c_relay_sql_history'; +const MAX_HISTORY_ITEMS = 20; + +// Load query history from localStorage +function loadQueryHistory() { + try { + const history = localStorage.getItem(QUERY_HISTORY_KEY); + return history ? JSON.parse(history) : []; + } catch (e) { + console.error('Failed to load query history:', e); + return []; + } +} + +// Save query to history +function saveQueryToHistory(query) { + if (!query || query.trim().length === 0) return; + + try { + let history = loadQueryHistory(); + + // Remove duplicate if exists + history = history.filter(q => q !== query); + + // Add to beginning + history.unshift(query); + + // Limit size + if (history.length > MAX_HISTORY_ITEMS) { + history = history.slice(0, MAX_HISTORY_ITEMS); + } + + localStorage.setItem(QUERY_HISTORY_KEY, JSON.stringify(history)); + updateQueryDropdown(); + } catch (e) { + console.error('Failed to save query history:', e); + } +} + +// Clear query history +function clearQueryHistory() { + if (confirm('Clear all query history?')) { + localStorage.removeItem(QUERY_HISTORY_KEY); + updateQueryDropdown(); + } +} + +// Update dropdown with history +function updateQueryDropdown() { + const historyGroup = document.getElementById('history-group'); + if (!historyGroup) return; + + // Clear existing history options + historyGroup.innerHTML = ''; + + const history = loadQueryHistory(); + if (history.length === 0) { + const option = document.createElement('option'); + option.value = ''; + option.textContent = '(no history)'; + option.disabled = true; + historyGroup.appendChild(option); + return; + } + + history.forEach((query, index) => { + const option = document.createElement('option'); + option.value = `history_${index}`; + // Truncate long queries for display + const displayQuery = query.length > 60 ? query.substring(0, 60) + '...' : query; + option.textContent = displayQuery; + option.dataset.query = query; + historyGroup.appendChild(option); + }); +} + +// Load selected query from dropdown +function loadSelectedQuery() { + const dropdown = document.getElementById('query-dropdown'); + const selectedValue = dropdown.value; + + if (!selectedValue) return; + + let query = ''; + + // Check if it's a template + if (SQL_QUERY_TEMPLATES[selectedValue]) { + query = SQL_QUERY_TEMPLATES[selectedValue]; + } + // Check if it's from history + else if (selectedValue.startsWith('history_')) { + const selectedOption = dropdown.options[dropdown.selectedIndex]; + query = selectedOption.dataset.query; + } + + if (query) { + document.getElementById('sql-input').value = query; + } + + // Reset dropdown to placeholder + dropdown.value = ''; +} + +// Clear the SQL query input +function clearSqlQuery() { + document.getElementById('sql-input').value = ''; + document.getElementById('query-info').innerHTML = ''; + document.getElementById('query-table').innerHTML = ''; +} + +// Execute SQL query via admin API +async function executeSqlQuery() { + const query = document.getElementById('sql-input').value; + if (!query.trim()) { + log('Please enter a SQL query', 'ERROR'); + document.getElementById('query-info').innerHTML = '
❌ Please enter a SQL query
'; + return; + } + + try { + // Show loading state + document.getElementById('query-info').innerHTML = '
Executing query...
'; + document.getElementById('query-table').innerHTML = ''; + + // Save to history (before execution, so it's saved even if query fails) + saveQueryToHistory(query.trim()); + + // Send query as kind 23456 admin command + const command = ["sql_query", query]; + const requestEvent = await sendAdminCommand(command); + + // Store query info for when response arrives + if (requestEvent && requestEvent.id) { + pendingSqlQueries.set(requestEvent.id, { + query: query, + timestamp: Date.now() + }); + } + + // Note: Response will be handled by the event listener + // which will call displaySqlQueryResults() when response arrives + } catch (error) { + log('Failed to execute query: ' + error.message, 'ERROR'); + document.getElementById('query-info').innerHTML = '
❌ Failed to execute query: ' + error.message + '
'; + } +} + +// Helper function to send admin commands (kind 23456 events) +async function sendAdminCommand(commandArray) { + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to send admin commands'); + } + + if (!relayPool) { + throw new Error('SimplePool connection not available'); + } + + try { + log(`Sending admin command: ${JSON.stringify(commandArray)}`, 'INFO'); + + // Encrypt the command array directly using NIP-44 + const encrypted_content = await encryptForRelay(JSON.stringify(commandArray)); + if (!encrypted_content) { + throw new Error('Failed to encrypt command array'); + } + + // Create single kind 23456 admin event + const adminEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", getRelayPubkey()]], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(adminEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + // Publish via SimplePool with detailed error diagnostics + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Use Promise.allSettled to capture per-relay outcomes + const results = await Promise.allSettled(publishPromises); + + // Log detailed publish results for diagnostics + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + log(`✅ Admin command published successfully to relay ${index}`, 'INFO'); + } else { + log(`❌ Admin command failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR'); + } + }); + + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected admin command event. Details: ${errorDetails}`); + } + + log('Admin command sent successfully', 'INFO'); + return signedEvent; // Return the signed event for request ID tracking + + } catch (error) { + log(`Failed to send admin command: ${error.message}`, 'ERROR'); + throw error; + } +} + +// Display SQL query results +function displaySqlQueryResults(response) { + const infoDiv = document.getElementById('query-info'); + const tableDiv = document.getElementById('query-table'); + + if (response.status === 'error' || response.error) { + infoDiv.innerHTML = `
❌ ${response.error || 'Query failed'}
`; + tableDiv.innerHTML = ''; + return; + } + + // Show query info with request ID for debugging + const rowCount = response.row_count || 0; + const execTime = response.execution_time_ms || 0; + const requestId = response.request_id ? response.request_id.substring(0, 8) + '...' : 'unknown'; + infoDiv.innerHTML = ` +
+ ✅ Query executed successfully + Rows: ${rowCount} + Execution Time: ${execTime}ms + Request: ${requestId} +
+ `; + + // Build results table + if (response.rows && response.rows.length > 0) { + let html = ''; + response.columns.forEach(col => { + html += ``; + }); + html += ''; + + response.rows.forEach(row => { + html += ''; + row.forEach(cell => { + const cellValue = cell === null ? 'NULL' : escapeHtml(String(cell)); + html += ``; + }); + html += ''; + }); + + html += '
${escapeHtml(col)}
${cellValue}
'; + tableDiv.innerHTML = html; + } else { + tableDiv.innerHTML = '

No results returned

'; + } +} + +// Handle SQL query response (called by event listener) +function handleSqlQueryResponse(response) { + console.log('=== HANDLING SQL QUERY RESPONSE ==='); + console.log('Response:', response); + + // Always display SQL query results when received + displaySqlQueryResults(response); + + // Clean up any pending queries + if (response.request_id && pendingSqlQueries.has(response.request_id)) { + pendingSqlQueries.delete(response.request_id); + } +} + +// Helper function to escape HTML +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Initialize query history on page load +document.addEventListener('DOMContentLoaded', function() { + updateQueryDropdown(); +}); + +// RELAY letter animation function +function startRelayAnimation() { + const letters = document.querySelectorAll('.relay-letter'); + let currentIndex = 0; + + function animateLetter() { + // Remove underline from all letters first + letters.forEach(letter => letter.classList.remove('underlined')); + + // Add underline to current letter + if (letters[currentIndex]) { + letters[currentIndex].classList.add('underlined'); + } + + // Move to next letter + currentIndex++; + + // If we've gone through all letters, remove all underlines and wait 4000ms then restart + if (currentIndex > letters.length) { + // Remove all underlines before the pause + letters.forEach(letter => letter.classList.remove('underlined')); + setTimeout(() => { + currentIndex = 0; + animateLetter(); + }, 4000); + } else { + // Otherwise, continue to next letter after 200ms + setTimeout(animateLetter, 100); + } + } + + // Start the animation + animateLetter(); +} + +// ================================ +// CONFIG TOGGLE BUTTON COMPONENT +// ================================ + +// Global registry for config toggle buttons +const configToggleButtons = new Map(); + +// ConfigToggleButton class for tri-state boolean config toggles +class ConfigToggleButton { + constructor(configKey, container, options = {}) { + this.configKey = configKey; + this.container = container; + this.state = 'false'; // Start in false state by default + this.pendingValue = null; + this.options = { + dataType: 'boolean', + category: 'monitoring', + ...options + }; + + this.render(); + this.attachEventListeners(); + + // Register this button instance + configToggleButtons.set(configKey, this); + } + + render() { + console.log('=== RENDERING CONFIG TOGGLE BUTTON ==='); + console.log('Config key:', this.configKey); + console.log('Container:', this.container); + + // Create button element + this.button = document.createElement('button'); + this.button.className = 'config-toggle-btn'; + this.button.setAttribute('data-config-key', this.configKey); + this.button.setAttribute('data-state', this.state); + this.button.setAttribute('title', `Toggle ${this.configKey}`); + this.updateIcon(); + + console.log('Button element created:', this.button); + console.log('Container before append:', this.container); + console.log('Container children before:', this.container.children.length); + + this.container.appendChild(this.button); + + console.log('Container children after:', this.container.children.length); + console.log('Button in DOM:', document.contains(this.button)); + } + + updateIcon() { + const icons = { + 'true': 'I', + 'false': '0', + 'indeterminate': '⟳' + }; + this.button.textContent = icons[this.state] || '?'; + } + + setState(newState) { + if (['true', 'false', 'indeterminate'].includes(newState)) { + this.state = newState; + this.button.setAttribute('data-state', newState); + this.updateIcon(); + } + } + + async toggle() { + console.log('=== TOGGLE BUTTON CLICKED ==='); + console.log('Current state:', this.state); + console.log('Button element:', this.button); + + if (this.state === 'indeterminate') { + console.log('Ignoring toggle - currently indeterminate'); + return; // Don't toggle while pending + } + + // Toggle between true and false + const newValue = this.state === 'true' ? 'false' : 'true'; + this.pendingValue = newValue; + + console.log('Sending toggle command:', newValue); + + // Set to indeterminate while waiting + this.setState('indeterminate'); + + // Create config object + const configObj = { + key: this.configKey, + value: newValue, + data_type: this.options.dataType, + category: this.options.category + }; + + console.log('Config object:', configObj); + + try { + // Send config update command + console.log('Sending config update command...'); + await sendConfigUpdateCommand([configObj]); + console.log('Config update command sent successfully'); + log(`Config toggle sent: ${this.configKey} = ${newValue}`, 'INFO'); + } catch (error) { + console.log('Config update command failed:', error); + log(`Failed to send config toggle: ${error.message}`, 'ERROR'); + // Revert to previous state on error + this.setState('false'); + this.pendingValue = null; + } + } + + handleResponse(success, actualValue) { + console.log('=== HANDLE RESPONSE ==='); + console.log('Success:', success); + console.log('Actual value:', actualValue); + console.log('Pending value:', this.pendingValue); + + if (success) { + console.log('Success - setting to actual server value:', actualValue); + this.setState(actualValue); + } else { + console.log('Failed - reverting to false state'); + // Failed - revert to false state + this.setState('false'); + } + this.pendingValue = null; + console.log('Pending value cleared'); + } + + attachEventListeners() { + this.button.addEventListener('click', () => this.toggle()); + } +} + +// Helper function to get a registered toggle button +function getConfigToggleButton(configKey) { + return configToggleButtons.get(configKey); +} + +// Monitoring is now subscription-based - no toggle button needed +// Monitoring automatically activates when someone subscribes to kind 24567 events +function initializeMonitoringToggleButton() { + console.log('=== MONITORING IS NOW SUBSCRIPTION-BASED ==='); + console.log('No toggle button needed - monitoring activates automatically when subscribing to kind 24567'); + log('Monitoring system is subscription-based - no manual toggle required', 'INFO'); + return null; +} + +// Monitoring is subscription-based - no toggle button response handling needed +const originalHandleConfigUpdateResponse = handleConfigUpdateResponse; +handleConfigUpdateResponse = function(responseData) { + console.log('=== CONFIG UPDATE RESPONSE HANDLER ==='); + console.log('Response data:', responseData); + + // Call original handler + originalHandleConfigUpdateResponse(responseData); + + // Monitoring is now subscription-based - no toggle buttons to update + console.log('Monitoring system is subscription-based - no toggle buttons to handle'); +}; + +// Monitoring is now subscription-based - no toggle buttons needed +function initializeToggleButtonsFromConfig(configData) { + console.log('=== MONITORING IS SUBSCRIPTION-BASED ==='); + console.log('No toggle buttons needed - monitoring activates automatically when subscribing to kind 24567'); + log('Monitoring system initialized - subscription-based activation ready', 'INFO'); +} + +// ================================ +// RELAY EVENTS FUNCTIONS +// ================================ + + +// Handle received relay events +function handleRelayEventReceived(event) { + console.log('Handling relay event:', event.kind, event); + + switch (event.kind) { + case 0: + populateKind0Form(event); + break; + case 10050: + populateKind10050Form(event); + break; + case 10002: + populateKind10002Form(event); + break; + default: + console.log('Unknown relay event kind:', event.kind); + } +} + +// Populate Kind 0 form (User Metadata) +function populateKind0Form(event) { + try { + const metadata = JSON.parse(event.content); + console.log('Populating Kind 0 form with:', metadata); + + // Update form fields + const nameField = document.getElementById('kind0-name'); + const aboutField = document.getElementById('kind0-about'); + const pictureField = document.getElementById('kind0-picture'); + const bannerField = document.getElementById('kind0-banner'); + const nip05Field = document.getElementById('kind0-nip05'); + const websiteField = document.getElementById('kind0-website'); + + if (nameField) nameField.value = metadata.name || ''; + if (aboutField) aboutField.value = metadata.about || ''; + if (pictureField) pictureField.value = metadata.picture || ''; + if (bannerField) bannerField.value = metadata.banner || ''; + if (nip05Field) nip05Field.value = metadata.nip05 || ''; + if (websiteField) websiteField.value = metadata.website || ''; + + showStatus('kind0-status', 'Metadata loaded from relay', 'success'); + + } catch (error) { + console.error('Error populating Kind 0 form:', error); + showStatus('kind0-status', 'Error loading metadata', 'error'); + } +} + +// Populate Kind 10050 form (DM Relay List) +function populateKind10050Form(event) { + try { + console.log('Populating Kind 10050 form with tags:', event.tags); + + // Extract relay URLs from "relay" tags + const relayUrls = event.tags + .filter(tag => tag[0] === 'relay' && tag[1]) + .map(tag => tag[1]); + + const relaysField = document.getElementById('kind10050-relays'); + if (relaysField) { + relaysField.value = relayUrls.join('\n'); + } + + showStatus('kind10050-status', 'DM relay list loaded from relay', 'success'); + + } catch (error) { + console.error('Error populating Kind 10050 form:', error); + showStatus('kind10050-status', 'Error loading DM relay list', 'error'); + } +} + +// Populate Kind 10002 form (Relay List) +function populateKind10002Form(event) { + try { + console.log('Populating Kind 10002 form with tags:', event.tags); + + // Clear existing entries + const container = document.getElementById('kind10002-relay-entries'); + if (container) { + container.innerHTML = ''; + } + + // Extract relay entries from "r" tags + event.tags.forEach(tag => { + if (tag[0] === 'r' && tag[1]) { + const url = tag[1]; + const marker = tag[2] || 'read'; // Default to read if no marker + const read = marker.includes('read'); + const write = marker.includes('write'); + + addRelayEntry(url, read, write); + } + }); + + showStatus('kind10002-status', 'Relay list loaded from relay', 'success'); + + } catch (error) { + console.error('Error populating Kind 10002 form:', error); + showStatus('kind10002-status', 'Error loading relay list', 'error'); + } +} + +// Submit Kind 0 event +async function submitKind0Event() { + try { + showStatus('kind0-status', 'Submitting metadata...', 'info'); + + // Collect form data + const metadata = { + name: document.getElementById('kind0-name').value.trim(), + about: document.getElementById('kind0-about').value.trim(), + picture: document.getElementById('kind0-picture').value.trim(), + banner: document.getElementById('kind0-banner').value.trim(), + nip05: document.getElementById('kind0-nip05').value.trim(), + website: document.getElementById('kind0-website').value.trim() + }; + + // Remove empty fields + Object.keys(metadata).forEach(key => { + if (!metadata[key]) delete metadata[key]; + }); + + // Validate required fields + if (!metadata.name) { + showStatus('kind0-status', 'Name is required', 'error'); + return; + } + + await sendCreateRelayEventCommand(0, metadata); + showStatus('kind0-status', 'Metadata updated successfully', 'success'); + + } catch (error) { + console.error('Error submitting Kind 0 event:', error); + showStatus('kind0-status', 'Error updating metadata: ' + error.message, 'error'); + } +} + +// Submit Kind 10050 event +async function submitKind10050Event() { + try { + showStatus('kind10050-status', 'Submitting DM relay list...', 'info'); + + // Parse textarea content + const relaysText = document.getElementById('kind10050-relays').value.trim(); + const relays = relaysText.split('\n') + .map(url => url.trim()) + .filter(url => url.length > 0) + .filter(url => isValidRelayUrl(url)); + + if (relays.length === 0) { + showStatus('kind10050-status', 'At least one valid relay URL is required', 'error'); + return; + } + + await sendCreateRelayEventCommand(10050, { relays }); + showStatus('kind10050-status', 'DM relay list updated successfully', 'success'); + + } catch (error) { + console.error('Error submitting Kind 10050 event:', error); + showStatus('kind10050-status', 'Error updating DM relay list: ' + error.message, 'error'); + } +} + +// Submit Kind 10002 event +async function submitKind10002Event() { + try { + showStatus('kind10002-status', 'Submitting relay list...', 'info'); + + // Collect relay entries + const relays = []; + const entries = document.querySelectorAll('.relay-entry'); + + entries.forEach(entry => { + const url = entry.querySelector('.relay-url').value.trim(); + const read = entry.querySelector('.relay-read').checked; + const write = entry.querySelector('.relay-write').checked; + + if (url && isValidRelayUrl(url)) { + relays.push({ + url: url, + read: read, + write: write + }); + } + }); + + if (relays.length === 0) { + showStatus('kind10002-status', 'At least one valid relay entry is required', 'error'); + return; + } + + await sendCreateRelayEventCommand(10002, { relays }); + showStatus('kind10002-status', 'Relay list updated successfully', 'success'); + + } catch (error) { + console.error('Error submitting Kind 10002 event:', error); + showStatus('kind10002-status', 'Error updating relay list: ' + error.message, 'error'); + } +} + +// Send create_relay_event command +async function sendCreateRelayEventCommand(kind, eventData) { + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to create relay events'); + } + + if (!relayPool) { + throw new Error('SimplePool connection not available'); + } + + try { + console.log(`Sending create_relay_event command for kind ${kind}...`); + + // Create command array + const command_array = ["create_relay_event", kind, eventData]; + + // Encrypt the command array + const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); + if (!encrypted_content) { + throw new Error('Failed to encrypt command array'); + } + + // Create kind 23456 admin event + const adminEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", getRelayPubkey()]], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(adminEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + // Publish via SimplePool + const url = relayConnectionUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + + // Wait for publish results + const results = await Promise.allSettled(publishPromises); + let successCount = 0; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount++; + console.log(`✅ Relay event published successfully to relay ${index}`); + } else { + console.error(`❌ Relay event failed on relay ${index}:`, result.reason); + } + }); + + if (successCount === 0) { + const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); + throw new Error(`All relays rejected relay event. Details: ${errorDetails}`); + } + + console.log(`Relay event command sent successfully for kind ${kind}`); + + } catch (error) { + console.error(`Failed to send create_relay_event command for kind ${kind}:`, error); + throw error; + } +} + +// Validation helpers +function isValidUrl(url) { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +function isValidRelayUrl(url) { + if (!isValidUrl(url)) return false; + return url.startsWith('ws://') || url.startsWith('wss://'); +} + +// UI helpers +function showStatus(elementId, message, type = 'info') { + const element = document.getElementById(elementId); + if (!element) return; + + // Remove emojis from message + const cleanMessage = message.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, ''); + + element.textContent = cleanMessage; + element.className = 'status-message'; + element.style.display = 'block'; // Ensure it's visible + + // Add type-specific styling + switch (type) { + case 'success': + element.style.color = 'var(--accent-color)'; + break; + case 'error': + element.style.color = '#ff0000'; + break; + case 'info': + default: + element.style.color = 'var(--primary-color)'; + break; + } + + // Auto-hide after 5 seconds + setTimeout(() => { + element.style.display = 'none'; + }, 5000); +} + +function addRelayEntry(url = '', read = true, write = true) { + const container = document.getElementById('kind10002-relay-entries'); + if (!container) return; + + const entryDiv = document.createElement('div'); + entryDiv.className = 'relay-entry'; + entryDiv.innerHTML = ` +
+ + + + +
+ `; + + container.appendChild(entryDiv); +} + +function removeRelayEntry(button) { + const entry = button.closest('.relay-entry'); + if (entry) { + entry.remove(); + } +} + +// Initialize toggle button after DOM is ready +document.addEventListener('DOMContentLoaded', function() { + console.log('=== DOM CONTENT LOADED - INITIALIZING TOGGLE BUTTON ==='); + + // Initialize the monitoring toggle button + setTimeout(() => { + console.log('=== SETTIMEOUT CALLBACK - CALLING initializeMonitoringToggleButton ==='); + initializeMonitoringToggleButton(); + }, 500); // Small delay to ensure DOM is fully ready + + // Initialize relay events functionality + initializeRelayEvents(); +}); + +// Initialize relay events functionality +function initializeRelayEvents() { + console.log('Initializing relay events functionality...'); + + // Set up event handlers for relay events page + const submitKind0Btn = document.getElementById('submit-kind0-btn'); + const submitKind10050Btn = document.getElementById('submit-kind10050-btn'); + const submitKind10002Btn = document.getElementById('submit-kind10002-btn'); + const addRelayEntryBtn = document.getElementById('add-relay-entry-btn'); + + if (submitKind0Btn) { + submitKind0Btn.addEventListener('click', submitKind0Event); + } + + if (submitKind10050Btn) { + submitKind10050Btn.addEventListener('click', submitKind10050Event); + } + + if (submitKind10002Btn) { + submitKind10002Btn.addEventListener('click', submitKind10002Event); + } + + if (addRelayEntryBtn) { + addRelayEntryBtn.addEventListener('click', () => addRelayEntry()); + } + + // Add one empty relay entry by default for Kind 10002 + const kind10002Container = document.getElementById('kind10002-relay-entries'); + if (kind10002Container && kind10002Container.children.length === 0) { + addRelayEntry(); // Add one empty entry to start + console.log('Added initial empty relay entry for Kind 10002'); + } + + console.log('Relay events functionality initialized'); +} \ No newline at end of file diff --git a/api/nostr-lite.js b/api/nostr-lite.js new file mode 100644 index 0000000..c6773cf --- /dev/null +++ b/api/nostr-lite.js @@ -0,0 +1,4282 @@ +/** + * NOSTR_LOGIN_LITE - Authentication Library + * + * ⚠️ WARNING: THIS FILE IS AUTO-GENERATED - DO NOT EDIT MANUALLY! + * ⚠️ To make changes, edit lite/build.js and run: cd lite && node build.js + * ⚠️ Any manual edits to this file will be OVERWRITTEN when build.js runs! + * + * Two-file architecture: + * 1. Load nostr.bundle.js (official nostr-tools bundle) + * 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes) + * Generated on: 2025-10-01T14:18:10.269Z + */ + +// Verify dependencies are loaded +if (typeof window !== 'undefined') { + if (!window.NostrTools) { + console.error('NOSTR_LOGIN_LITE: nostr.bundle.js must be loaded first'); + throw new Error('Missing dependency: nostr.bundle.js'); + } + + console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓'); + console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools)); + console.log('NOSTR_LOGIN_LITE: NIP-06 available:', !!window.NostrTools.nip06); + console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46); +} + +// ====================================== +// NOSTR_LOGIN_LITE Components +// ====================================== + +// ====================================== +// CSS-Only Theme System +// ====================================== + +const THEME_CSS = { + 'default': `/** + * NOSTR_LOGIN_LITE - Default Monospace Theme + * Black/white/red color scheme with monospace typography + * Simplified 14-variable system (6 core + 8 floating tab) + */ + +:root { + /* Core Variables (6) */ + --nl-primary-color: #000000; + --nl-secondary-color: #ffffff; + --nl-accent-color: #ff0000; + --nl-muted-color: #CCCCCC; + --nl-font-family: "Courier New", Courier, monospace; + --nl-border-radius: 15px; + --nl-border-width: 3px; + + /* Floating Tab Variables (8) */ + --nl-tab-bg-logged-out: #ffffff; + --nl-tab-bg-logged-in: #ffffff; + --nl-tab-bg-opacity-logged-out: 0.9; + --nl-tab-bg-opacity-logged-in: 0.2; + --nl-tab-color-logged-out: #000000; + --nl-tab-color-logged-in: #ffffff; + --nl-tab-border-logged-out: #000000; + --nl-tab-border-logged-in: #ff0000; + --nl-tab-border-opacity-logged-out: 1.0; + --nl-tab-border-opacity-logged-in: 0.1; +} + +/* Base component styles using simplified variables */ +.nl-component { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-button { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + cursor: pointer; + transition: all 0.2s ease; +} + +.nl-button:hover { + border-color: var(--nl-accent-color); +} + +.nl-button:active { + background: var(--nl-accent-color); + color: var(--nl-secondary-color); +} + +.nl-input { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + box-sizing: border-box; +} + +.nl-input:focus { + border-color: var(--nl-accent-color); + outline: none; +} + +.nl-container { + background: var(--nl-secondary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); +} + +.nl-title, .nl-heading { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); + margin: 0; +} + +.nl-text { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-text--muted { + color: var(--nl-muted-color); +} + +.nl-icon { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +/* Floating tab styles */ +.nl-floating-tab { + font-family: var(--nl-font-family); + border-radius: var(--nl-border-radius); + border: var(--nl-border-width) solid; + transition: all 0.2s ease; +} + +.nl-floating-tab--logged-out { + background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out)); + color: var(--nl-tab-color-logged-out); + border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out)); +} + +.nl-floating-tab--logged-in { + background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in)); + color: var(--nl-tab-color-logged-in); + border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in)); +} + +.nl-transition { + transition: all 0.2s ease; +}`, + 'dark': `/** + * NOSTR_LOGIN_LITE - Dark Monospace Theme + */ + +:root { + /* Core Variables (6) */ + --nl-primary-color: #white; + --nl-secondary-color: #black; + --nl-accent-color: #ff0000; + --nl-muted-color: #666666; + --nl-font-family: "Courier New", Courier, monospace; + --nl-border-radius: 15px; + --nl-border-width: 3px; + + /* Floating Tab Variables (8) */ + --nl-tab-bg-logged-out: #ffffff; + --nl-tab-bg-logged-in: #000000; + --nl-tab-bg-opacity-logged-out: 0.9; + --nl-tab-bg-opacity-logged-in: 0.8; + --nl-tab-color-logged-out: #000000; + --nl-tab-color-logged-in: #ffffff; + --nl-tab-border-logged-out: #000000; + --nl-tab-border-logged-in: #ff0000; + --nl-tab-border-opacity-logged-out: 1.0; + --nl-tab-border-opacity-logged-in: 0.9; +} + +/* Base component styles using simplified variables */ +.nl-component { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-button { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + cursor: pointer; + transition: all 0.2s ease; +} + +.nl-button:hover { + border-color: var(--nl-accent-color); +} + +.nl-button:active { + background: var(--nl-accent-color); + color: var(--nl-secondary-color); +} + +.nl-input { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + box-sizing: border-box; +} + +.nl-input:focus { + border-color: var(--nl-accent-color); + outline: none; +} + +.nl-container { + background: var(--nl-secondary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); +} + +.nl-title, .nl-heading { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); + margin: 0; +} + +.nl-text { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-text--muted { + color: var(--nl-muted-color); +} + +.nl-icon { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +/* Floating tab styles */ +.nl-floating-tab { + font-family: var(--nl-font-family); + border-radius: var(--nl-border-radius); + border: var(--nl-border-width) solid; + transition: all 0.2s ease; +} + +.nl-floating-tab--logged-out { + background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out)); + color: var(--nl-tab-color-logged-out); + border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out)); +} + +.nl-floating-tab--logged-in { + background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in)); + color: var(--nl-tab-color-logged-in); + border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in)); +} + +.nl-transition { + transition: all 0.2s ease; +}` +}; + +// Theme management functions +function injectThemeCSS(themeName = 'default') { + if (typeof document !== 'undefined') { + // Remove existing theme CSS + const existingStyle = document.getElementById('nl-theme-css'); + if (existingStyle) { + existingStyle.remove(); + } + + // Inject selected theme CSS + const themeCss = THEME_CSS[themeName] || THEME_CSS['default']; + const style = document.createElement('style'); + style.id = 'nl-theme-css'; + style.textContent = themeCss; + document.head.appendChild(style); + console.log('NOSTR_LOGIN_LITE: ' + themeName + ' theme CSS injected'); + } +} + +// Auto-inject default theme when DOM is ready +if (typeof document !== 'undefined') { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => injectThemeCSS('default')); + } else { + injectThemeCSS('default'); + } +} + +// ====================================== +// Modal UI Component +// ====================================== + + +class Modal { + 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(); + } + + _initModal() { + // Create modal container + this.container = document.createElement('div'); + 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; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + display: none; + z-index: 10000; + 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: 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, no fixed height + 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: var(--nl-border-radius, 15px); + border: var(--nl-border-width) solid var(--nl-primary-color); + overflow: hidden; + `; + } + + // Header + const modalHeader = document.createElement('div'); + modalHeader.style.cssText = ` + padding: 20px 24px 0 24px; + display: flex; + justify-content: space-between; + align-items: center; + background: transparent; + border-bottom: none; + `; + + const modalTitle = document.createElement('h2'); + modalTitle.textContent = 'Nostr Login'; + modalTitle.style.cssText = ` + margin: 0; + font-size: 24px; + font-weight: 600; + 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: var(--nl-secondary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: 4px; + font-size: 28px; + color: var(--nl-primary-color); + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--nl-font-family, 'Courier New', monospace); + `; + 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(closeButton); + } + + // Body + this.modalBody = document.createElement('div'); + this.modalBody.style.cssText = ` + padding: 24px; + background: transparent; + font-family: var(--nl-font-family, 'Courier New', monospace); + `; + + modalContent.appendChild(modalHeader); + // Add version element in bottom-right corner aligned with modal body + const versionElement = document.createElement('div'); + versionElement.textContent = 'v0.1.7'; + versionElement.style.cssText = ` + position: absolute; + bottom: 8px; + right: 24px; + font-size: 14px; + color: #666666; + font-family: var(--nl-font-family, 'Courier New', monospace); + pointer-events: none; + z-index: 1; + `; + modalContent.appendChild(versionElement); + + modalContent.appendChild(this.modalBody); + this.container.appendChild(modalContent); + + // 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 (only for modal mode) + if (!this.isEmbedded) { + this.container.onclick = (e) => { + if (e.target === this.container) { + this.close(); + } + }; + } + + // Update theme + this.updateTheme(); + } + + updateTheme() { + // The theme will automatically update through CSS custom properties + // No manual styling needed - the CSS variables handle everything + } + + open(opts = {}) { + this.currentScreen = opts.startScreen; + this.isVisible = true; + this.container.style.display = 'block'; + + // Render login options + this._renderLoginOptions(); + } + + close() { + this.isVisible = false; + this.container.style.display = 'none'; + this.modalBody.innerHTML = ''; + } + + _renderLoginOptions() { + this.modalBody.innerHTML = ''; + + const options = []; + + // Extension option + if (this.options?.methods?.extension !== false) { + options.push({ + type: 'extension', + title: 'Browser Extension', + description: 'Use your browser extension', + icon: '🔌' + }); + } + + // Local key option + if (this.options?.methods?.local !== false) { + options.push({ + type: 'local', + title: 'Local Key', + description: 'Create or import your own key', + icon: '🔑' + }); + } + + // Seed Phrase option - only show if explicitly enabled + if (this.options?.methods?.seedphrase === true) { + options.push({ + type: 'seedphrase', + title: 'Seed Phrase', + description: 'Import from mnemonic seed phrase', + icon: '🌱' + }); + } + + // 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', + description: 'Connect with external signer', + icon: '🌐' + }); + } + + // Read-only option + if (this.options?.methods?.readonly !== false) { + options.push({ + type: 'readonly', + title: 'Read Only', + description: 'Browse without signing', + icon: '👁️' + }); + } + + // OTP/DM option + if (this.options?.methods?.otp !== false) { + options.push({ + type: 'otp', + title: 'DM/OTP', + description: 'Receive OTP via DM', + icon: '📱' + }); + } + + // Render each option + options.forEach(option => { + const button = document.createElement('button'); + button.onclick = () => this._handleOptionClick(option.type); + button.style.cssText = ` + display: flex; + align-items: center; + width: 100%; + padding: 16px; + margin-bottom: 12px; + 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.borderColor = 'var(--nl-accent-color)'; + button.style.background = 'var(--nl-secondary-color)'; + }; + button.onmouseout = () => { + button.style.borderColor = 'var(--nl-primary-color)'; + button.style.background = 'var(--nl-secondary-color)'; + }; + + const iconDiv = document.createElement('div'); + // Remove the icon entirely - no emojis or text-based icons + iconDiv.textContent = ''; + iconDiv.style.cssText = ` + font-size: 16px; + font-weight: bold; + margin-right: 16px; + width: 0px; + text-align: center; + color: var(--nl-primary-color); + font-family: var(--nl-font-family, 'Courier New', monospace); + `; + + const contentDiv = document.createElement('div'); + contentDiv.style.cssText = 'flex: 1; text-align: left;'; + + const titleDiv = document.createElement('div'); + titleDiv.textContent = option.title; + titleDiv.style.cssText = ` + font-weight: 600; + margin-bottom: 4px; + 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: #666666; + font-family: var(--nl-font-family, 'Courier New', monospace); + `; + + contentDiv.appendChild(titleDiv); + contentDiv.appendChild(descDiv); + + button.appendChild(iconDiv); + button.appendChild(contentDiv); + this.modalBody.appendChild(button); + }); + } + + _handleOptionClick(type) { + console.log('Selected login type:', type); + + // Handle different login types + switch (type) { + case 'extension': + this._handleExtension(); + break; + case 'local': + this._showLocalKeyScreen(); + break; + case 'seedphrase': + this._showSeedPhraseScreen(); + break; + case 'connect': + this._showConnectScreen(); + break; + case 'readonly': + this._handleReadonly(); + break; + case 'otp': + this._showOtpScreen(); + break; + } + } + + _handleExtension() { + // SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension + let extension = null; + + // Check if NostrLite instance has a preserved extension (real extension detected at init) + if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) { + extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension; + console.log('Modal: Using preserved extension:', extension.constructor?.name); + } + // Otherwise check current window.nostr + else if (window.nostr && this._isRealExtension(window.nostr)) { + extension = window.nostr; + console.log('Modal: Using current window.nostr extension:', extension.constructor?.name); + } + + if (!extension) { + console.log('Modal: No extension detected yet, waiting for deferred detection...'); + + // DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library + let attempts = 0; + const maxAttempts = 10; // Try for 2 seconds + const checkForExtension = () => { + attempts++; + + // Check again for preserved extension (might be set by deferred detection) + if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) { + extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension; + console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name); + this._tryExtensionLogin(extension); + return; + } + + // Check current window.nostr again + if (window.nostr && this._isRealExtension(window.nostr)) { + extension = window.nostr; + console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name); + this._tryExtensionLogin(extension); + return; + } + + // Keep trying or give up + if (attempts < maxAttempts) { + setTimeout(checkForExtension, 200); + } else { + console.log('Modal: No browser extension found after waiting 2 seconds'); + this._showExtensionRequired(); + } + }; + + // Start checking after a brief delay + setTimeout(checkForExtension, 200); + return; + } + + // Use the single detected extension directly - no choice UI + console.log('Modal: Single extension mode - using extension directly'); + this._tryExtensionLogin(extension); + } + + _detectAllExtensions() { + const extensions = []; + const seenExtensions = new Set(); // Track extensions by object reference to avoid duplicates + + // Extension locations to check (in priority order) + const locations = [ + { path: 'window.navigator?.nostr', name: 'navigator.nostr', displayName: 'Standard Extension (navigator.nostr)', icon: '🌐', getter: () => window.navigator?.nostr }, + { path: 'window.webln?.nostr', name: 'webln.nostr', displayName: 'Alby WebLN Extension', icon: '⚡', getter: () => window.webln?.nostr }, + { path: 'window.alby?.nostr', name: 'alby.nostr', displayName: 'Alby Extension (Direct)', icon: '🐝', getter: () => window.alby?.nostr }, + { path: 'window.nos2x', name: 'nos2x', displayName: 'nos2x Extension', icon: '🔌', getter: () => window.nos2x }, + { path: 'window.flamingo?.nostr', name: 'flamingo.nostr', displayName: 'Flamingo Extension', icon: '🦩', getter: () => window.flamingo?.nostr }, + { path: 'window.mutiny?.nostr', name: 'mutiny.nostr', displayName: 'Mutiny Extension', icon: '⚔️', getter: () => window.mutiny?.nostr }, + { path: 'window.nostrich?.nostr', name: 'nostrich.nostr', displayName: 'Nostrich Extension', icon: '🐦', getter: () => window.nostrich?.nostr }, + { path: 'window.getAlby?.nostr', name: 'getAlby.nostr', displayName: 'getAlby Extension', icon: '🔧', getter: () => window.getAlby?.nostr } + ]; + + // Check each location + for (const location of locations) { + try { + const obj = location.getter(); + + console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name); + + if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) { + extensions.push({ + name: location.name, + displayName: location.displayName, + icon: location.icon, + extension: obj + }); + seenExtensions.add(obj); + console.log(`Modal: ✓ Detected extension at ${location.name} (${obj.constructor?.name})`); + } else if (obj) { + console.log(`Modal: ✗ Filtered out ${location.name} (${obj.constructor?.name})`); + } + } catch (e) { + // Location doesn't exist or can't be accessed + console.log(`Modal: ${location.name} not accessible:`, e.message); + } + } + + // Also check window.nostr but be extra careful to avoid our library + console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name); + + if (window.nostr) { + // Check if window.nostr is our WindowNostr facade with a preserved extension + if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) { + console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension'); + const preservedExtension = window.nostr.existingNostr; + console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name); + + if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) { + extensions.push({ + name: 'window.nostr.existingNostr', + displayName: 'Extension (preserved by WindowNostr)', + icon: '🔑', + extension: preservedExtension + }); + seenExtensions.add(preservedExtension); + console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`); + } + } + // Check if window.nostr is directly a real extension (not our facade) + else if (this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) { + extensions.push({ + name: 'window.nostr', + displayName: 'Extension (window.nostr)', + icon: '🔑', + extension: window.nostr + }); + seenExtensions.add(window.nostr); + console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`); + } else { + console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`); + } + } + + return extensions; + } + + _isRealExtension(obj) { + console.log(`Modal: EXTENSIVE DEBUG - _isRealExtension called with:`, obj); + console.log(`Modal: Object type: ${typeof obj}`); + console.log(`Modal: Object truthy: ${!!obj}`); + + if (!obj || typeof obj !== 'object') { + console.log(`Modal: REJECT - Not an object`); + return false; + } + + console.log(`Modal: getPublicKey type: ${typeof obj.getPublicKey}`); + console.log(`Modal: signEvent type: ${typeof obj.signEvent}`); + + // Must have required Nostr methods + if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { + console.log(`Modal: REJECT - Missing required methods`); + return false; + } + + // Exclude NostrTools library object + if (obj === window.NostrTools) { + console.log(`Modal: REJECT - Is NostrTools object`); + return false; + } + + // Use the EXACT SAME logic as the comprehensive test (lines 804-809) + // This is the key fix - match the comprehensive test's successful detection logic + const constructorName = obj.constructor?.name; + const objectKeys = Object.keys(obj); + + console.log(`Modal: Constructor name: "${constructorName}"`); + console.log(`Modal: Object keys: [${objectKeys.join(', ')}]`); + + // COMPREHENSIVE TEST LOGIC - Accept anything with required methods that's not our specific library classes + const isRealExtension = ( + typeof obj.getPublicKey === 'function' && + typeof obj.signEvent === 'function' && + constructorName !== 'WindowNostr' && // Our library class + constructorName !== 'NostrLite' // Our main class + ); + + console.log(`Modal: Using comprehensive test logic:`); + console.log(` Has getPublicKey: ${typeof obj.getPublicKey === 'function'}`); + console.log(` Has signEvent: ${typeof obj.signEvent === 'function'}`); + console.log(` Not WindowNostr: ${constructorName !== 'WindowNostr'}`); + console.log(` Not NostrLite: ${constructorName !== 'NostrLite'}`); + console.log(` Constructor: "${constructorName}"`); + + // Additional debugging for comparison + const extensionPropChecks = { + _isEnabled: !!obj._isEnabled, + enabled: !!obj.enabled, + kind: !!obj.kind, + _eventEmitter: !!obj._eventEmitter, + _scope: !!obj._scope, + _requests: !!obj._requests, + _pubkey: !!obj._pubkey, + name: !!obj.name, + version: !!obj.version, + description: !!obj.description + }; + + console.log(`Modal: Extension property analysis:`, extensionPropChecks); + + const hasExtensionProps = !!( + obj._isEnabled || obj.enabled || obj.kind || + obj._eventEmitter || obj._scope || obj._requests || obj._pubkey || + obj.name || obj.version || obj.description + ); + + const underscoreKeys = objectKeys.filter(key => key.startsWith('_')); + const hexToUint8Keys = objectKeys.filter(key => key.startsWith('_hex')); + console.log(`Modal: Underscore keys: [${underscoreKeys.join(', ')}]`); + console.log(`Modal: _hex* keys: [${hexToUint8Keys.join(', ')}]`); + + console.log(`Modal: Additional analysis:`); + console.log(` hasExtensionProps: ${hasExtensionProps}`); + console.log(` hasLibraryMethod (_hexToUint8Array): ${objectKeys.includes('_hexToUint8Array')}`); + + console.log(`Modal: COMPREHENSIVE TEST LOGIC RESULT: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); + console.log(`Modal: FINAL DECISION for ${constructorName}: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); + + return isRealExtension; + } + + _showExtensionChoice(extensions) { + this.modalBody.innerHTML = ''; + + const title = document.createElement('h3'); + title.textContent = 'Choose Browser Extension'; + 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: #666666; + font-size: 14px; + font-family: var(--nl-font-family, 'Courier New', monospace); + `; + + this.modalBody.appendChild(title); + this.modalBody.appendChild(description); + + // Create button for each extension + extensions.forEach((ext, index) => { + const button = document.createElement('button'); + button.onclick = () => this._tryExtensionLogin(ext.extension); + button.style.cssText = ` + display: flex; + align-items: center; + width: 100%; + padding: 16px; + margin-bottom: 12px; + 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.borderColor = 'var(--nl-accent-color)'; + button.style.background = 'var(--nl-secondary-color)'; + }; + button.onmouseout = () => { + button.style.borderColor = 'var(--nl-primary-color)'; + button.style.background = 'var(--nl-secondary-color)'; + }; + + const iconDiv = document.createElement('div'); + iconDiv.textContent = ext.icon; + iconDiv.style.cssText = ` + font-size: 24px; + margin-right: 16px; + width: 24px; + text-align: center; + `; + + const contentDiv = document.createElement('div'); + contentDiv.style.cssText = 'flex: 1;'; + + const nameDiv = document.createElement('div'); + nameDiv.textContent = ext.displayName; + nameDiv.style.cssText = ` + font-weight: 600; + margin-bottom: 4px; + 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: #666666; + font-family: var(--nl-font-family, 'Courier New', monospace); + `; + + contentDiv.appendChild(nameDiv); + contentDiv.appendChild(pathDiv); + + button.appendChild(iconDiv); + button.appendChild(contentDiv); + this.modalBody.appendChild(button); + }); + + // Add back button + const backButton = document.createElement('button'); + backButton.textContent = 'Back to Login Options'; + backButton.onclick = () => this._renderLoginOptions(); + backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 20px;'; + + this.modalBody.appendChild(backButton); + } + + async _tryExtensionLogin(extensionObj) { + try { + // Show loading state + this.modalBody.innerHTML = '
🔄 Connecting to extension...
'; + + // Get pubkey from extension + const pubkey = await extensionObj.getPublicKey(); + console.log('Extension provided pubkey:', pubkey); + + // Set extension method with the extension object + this._setAuthMethod('extension', { pubkey, extension: extensionObj }); + + } catch (error) { + console.error('Extension login failed:', error); + this._showError(`Extension login failed: ${error.message}`); + } + } + + _showLocalKeyScreen() { + this.modalBody.innerHTML = ''; + + const description = document.createElement('p'); + description.innerHTML = 'Enter your secret key in nsec or hex format, or generate new.'; + description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;'; + + const textarea = document.createElement('textarea'); + textarea.placeholder = 'Enter your secret key:\n• nsec1... (bech32 format)\n• 64-character hex string'; + textarea.style.cssText = ` + width: 100%; + height: 100px; + padding: 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + margin-bottom: 12px; + resize: none; + font-family: monospace; + font-size: 14px; + box-sizing: border-box; + `; + + // Add real-time format detection + const formatHint = document.createElement('div'); + formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;'; + + const importButton = document.createElement('button'); + importButton.textContent = 'Import Key'; + importButton.disabled = true; + importButton.onclick = () => { + if (!importButton.disabled) { + this._importLocalKey(textarea.value); + } + }; + + // Set initial disabled state + importButton.style.cssText = ` + display: block; + width: 100%; + padding: 12px; + border: var(--nl-border-width) solid var(--nl-muted-color); + border-radius: var(--nl-border-radius); + font-size: 16px; + font-weight: 500; + cursor: not-allowed; + transition: all 0.2s; + font-family: var(--nl-font-family, 'Courier New', monospace); + background: var(--nl-secondary-color); + color: var(--nl-muted-color); + `; + + textarea.oninput = () => { + const value = textarea.value.trim(); + if (!value) { + formatHint.textContent = ''; + // Disable button + importButton.disabled = true; + importButton.style.borderColor = 'var(--nl-muted-color)'; + importButton.style.color = 'var(--nl-muted-color)'; + importButton.style.cursor = 'not-allowed'; + return; + } + + const format = this._detectKeyFormat(value); + if (format === 'nsec') { + formatHint.textContent = '✅ Valid nsec format detected'; + formatHint.style.color = '#059669'; + // Enable button + importButton.disabled = false; + importButton.style.borderColor = 'var(--nl-primary-color)'; + importButton.style.color = 'var(--nl-primary-color)'; + importButton.style.cursor = 'pointer'; + } else if (format === 'hex') { + formatHint.textContent = '✅ Valid hex format detected'; + formatHint.style.color = '#059669'; + // Enable button + importButton.disabled = false; + importButton.style.borderColor = 'var(--nl-primary-color)'; + importButton.style.color = 'var(--nl-primary-color)'; + importButton.style.cursor = 'pointer'; + } else { + formatHint.textContent = '❌ Invalid key format - must be nsec1... or 64-character hex'; + formatHint.style.color = '#dc2626'; + // Disable button + importButton.disabled = true; + importButton.style.borderColor = 'var(--nl-muted-color)'; + importButton.style.color = 'var(--nl-muted-color)'; + importButton.style.cursor = 'not-allowed'; + } + }; + + const backButton = document.createElement('button'); + backButton.textContent = 'Back'; + backButton.onclick = () => this._renderLoginOptions(); + backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; + + this.modalBody.appendChild(description); + this.modalBody.appendChild(textarea); + this.modalBody.appendChild(formatHint); + this.modalBody.appendChild(importButton); + this.modalBody.appendChild(backButton); + + // Add click handler for the "generate new" link + const generateLink = document.getElementById('generate-new'); + if (generateLink) { + generateLink.addEventListener('mouseenter', () => { + generateLink.style.color = 'var(--nl-accent-color)'; + }); + generateLink.addEventListener('mouseleave', () => { + generateLink.style.color = 'var(--nl-primary-color)'; + }); + generateLink.addEventListener('click', () => { + this._generateNewLocalKey(textarea, formatHint); + }); + } + } + + _generateNewLocalKey(textarea, formatHint) { + try { + // Generate a new secret key using NostrTools + const sk = window.NostrTools.generateSecretKey(); + const nsec = window.NostrTools.nip19.nsecEncode(sk); + + // Set the generated key in the textarea + textarea.value = nsec; + + // Trigger the oninput event to properly validate and enable the button + if (textarea.oninput) { + textarea.oninput(); + } + + console.log('Generated new local secret key (nsec format)'); + + } catch (error) { + console.error('Failed to generate local key:', error); + formatHint.textContent = '❌ Failed to generate key - NostrTools not available'; + formatHint.style.color = '#dc2626'; + } + } + + _createLocalKey() { + try { + const sk = window.NostrTools.generateSecretKey(); + const pk = window.NostrTools.getPublicKey(sk); + const nsec = window.NostrTools.nip19.nsecEncode(sk); + const npub = window.NostrTools.nip19.npubEncode(pk); + + this._showKeyDisplay(pk, nsec, 'created'); + } catch (error) { + this._showError('Failed to create key: ' + error.message); + } + } + + + + _detectKeyFormat(keyValue) { + const trimmed = keyValue.trim(); + + // Check for nsec format + if (trimmed.startsWith('nsec1') && trimmed.length === 63) { + try { + window.NostrTools.nip19.decode(trimmed); + return 'nsec'; + } catch { + return 'invalid'; + } + } + + // Check for hex format (64 characters, valid hex) + if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) { + return 'hex'; + } + + return 'invalid'; + } + + _importLocalKey(keyValue) { + try { + const trimmed = keyValue.trim(); + if (!trimmed) { + throw new Error('Please enter a secret key'); + } + + const format = this._detectKeyFormat(trimmed); + let sk; + + if (format === 'nsec') { + // Decode nsec format - this returns Uint8Array + const decoded = window.NostrTools.nip19.decode(trimmed); + if (decoded.type !== 'nsec') { + throw new Error('Invalid nsec format'); + } + sk = decoded.data; // This is already Uint8Array + } else if (format === 'hex') { + // Convert hex string to Uint8Array + sk = this._hexToUint8Array(trimmed); + // Test that it's a valid secret key by trying to get public key + window.NostrTools.getPublicKey(sk); + } else { + throw new Error('Invalid key format. Please enter either nsec1... or 64-character hex string'); + } + + // Generate public key and encoded formats + const pk = window.NostrTools.getPublicKey(sk); + const nsec = window.NostrTools.nip19.nsecEncode(sk); + const npub = window.NostrTools.nip19.npubEncode(pk); + + this._showKeyDisplay(pk, nsec, 'imported'); + } catch (error) { + this._showError('Invalid key: ' + error.message); + } + } + + _hexToUint8Array(hex) { + // Convert hex string to Uint8Array + if (hex.length % 2 !== 0) { + throw new Error('Invalid hex string length'); + } + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return bytes; + } + + _showKeyDisplay(pubkey, nsec, action) { + this.modalBody.innerHTML = ''; + + const title = document.createElement('h3'); + title.textContent = `Key ${action} successfully!`; + title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;'; + + const warningDiv = document.createElement('div'); + warningDiv.textContent = '⚠️ Save your secret key securely!'; + warningDiv.style.cssText = 'background: #fef3c7; color: #92400e; padding: 12px; border-radius: 6px; margin-bottom: 16px; font-size: 12px;'; + + // Helper function to create copy button + const createCopyButton = (text, label) => { + const copyBtn = document.createElement('button'); + copyBtn.textContent = `Copy ${label}`; + copyBtn.style.cssText = ` + margin-left: 8px; + padding: 4px 8px; + font-size: 10px; + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: 1px solid var(--nl-primary-color); + border-radius: 4px; + cursor: pointer; + font-family: var(--nl-font-family, 'Courier New', monospace); + `; + copyBtn.onclick = async (e) => { + e.preventDefault(); + try { + await navigator.clipboard.writeText(text); + const originalText = copyBtn.textContent; + copyBtn.textContent = '✓ Copied!'; + copyBtn.style.color = '#059669'; + setTimeout(() => { + copyBtn.textContent = originalText; + copyBtn.style.color = 'var(--nl-primary-color)'; + }, 2000); + } catch (err) { + console.error('Failed to copy:', err); + copyBtn.textContent = '✗ Failed'; + copyBtn.style.color = '#dc2626'; + setTimeout(() => { + copyBtn.textContent = originalText; + copyBtn.style.color = 'var(--nl-primary-color)'; + }, 2000); + } + }; + return copyBtn; + }; + + // Convert pubkey to hex for verification + const pubkeyHex = typeof pubkey === 'string' ? pubkey : Array.from(pubkey).map(b => b.toString(16).padStart(2, '0')).join(''); + + // Decode nsec to get secret key as hex + let secretKeyHex = ''; + try { + const decoded = window.NostrTools.nip19.decode(nsec); + secretKeyHex = Array.from(decoded.data).map(b => b.toString(16).padStart(2, '0')).join(''); + } catch (err) { + console.error('Failed to decode nsec for hex display:', err); + } + + // Secret Key Section + const nsecSection = document.createElement('div'); + nsecSection.style.cssText = 'margin-bottom: 16px;'; + + const nsecLabel = document.createElement('div'); + nsecLabel.innerHTML = 'Your Secret Key (nsec):'; + nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; + + const nsecContainer = document.createElement('div'); + nsecContainer.style.cssText = 'margin-bottom: 8px;'; + + const nsecCode = document.createElement('code'); + nsecCode.textContent = nsec; + nsecCode.style.cssText = ` + display: block; + word-wrap: break-word; + overflow-wrap: break-word; + background: #f3f4f6; + padding: 6px; + border-radius: 4px; + font-size: 10px; + line-height: 1.3; + font-family: 'Courier New', monospace; + margin-bottom: 4px; + `; + + const nsecCopyBtn = createCopyButton(nsec, 'nsec'); + nsecCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;'; + + nsecContainer.appendChild(nsecCode); + nsecContainer.appendChild(nsecCopyBtn); + nsecSection.appendChild(nsecLabel); + nsecSection.appendChild(nsecContainer); + + // Secret Key Hex Section + if (secretKeyHex) { + const secretHexLabel = document.createElement('div'); + secretHexLabel.innerHTML = 'Secret Key (hex):'; + secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; + + const secretHexContainer = document.createElement('div'); + secretHexContainer.style.cssText = 'margin-bottom: 8px;'; + + const secretHexCode = document.createElement('code'); + secretHexCode.textContent = secretKeyHex; + secretHexCode.style.cssText = ` + display: block; + word-wrap: break-word; + overflow-wrap: break-word; + background: #f3f4f6; + padding: 6px; + border-radius: 4px; + font-size: 10px; + line-height: 1.3; + font-family: 'Courier New', monospace; + margin-bottom: 4px; + `; + + const secretHexCopyBtn = createCopyButton(secretKeyHex, 'hex'); + secretHexCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;'; + + secretHexContainer.appendChild(secretHexCode); + secretHexContainer.appendChild(secretHexCopyBtn); + nsecSection.appendChild(secretHexLabel); + nsecSection.appendChild(secretHexContainer); + } + + // Public Key Section + const npubSection = document.createElement('div'); + npubSection.style.cssText = 'margin-bottom: 16px;'; + + const npub = window.NostrTools.nip19.npubEncode(pubkey); + + const npubLabel = document.createElement('div'); + npubLabel.innerHTML = 'Your Public Key (npub):'; + npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; + + const npubContainer = document.createElement('div'); + npubContainer.style.cssText = 'margin-bottom: 8px;'; + + const npubCode = document.createElement('code'); + npubCode.textContent = npub; + npubCode.style.cssText = ` + display: block; + word-wrap: break-word; + overflow-wrap: break-word; + background: #f3f4f6; + padding: 6px; + border-radius: 4px; + font-size: 10px; + line-height: 1.3; + font-family: 'Courier New', monospace; + margin-bottom: 4px; + `; + + const npubCopyBtn = createCopyButton(npub, 'npub'); + npubCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;'; + + npubContainer.appendChild(npubCode); + npubContainer.appendChild(npubCopyBtn); + npubSection.appendChild(npubLabel); + npubSection.appendChild(npubContainer); + + // Public Key Hex Section + const pubHexLabel = document.createElement('div'); + pubHexLabel.innerHTML = 'Public Key (hex):'; + pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; + + const pubHexContainer = document.createElement('div'); + pubHexContainer.style.cssText = ''; + + const pubHexCode = document.createElement('code'); + pubHexCode.textContent = pubkeyHex; + pubHexCode.style.cssText = ` + display: block; + word-wrap: break-word; + overflow-wrap: break-word; + background: #f3f4f6; + padding: 6px; + border-radius: 4px; + font-size: 10px; + line-height: 1.3; + font-family: 'Courier New', monospace; + margin-bottom: 4px; + `; + + const pubHexCopyBtn = createCopyButton(pubkeyHex, 'hex'); + pubHexCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;'; + + pubHexContainer.appendChild(pubHexCode); + pubHexContainer.appendChild(pubHexCopyBtn); + npubSection.appendChild(pubHexLabel); + npubSection.appendChild(pubHexContainer); + + const continueButton = document.createElement('button'); + continueButton.textContent = 'Continue'; + continueButton.onclick = () => this._setAuthMethod('local', { secret: nsec, pubkey }); + continueButton.style.cssText = this._getButtonStyle(); + + this.modalBody.appendChild(title); + this.modalBody.appendChild(warningDiv); + this.modalBody.appendChild(nsecSection); + this.modalBody.appendChild(npubSection); + this.modalBody.appendChild(continueButton); + } + + _setAuthMethod(method, options = {}) { + console.log('Modal: _setAuthMethod called with:', method, options); + + // CRITICAL: Never install facade for extension methods - leave window.nostr as the extension + if (method === 'extension') { + console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension'); + + // Save extension authentication state using global setAuthState function + if (typeof window.setAuthState === 'function') { + console.log('Modal: Saving extension auth state to storage'); + window.setAuthState({ method, ...options }, { isolateSession: this.options?.isolateSession }); + } + + // Emit auth method selection directly for extension + const event = new CustomEvent('nlMethodSelected', { + detail: { method, ...options } + }); + window.dispatchEvent(event); + + this.close(); + return; + } + + // FOR NON-EXTENSION METHODS: Force-install facade with resilience + console.log('Modal: Non-extension method - FORCE-INSTALLING facade with resilience:', method); + + // Store the current extension if any (for potential restoration later) + const currentExtension = (window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null; + + // Get NostrLite instance for facade operations + const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; + if (!nostrLiteInstance || typeof nostrLiteInstance._installFacade !== 'function') { + console.error('Modal: Cannot access NostrLite instance or _installFacade method'); + // Fallback: emit event anyway + const event = new CustomEvent('nlMethodSelected', { + detail: { method, ...options } + }); + window.dispatchEvent(event); + this.close(); + return; + } + + // IMMEDIATE FACADE INSTALLATION + console.log('Modal: Installing WindowNostr facade immediately for method:', method); + const preservedExtension = nostrLiteInstance.preservedExtension || currentExtension; + nostrLiteInstance._installFacade(preservedExtension, true); + console.log('Modal: WindowNostr facade force-installed, current window.nostr:', window.nostr?.constructor?.name); + + // DELAYED FACADE RESILIENCE - Reinstall after extension override attempts + const forceReinstallFacade = () => { + console.log('Modal: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name); + + // If facade was overridden by extension, reinstall it + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('Modal: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method); + nostrLiteInstance._installFacade(preservedExtension, true); + console.log('Modal: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name); + + // Schedule another check in case of persistent extension override + setTimeout(() => { + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('Modal: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method); + nostrLiteInstance._installFacade(preservedExtension, true); + } + }, 1000); + } else { + console.log('Modal: Facade persistence verified - no override detected'); + } + }; + + // Schedule resilience checks at multiple intervals + setTimeout(forceReinstallFacade, 100); // Quick check + setTimeout(forceReinstallFacade, 500); // Main check + setTimeout(forceReinstallFacade, 1500); // Final check + + // Emit auth method selection + const event = new CustomEvent('nlMethodSelected', { + detail: { method, ...options } + }); + window.dispatchEvent(event); + + this.close(); + } + + _showError(message) { + this.modalBody.innerHTML = ''; + + const errorDiv = document.createElement('div'); + errorDiv.style.cssText = 'background: #fee2e2; color: #dc2626; padding: 16px; border-radius: 6px; margin-bottom: 16px;'; + errorDiv.innerHTML = `Error: ${message}`; + + const backButton = document.createElement('button'); + backButton.textContent = 'Back'; + backButton.onclick = () => this._renderLoginOptions(); + backButton.style.cssText = this._getButtonStyle('secondary'); + + this.modalBody.appendChild(errorDiv); + this.modalBody.appendChild(backButton); + } + + _showExtensionRequired() { + this.modalBody.innerHTML = ''; + + const title = document.createElement('h3'); + title.textContent = 'Browser Extension Required'; + title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;'; + + const message = document.createElement('p'); + message.innerHTML = ` + Please install a Nostr browser extension and refresh the page.

+ Important: If you have multiple extensions installed, please disable all but one to avoid conflicts. +

+ Popular extensions: Alby, nos2x, Flamingo + `; + message.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px; line-height: 1.4;'; + + const backButton = document.createElement('button'); + backButton.textContent = 'Back'; + backButton.onclick = () => this._renderLoginOptions(); + backButton.style.cssText = this._getButtonStyle('secondary'); + + this.modalBody.appendChild(title); + this.modalBody.appendChild(message); + this.modalBody.appendChild(backButton); + } + + _showConnectScreen() { + this.modalBody.innerHTML = ''; + + const description = document.createElement('p'); + description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.'; + description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;'; + + const formGroup = document.createElement('div'); + formGroup.style.cssText = 'margin-bottom: 20px;'; + + const label = document.createElement('label'); + label.textContent = 'Bunker Public Key:'; + label.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;'; + + const pubkeyInput = document.createElement('input'); + pubkeyInput.type = 'text'; + pubkeyInput.placeholder = 'bunker://pubkey?relay=..., bunker:hex, hex, or npub...'; + pubkeyInput.style.cssText = ` + width: 100%; + padding: 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + margin-bottom: 12px; + font-family: monospace; + box-sizing: border-box; + `; + + // Add real-time bunker key validation + const formatHint = document.createElement('div'); + formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;'; + + const connectButton = document.createElement('button'); + connectButton.textContent = 'Connect to Bunker'; + connectButton.disabled = true; + connectButton.onclick = () => { + if (!connectButton.disabled) { + this._handleNip46Connect(pubkeyInput.value); + } + }; + + // Set initial disabled state + connectButton.style.cssText = ` + display: block; + width: 100%; + padding: 12px; + border: var(--nl-border-width) solid var(--nl-muted-color); + border-radius: var(--nl-border-radius); + font-size: 16px; + font-weight: 500; + cursor: not-allowed; + transition: all 0.2s; + font-family: var(--nl-font-family, 'Courier New', monospace); + background: var(--nl-secondary-color); + color: var(--nl-muted-color); + margin-bottom: 12px; + `; + + pubkeyInput.oninput = () => { + const value = pubkeyInput.value.trim(); + if (!value) { + formatHint.textContent = ''; + // Disable button + connectButton.disabled = true; + connectButton.style.borderColor = 'var(--nl-muted-color)'; + connectButton.style.color = 'var(--nl-muted-color)'; + connectButton.style.cursor = 'not-allowed'; + return; + } + + const isValid = this._validateBunkerKey(value); + if (isValid) { + formatHint.textContent = '✅ Valid bunker connection format detected'; + formatHint.style.color = '#059669'; + // Enable button + connectButton.disabled = false; + connectButton.style.borderColor = 'var(--nl-primary-color)'; + connectButton.style.color = 'var(--nl-primary-color)'; + connectButton.style.cursor = 'pointer'; + } else { + formatHint.textContent = '❌ Invalid format - must be bunker://, npub, or 64-char hex'; + formatHint.style.color = '#dc2626'; + // Disable button + connectButton.disabled = true; + connectButton.style.borderColor = 'var(--nl-muted-color)'; + connectButton.style.color = 'var(--nl-muted-color)'; + connectButton.style.cursor = 'not-allowed'; + } + }; + + const backButton = document.createElement('button'); + backButton.textContent = 'Back'; + backButton.onclick = () => this._renderLoginOptions(); + backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; + + formGroup.appendChild(label); + formGroup.appendChild(pubkeyInput); + formGroup.appendChild(formatHint); + + this.modalBody.appendChild(description); + this.modalBody.appendChild(formGroup); + this.modalBody.appendChild(connectButton); + this.modalBody.appendChild(backButton); + } + + _validateBunkerKey(bunkerKey) { + try { + const trimmed = bunkerKey.trim(); + + // Check for bunker:// format + if (trimmed.startsWith('bunker://')) { + // Should have format: bunker://pubkey or bunker://pubkey?param=value + const match = trimmed.match(/^bunker:\/\/([0-9a-fA-F]{64})(\?.*)?$/); + return !!match; + } + + // Check for npub format + if (trimmed.startsWith('npub1') && trimmed.length === 63) { + try { + if (window.NostrTools?.nip19) { + const decoded = window.NostrTools.nip19.decode(trimmed); + return decoded.type === 'npub'; + } + } catch { + return false; + } + } + + // Check for hex format (64 characters, valid hex) + if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) { + return true; + } + + return false; + } catch (error) { + console.log('Bunker key validation failed:', error.message); + return false; + } + } + + _handleNip46Connect(bunkerPubkey) { + if (!bunkerPubkey || !bunkerPubkey.length) { + this._showError('Bunker pubkey is required'); + return; + } + + this._showNip46Connecting(bunkerPubkey); + this._performNip46Connect(bunkerPubkey); + } + + _showNip46Connecting(bunkerPubkey) { + this.modalBody.innerHTML = ''; + + const title = document.createElement('h3'); + title.textContent = 'Connecting to Remote Signer...'; + title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;'; + + const description = document.createElement('p'); + description.textContent = 'Establishing secure connection to your remote signer.'; + description.style.cssText = 'margin-bottom: 20px; color: #6b7280;'; + + // Normalize bunker pubkey for display (= show original format if bunker: prefix) + const displayPubkey = bunkerPubkey.startsWith('bunker:') || bunkerPubkey.startsWith('npub') || bunkerPubkey.length === 64 ? bunkerPubkey : bunkerPubkey; + + const bunkerInfo = document.createElement('div'); + bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;'; + bunkerInfo.innerHTML = ` + Connecting to bunker:
+ Connection: ${displayPubkey}
+ Connection string contains all necessary relay information. + `; + + const connectingDiv = document.createElement('div'); + connectingDiv.style.cssText = 'text-align: center; color: #6b7280;'; + connectingDiv.innerHTML = ` +
+
Please wait while we establish the connection...
+
This may take a few seconds
+ `; + + this.modalBody.appendChild(title); + this.modalBody.appendChild(description); + this.modalBody.appendChild(bunkerInfo); + this.modalBody.appendChild(connectingDiv); + } + + async _performNip46Connect(bunkerPubkey) { + try { + console.log('Starting NIP-46 connection to bunker:', bunkerPubkey); + + // Check if nostr-tools NIP-46 is available + if (!window.NostrTools?.nip46) { + throw new Error('nostr-tools NIP-46 module not available'); + } + + // Use nostr-tools to parse bunker input - this handles all formats correctly + console.log('Parsing bunker input with nostr-tools...'); + const bunkerPointer = await window.NostrTools.nip46.parseBunkerInput(bunkerPubkey); + + if (!bunkerPointer) { + throw new Error('Unable to parse bunker connection string or resolve NIP-05 identifier'); + } + + console.log('Parsed bunker pointer:', bunkerPointer); + + // Create local client keypair for this session + const localSecretKey = window.NostrTools.generateSecretKey(); + console.log('Generated local client keypair for NIP-46 session'); + + // Use nostr-tools BunkerSigner factory method (not constructor - it's private) + console.log('Creating nip46 BunkerSigner...'); + const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, { + onauth: (url) => { + console.log('Received auth URL from bunker:', url); + // Open auth URL in popup or redirect + window.open(url, '_blank', 'width=600,height=800'); + } + }); + + console.log('NIP-46 BunkerSigner created successfully'); + + // 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'); + + // Get the user's public key from the bunker + console.log('Getting public key from bunker...'); + const userPubkey = await signer.getPublicKey(); + console.log('NIP-46 user public key:', userPubkey); + + // Store the NIP-46 authentication info + const nip46Info = { + pubkey: userPubkey, + signer: { + method: 'nip46', + remotePubkey: bunkerPointer.pubkey, + bunkerSigner: signer, + secret: bunkerPointer.secret, + relays: bunkerPointer.relays + } + }; + + console.log('NOSTR_LOGIN_LITE NIP-46 connection established successfully!'); + + // Set as current auth method + this._setAuthMethod('nip46', nip46Info); + return; + + } catch (error) { + console.error('NIP-46 connection failed:', error); + this._showNip46Error(error.message); + } + } + + _showNip46Error(message) { + this.modalBody.innerHTML = ''; + + const title = document.createElement('h3'); + title.textContent = 'Connection Failed'; + title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #dc2626;'; + + const errorMsg = document.createElement('p'); + errorMsg.textContent = `Unable to connect to remote signer: ${message}`; + errorMsg.style.cssText = 'margin-bottom: 20px; color: #6b7280;'; + + const retryButton = document.createElement('button'); + retryButton.textContent = 'Try Again'; + retryButton.onclick = () => this._showConnectScreen(); + retryButton.style.cssText = this._getButtonStyle(); + + const backButton = document.createElement('button'); + backButton.textContent = 'Back to Options'; + backButton.onclick = () => this._renderLoginOptions(); + backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; + + this.modalBody.appendChild(title); + this.modalBody.appendChild(errorMsg); + this.modalBody.appendChild(retryButton); + this.modalBody.appendChild(backButton); + } + + _handleReadonly() { + // Set read-only mode + this._setAuthMethod('readonly'); + } + + _showSeedPhraseScreen() { + this.modalBody.innerHTML = ''; + + const description = document.createElement('p'); + description.innerHTML = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts, or generate new.'; + description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;'; + + const textarea = document.createElement('textarea'); + // Remove default placeholder text as requested + textarea.placeholder = ''; + textarea.style.cssText = ` + width: 100%; + height: 100px; + padding: 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + margin-bottom: 12px; + resize: none; + font-family: monospace; + font-size: 14px; + box-sizing: border-box; + `; + + // Add real-time mnemonic validation + const formatHint = document.createElement('div'); + formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;'; + + const importButton = document.createElement('button'); + importButton.textContent = 'Import Accounts'; + importButton.disabled = true; + importButton.onclick = () => { + if (!importButton.disabled) { + this._importFromSeedPhrase(textarea.value); + } + }; + + // Set initial disabled state + importButton.style.cssText = ` + display: block; + width: 100%; + padding: 12px; + border: var(--nl-border-width) solid var(--nl-muted-color); + border-radius: var(--nl-border-radius); + font-size: 16px; + font-weight: 500; + cursor: not-allowed; + transition: all 0.2s; + font-family: var(--nl-font-family, 'Courier New', monospace); + background: var(--nl-secondary-color); + color: var(--nl-muted-color); + `; + + textarea.oninput = () => { + const value = textarea.value.trim(); + if (!value) { + formatHint.textContent = ''; + // Disable button + importButton.disabled = true; + importButton.style.borderColor = 'var(--nl-muted-color)'; + importButton.style.color = 'var(--nl-muted-color)'; + importButton.style.cursor = 'not-allowed'; + return; + } + + const isValid = this._validateMnemonic(value); + if (isValid) { + const wordCount = value.split(/\s+/).length; + formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`; + formatHint.style.color = '#059669'; + // Enable button + importButton.disabled = false; + importButton.style.borderColor = 'var(--nl-primary-color)'; + importButton.style.color = 'var(--nl-primary-color)'; + importButton.style.cursor = 'pointer'; + } else { + formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words'; + formatHint.style.color = '#dc2626'; + // Disable button + importButton.disabled = true; + importButton.style.borderColor = 'var(--nl-muted-color)'; + importButton.style.color = 'var(--nl-muted-color)'; + importButton.style.cursor = 'not-allowed'; + } + }; + + const backButton = document.createElement('button'); + backButton.textContent = 'Back'; + backButton.onclick = () => this._renderLoginOptions(); + backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; + + this.modalBody.appendChild(description); + this.modalBody.appendChild(textarea); + this.modalBody.appendChild(formatHint); + this.modalBody.appendChild(importButton); + this.modalBody.appendChild(backButton); + + // Add click handler for the "generate new" link + const generateLink = document.getElementById('generate-new'); + if (generateLink) { + generateLink.addEventListener('mouseenter', () => { + generateLink.style.color = 'var(--nl-accent-color)'; + }); + generateLink.addEventListener('mouseleave', () => { + generateLink.style.color = 'var(--nl-primary-color)'; + }); + generateLink.addEventListener('click', () => { + this._generateNewSeedPhrase(textarea, formatHint); + }); + } + } + + _generateNewSeedPhrase(textarea, formatHint) { + try { + // Check if NIP-06 is available + if (!window.NostrTools?.nip06) { + throw new Error('NIP-06 not available in bundle'); + } + + // Generate a random 12-word mnemonic using NostrTools + const mnemonic = window.NostrTools.nip06.generateSeedWords(); + + // Set the generated mnemonic in the textarea + textarea.value = mnemonic; + + // Trigger the oninput event to properly validate and enable the button + if (textarea.oninput) { + textarea.oninput(); + } + + console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words'); + + } catch (error) { + console.error('Failed to generate seed phrase:', error); + formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available'; + formatHint.style.color = '#dc2626'; + } + } + + _validateMnemonic(mnemonic) { + try { + // Check if NIP-06 is available + if (!window.NostrTools?.nip06) { + console.error('NIP-06 not available in bundle'); + return false; + } + + const words = mnemonic.trim().split(/\s+/); + + // Must be 12 or 24 words + if (words.length !== 12 && words.length !== 24) { + return false; + } + + // Try to validate using NostrTools nip06 - this will throw if invalid + window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0); + return true; + } catch (error) { + console.log('Mnemonic validation failed:', error.message); + return false; + } + } + + _importFromSeedPhrase(mnemonic) { + try { + const trimmed = mnemonic.trim(); + if (!trimmed) { + throw new Error('Please enter a mnemonic seed phrase'); + } + + // Validate the mnemonic + if (!this._validateMnemonic(trimmed)) { + throw new Error('Invalid mnemonic. Please enter a valid 12 or 24-word BIP-39 seed phrase'); + } + + // Generate accounts 0-5 using NIP-06 + const accounts = []; + for (let i = 0; i < 6; i++) { + try { + const privateKey = window.NostrTools.nip06.privateKeyFromSeedWords(trimmed, '', i); + const publicKey = window.NostrTools.getPublicKey(privateKey); + const nsec = window.NostrTools.nip19.nsecEncode(privateKey); + const npub = window.NostrTools.nip19.npubEncode(publicKey); + + accounts.push({ + index: i, + privateKey, + publicKey, + nsec, + npub + }); + } catch (error) { + console.error(`Failed to derive account ${i}:`, error); + } + } + + if (accounts.length === 0) { + throw new Error('Failed to derive any accounts from seed phrase'); + } + + console.log(`Successfully derived ${accounts.length} accounts from seed phrase`); + this._showAccountSelection(accounts); + + } catch (error) { + console.error('Seed phrase import failed:', error); + this._showError('Seed phrase import failed: ' + error.message); + } + } + + _showAccountSelection(accounts) { + this.modalBody.innerHTML = ''; + + const description = document.createElement('p'); + description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`; + description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;'; + + this.modalBody.appendChild(description); + + // Create table for account selection + const table = document.createElement('table'); + table.style.cssText = ` + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + font-family: var(--nl-font-family, 'Courier New', monospace); + font-size: 12px; + `; + + // Table header + const thead = document.createElement('thead'); + thead.innerHTML = ` + + # + Use + + `; + table.appendChild(thead); + + // Table body + const tbody = document.createElement('tbody'); + accounts.forEach(account => { + const row = document.createElement('tr'); + row.style.cssText = 'border: 1px solid #d1d5db;'; + + const indexCell = document.createElement('td'); + indexCell.textContent = account.index; + indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;'; + + const actionCell = document.createElement('td'); + actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;'; + + // Show truncated npub in the button + const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`; + + const selectButton = document.createElement('button'); + selectButton.textContent = truncatedNpub; + selectButton.onclick = () => this._selectAccount(account); + selectButton.style.cssText = ` + width: 100%; + padding: 8px 12px; + font-size: 11px; + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: 1px solid var(--nl-primary-color); + border-radius: 4px; + cursor: pointer; + font-family: 'Courier New', monospace; + text-align: center; + `; + selectButton.onmouseover = () => { + selectButton.style.borderColor = 'var(--nl-accent-color)'; + }; + selectButton.onmouseout = () => { + selectButton.style.borderColor = 'var(--nl-primary-color)'; + }; + + actionCell.appendChild(selectButton); + + row.appendChild(indexCell); + row.appendChild(actionCell); + tbody.appendChild(row); + }); + table.appendChild(tbody); + + this.modalBody.appendChild(table); + + // Back button + const backButton = document.createElement('button'); + backButton.textContent = 'Back to Seed Phrase'; + backButton.onclick = () => this._showSeedPhraseScreen(); + backButton.style.cssText = this._getButtonStyle('secondary'); + + this.modalBody.appendChild(backButton); + } + + _selectAccount(account) { + console.log('Selected account:', account.index, account.npub); + + // Use the same auth method as local keys, but with seedphrase identifier + this._setAuthMethod('local', { + secret: account.nsec, + pubkey: account.publicKey, + source: 'seedphrase', + accountIndex: account.index + }); + } + + _showOtpScreen() { + // Placeholder for OTP functionality + this._showError('OTP/DM not yet implemented - coming soon!'); + } + + _getButtonStyle(type = 'primary') { + const baseStyle = ` + display: block; + width: 100%; + padding: 12px; + 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: var(--nl-secondary-color); + color: var(--nl-primary-color); + `; + } else { + return baseStyle + ` + background: #cccccc; + color: var(--nl-primary-color); + `; + } + } + + // Public API + static init(options) { + if (Modal.instance) return Modal.instance; + Modal.instance = new Modal(options); + return Modal.instance; + } + + static getInstance() { + return Modal.instance; + } +} + +// Initialize global instance +let modalInstance = null; + +window.addEventListener('load', () => { + modalInstance = new Modal(); +}); + + +// ====================================== +// FloatingTab Component (Recovered from git history) +// ====================================== + +class FloatingTab { + constructor(modal, options = {}) { + this.modal = modal; + this.options = { + enabled: true, + hPosition: 1.0, // 0.0 = left, 1.0 = right + vPosition: 0.5, // 0.0 = top, 1.0 = bottom + offset: { x: 0, y: 0 }, + appearance: { + style: 'pill', // 'pill', 'square', 'circle' + theme: 'auto', // 'auto', 'light', 'dark' + icon: '', + text: 'Login', + iconOnly: false + }, + behavior: { + hideWhenAuthenticated: true, + showUserInfo: true, + autoSlide: true, + persistent: false + }, + getUserInfo: false, + getUserRelay: [], + ...options + }; + + this.userProfile = null; + this.container = null; + this.isVisible = false; + + if (this.options.enabled) { + this._init(); + } + } + + _init() { + console.log('FloatingTab: Initializing with options:', this.options); + this._createContainer(); + this._setupEventListeners(); + this._updateAppearance(); + this._position(); + this.show(); + } + + // Get authentication state from authoritative source (Global Storage-Based Function) + _getAuthState() { + return window.NOSTR_LOGIN_LITE?.getAuthState?.() || null; + } + + + _createContainer() { + // Remove existing floating tab if any + const existingTab = document.getElementById('nl-floating-tab'); + if (existingTab) { + existingTab.remove(); + } + + this.container = document.createElement('div'); + this.container.id = 'nl-floating-tab'; + this.container.className = 'nl-floating-tab'; + + // Base styles - positioning and behavior + this.container.style.cssText = ` + position: fixed; + z-index: 9999; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-size: 14px; + font-weight: 500; + padding: 8px 16px; + min-width: 80px; + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + `; + + document.body.appendChild(this.container); + } + + _setupEventListeners() { + if (!this.container) return; + + // Click handler + this.container.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this._handleClick(); + }); + + // Hover effects + this.container.addEventListener('mouseenter', () => { + if (this.options.behavior.autoSlide) { + this._slideIn(); + } + }); + + this.container.addEventListener('mouseleave', () => { + if (this.options.behavior.autoSlide) { + this._slideOut(); + } + }); + + // Listen for authentication events + window.addEventListener('nlMethodSelected', (e) => { + console.log('🔍 FloatingTab: Authentication method selected event received'); + console.log('🔍 FloatingTab: Event detail:', e.detail); + this._handleAuth(e.detail); + }); + + window.addEventListener('nlAuthRestored', (e) => { + console.log('🔍 FloatingTab: ✅ Authentication restored event received'); + console.log('🔍 FloatingTab: Event detail:', e.detail); + console.log('🔍 FloatingTab: Calling _handleAuth with restored data...'); + this._handleAuth(e.detail); + }); + + window.addEventListener('nlLogout', () => { + console.log('🔍 FloatingTab: Logout event received'); + this._handleLogout(); + }); + + // Check for existing authentication state on initialization + window.addEventListener('load', () => { + setTimeout(() => { + this._checkExistingAuth(); + }, 1000); // Wait 1 second for all initialization to complete + }); + } + + // Check for existing authentication on page load + async _checkExistingAuth() { + console.log('🔍 FloatingTab: === _checkExistingAuth START ==='); + + try { + const storageKey = 'nostr_login_lite_auth'; + let storedAuth = null; + + // Try sessionStorage first, then localStorage + if (sessionStorage.getItem(storageKey)) { + storedAuth = JSON.parse(sessionStorage.getItem(storageKey)); + console.log('🔍 FloatingTab: Found auth in sessionStorage:', storedAuth.method); + } else if (localStorage.getItem(storageKey)) { + storedAuth = JSON.parse(localStorage.getItem(storageKey)); + console.log('🔍 FloatingTab: Found auth in localStorage:', storedAuth.method); + } + + if (storedAuth) { + // Check if stored auth is not expired + const maxAge = storedAuth.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000; + if (Date.now() - storedAuth.timestamp <= maxAge) { + console.log('🔍 FloatingTab: Found valid stored auth, simulating auth event'); + + // Create auth data object for FloatingTab + const authData = { + method: storedAuth.method, + pubkey: storedAuth.pubkey + }; + + // For extensions, try to find the extension + if (storedAuth.method === 'extension') { + if (window.nostr && window.nostr.constructor?.name !== 'WindowNostr') { + authData.extension = window.nostr; + } + } + + await this._handleAuth(authData); + } else { + console.log('🔍 FloatingTab: Stored auth expired, clearing'); + sessionStorage.removeItem(storageKey); + localStorage.removeItem(storageKey); + } + } else { + console.log('🔍 FloatingTab: No existing authentication found'); + } + + } catch (error) { + console.error('🔍 FloatingTab: Error checking existing auth:', error); + } + + console.log('🔍 FloatingTab: === _checkExistingAuth END ==='); + } + + _handleClick() { + console.log('FloatingTab: Clicked'); + + const authState = this._getAuthState(); + if (authState && this.options.behavior.showUserInfo) { + // Show user menu or profile options + this._showUserMenu(); + } else { + // Always open login modal (consistent with login buttons) + if (this.modal) { + this.modal.open({ startScreen: 'login' }); + } + } + } + + // Check if object is a real extension (same logic as NostrLite._isRealExtension) + _isRealExtension(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + + // Must have required Nostr methods + if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { + return false; + } + + // Exclude our own library classes + const constructorName = obj.constructor?.name; + if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') { + return false; + } + + // Exclude NostrTools library object + if (obj === window.NostrTools) { + return false; + } + + // Conservative check: Look for common extension characteristics + const extensionIndicators = [ + '_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope', + '_requests', '_pubkey', 'name', 'version', 'description' + ]; + + const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop)); + + // Additional check: Extensions often have specific constructor patterns + const hasExtensionConstructor = constructorName && + constructorName !== 'Object' && + constructorName !== 'Function'; + + return hasIndicators || hasExtensionConstructor; + } + + // Try to login with extension and trigger proper persistence + async _tryExtensionLogin(extension) { + try { + console.log('FloatingTab: Attempting extension login'); + + // Get pubkey from extension + const pubkey = await extension.getPublicKey(); + console.log('FloatingTab: Extension provided pubkey:', pubkey); + + // Create extension auth data + const extensionAuth = { + method: 'extension', + pubkey: pubkey, + extension: extension + }; + + // **CRITICAL FIX**: Dispatch nlMethodSelected event to trigger persistence + console.log('FloatingTab: Dispatching nlMethodSelected for persistence'); + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('nlMethodSelected', { + detail: extensionAuth + })); + } + + // Also call our local _handleAuth for UI updates + await this._handleAuth(extensionAuth); + + } catch (error) { + console.error('FloatingTab: Extension login failed:', error); + // Fall back to opening modal + if (this.modal) { + this.modal.open({ startScreen: 'login' }); + } + } + } + + async _handleAuth(authData) { + console.log('🔍 FloatingTab: === _handleAuth START ==='); + console.log('🔍 FloatingTab: authData received:', authData); + + // Wait a brief moment for WindowNostr to process the authentication + setTimeout(async () => { + console.log('🔍 FloatingTab: Checking authentication state from authoritative source...'); + + const authState = this._getAuthState(); + const isAuthenticated = !!authState; + + console.log('🔍 FloatingTab: Authoritative auth state:', authState); + console.log('🔍 FloatingTab: Is authenticated:', isAuthenticated); + + if (isAuthenticated) { + console.log('🔍 FloatingTab: ✅ Authentication verified from authoritative source'); + } else { + console.error('🔍 FloatingTab: ❌ Authentication not found in authoritative source'); + } + + // Fetch user profile if enabled and we have a pubkey + if (this.options.getUserInfo && authData.pubkey) { + console.log('🔍 FloatingTab: getUserInfo enabled, fetching profile for:', authData.pubkey); + try { + const profile = await this._fetchUserProfile(authData.pubkey); + this.userProfile = profile; + console.log('🔍 FloatingTab: User profile fetched:', profile); + } catch (error) { + console.warn('🔍 FloatingTab: Failed to fetch user profile:', error); + this.userProfile = null; + } + } else { + console.log('🔍 FloatingTab: getUserInfo disabled or no pubkey, skipping profile fetch'); + } + + this._updateAppearance(); // Update UI based on authoritative state + + console.log('🔍 FloatingTab: hideWhenAuthenticated option:', this.options.behavior.hideWhenAuthenticated); + + if (this.options.behavior.hideWhenAuthenticated && isAuthenticated) { + console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true and authenticated)'); + this.hide(); + } else { + console.log('🔍 FloatingTab: Keeping tab visible'); + } + + }, 500); // Wait 500ms for WindowNostr to complete authentication processing + + console.log('🔍 FloatingTab: === _handleAuth END ==='); + } + + _handleLogout() { + console.log('FloatingTab: Handling logout'); + this.userProfile = null; + + if (this.options.behavior.hideWhenAuthenticated) { + this.show(); + } + + this._updateAppearance(); + } + + _showUserMenu() { + // Simple user menu - could be expanded + const menu = document.createElement('div'); + menu.style.cssText = ` + position: fixed; + background: var(--nl-secondary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + padding: 12px; + z-index: 10000; + font-family: var(--nl-font-family); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + `; + + // Position near the floating tab + const tabRect = this.container.getBoundingClientRect(); + if (this.options.hPosition > 0.5) { + // Tab is on right side, show menu to the left + menu.style.right = (window.innerWidth - tabRect.left) + 'px'; + } else { + // Tab is on left side, show menu to the right + menu.style.left = tabRect.right + 'px'; + } + menu.style.top = tabRect.top + 'px'; + + // Menu content - use _getAuthState() as single source of truth + const authState = this._getAuthState(); + let userDisplay; + + if (authState?.pubkey) { + // Use profile name if available, otherwise pubkey + if (this.userProfile?.name || this.userProfile?.display_name) { + const userName = this.userProfile.name || this.userProfile.display_name; + userDisplay = userName.length > 16 ? userName.slice(0, 16) + '...' : userName; + } else { + userDisplay = authState.pubkey.slice(0, 8) + '...' + authState.pubkey.slice(-4); + } + } else { + userDisplay = 'Authenticated'; + } + + menu.innerHTML = ` +
${userDisplay}
+ + `; + + document.body.appendChild(menu); + + // Auto-remove menu after delay or on outside click + const removeMenu = () => menu.remove(); + setTimeout(removeMenu, 5000); + + document.addEventListener('click', function onOutsideClick(e) { + if (!menu.contains(e.target) && e.target !== this.container) { + removeMenu(); + document.removeEventListener('click', onOutsideClick); + } + }); + } + + _updateAppearance() { + if (!this.container) return; + + // Query authoritative source for all state information + const authState = this._getAuthState(); + const isAuthenticated = authState !== null; + + // Update content + if (isAuthenticated && this.options.behavior.showUserInfo) { + let display; + + // Use profile name if available, otherwise fall back to pubkey + if (this.userProfile?.name || this.userProfile?.display_name) { + const userName = this.userProfile.name || this.userProfile.display_name; + display = this.options.appearance.iconOnly + ? userName.slice(0, 8) + : userName; + } else if (authState?.pubkey) { + // Fallback to pubkey display + display = this.options.appearance.iconOnly + ? authState.pubkey.slice(0, 6) + : authState.pubkey.slice(0, 6) + '...'; + } else { + display = this.options.appearance.iconOnly ? 'User' : 'Authenticated'; + } + + this.container.textContent = display; + this.container.className = 'nl-floating-tab nl-floating-tab--logged-in'; + } else { + const display = this.options.appearance.iconOnly ? + this.options.appearance.icon : + (this.options.appearance.icon ? this.options.appearance.icon + ' ' + this.options.appearance.text : this.options.appearance.text); + + this.container.textContent = display; + this.container.className = 'nl-floating-tab nl-floating-tab--logged-out'; + } + + // Apply appearance styles based on current state + this._applyThemeStyles(); + } + + _applyThemeStyles() { + if (!this.container) return; + + // The CSS classes will handle the theming through CSS custom properties + // Additional style customizations can be added here if needed + + // Apply style variant + if (this.options.appearance.style === 'circle') { + this.container.style.borderRadius = '50%'; + this.container.style.width = '48px'; + this.container.style.height = '48px'; + this.container.style.minWidth = '48px'; + this.container.style.padding = '0'; + } else if (this.options.appearance.style === 'square') { + this.container.style.borderRadius = '4px'; + } else { + // pill style (default) + this.container.style.borderRadius = 'var(--nl-border-radius)'; + } + } + + async _fetchUserProfile(pubkey) { + if (!this.options.getUserInfo) { + console.log('FloatingTab: getUserInfo disabled, skipping profile fetch'); + return null; + } + + // Determine which relays to use + const relays = this.options.getUserRelay.length > 0 + ? this.options.getUserRelay + : ['wss://relay.damus.io', 'wss://nos.lol']; + + console.log('FloatingTab: Fetching profile from relays:', relays); + + try { + // Create a SimplePool instance for querying + const pool = new window.NostrTools.SimplePool(); + + // Query for kind 0 (user metadata) events + const events = await pool.querySync(relays, { + kinds: [0], + authors: [pubkey], + limit: 1 + }, { timeout: 5000 }); + + console.log('FloatingTab: Profile query returned', events.length, 'events'); + + if (events.length === 0) { + console.log('FloatingTab: No profile events found'); + return null; + } + + // Get the most recent event + const latestEvent = events.sort((a, b) => b.created_at - a.created_at)[0]; + + try { + const profile = JSON.parse(latestEvent.content); + console.log('FloatingTab: Parsed profile:', profile); + + // Find the best name from any key containing "name" (case-insensitive) + let bestName = null; + const nameKeys = Object.keys(profile).filter(key => + key.toLowerCase().includes('name') && + typeof profile[key] === 'string' && + profile[key].trim().length > 0 + ); + + if (nameKeys.length > 0) { + // Find the shortest name value + bestName = nameKeys + .map(key => profile[key].trim()) + .reduce((shortest, current) => + current.length < shortest.length ? current : shortest + ); + console.log('FloatingTab: Found name keys:', nameKeys, 'selected:', bestName); + } + + // Return relevant profile fields with the best name + return { + name: bestName, + display_name: profile.display_name || null, + about: profile.about || null, + picture: profile.picture || null, + nip05: profile.nip05 || null + }; + } catch (parseError) { + console.warn('FloatingTab: Failed to parse profile JSON:', parseError); + return null; + } + } catch (error) { + console.error('FloatingTab: Profile fetch error:', error); + return null; + } + } + + _position() { + if (!this.container) return; + + const padding = 16; // Distance from screen edge + + // Calculate position based on percentage + const x = this.options.hPosition * (window.innerWidth - this.container.offsetWidth - padding * 2) + padding + this.options.offset.x; + const y = this.options.vPosition * (window.innerHeight - this.container.offsetHeight - padding * 2) + padding + this.options.offset.y; + + this.container.style.left = x + 'px'; + this.container.style.top = y + 'px'; + + console.log('FloatingTab: Positioned at (' + x + ', ' + y + ')'); + } + + _slideIn() { + if (!this.container || !this.options.behavior.autoSlide) return; + + // Slide towards center slightly + const currentTransform = this.container.style.transform || ''; + if (this.options.hPosition > 0.5) { + this.container.style.transform = currentTransform + ' translateX(-8px)'; + } else { + this.container.style.transform = currentTransform + ' translateX(8px)'; + } + } + + _slideOut() { + if (!this.container || !this.options.behavior.autoSlide) return; + + // Reset position + this.container.style.transform = ''; + } + + show() { + if (!this.container) return; + this.container.style.display = 'flex'; + this.isVisible = true; + console.log('FloatingTab: Shown'); + } + + hide() { + if (!this.container) return; + this.container.style.display = 'none'; + this.isVisible = false; + console.log('FloatingTab: Hidden'); + } + + destroy() { + if (this.container) { + this.container.remove(); + this.container = null; + } + this.isVisible = false; + console.log('FloatingTab: Destroyed'); + } + + // Update options and re-apply + updateOptions(newOptions) { + this.options = { ...this.options, ...newOptions }; + if (this.container) { + this._updateAppearance(); + this._position(); + } + } + + // Get current state + getState() { + const authState = this._getAuthState(); + return { + isVisible: this.isVisible, + isAuthenticated: !!authState, + userInfo: authState, + options: this.options + }; + } +} + +// ====================================== +// Main NOSTR_LOGIN_LITE Library +// ====================================== + +// Extension Bridge for managing browser extensions +class ExtensionBridge { + constructor() { + this.extensions = new Map(); + this.primaryExtension = null; + this._detectExtensions(); + } + + _detectExtensions() { + // Common extension locations + const locations = [ + { path: 'window.nostr', name: 'Generic' }, + { path: 'window.alby?.nostr', name: 'Alby' }, + { path: 'window.nos2x?.nostr', name: 'nos2x' }, + { path: 'window.flamingo?.nostr', name: 'Flamingo' }, + { path: 'window.getAlby?.nostr', name: 'Alby Legacy' }, + { path: 'window.mutiny?.nostr', name: 'Mutiny' } + ]; + + for (const location of locations) { + try { + const obj = eval(location.path); + if (obj && typeof obj.getPublicKey === 'function') { + this.extensions.set(location.name, { + name: location.name, + extension: obj, + constructor: obj.constructor?.name || 'Unknown' + }); + + if (!this.primaryExtension) { + this.primaryExtension = this.extensions.get(location.name); + } + } + } catch (e) { + // Extension not available + } + } + } + + getAllExtensions() { + return Array.from(this.extensions.values()); + } + + getExtensionCount() { + return this.extensions.size; + } +} + +// Main NostrLite class +class NostrLite { + constructor() { + this.options = {}; + this.extensionBridge = new ExtensionBridge(); + this.initialized = false; + this.currentTheme = 'default'; + this.modal = null; + this.floatingTab = null; + } + + async init(options = {}) { + console.log('NOSTR_LOGIN_LITE: Initializing with options:', options); + + this.options = { + theme: 'default', + persistence: true, // Enable persistent authentication by default + isolateSession: false, // Use localStorage by default for cross-window persistence + methods: { + extension: true, + local: true, + seedphrase: false, + readonly: true, + connect: false, + otp: false + }, + floatingTab: { + enabled: false, + hPosition: 1.0, + vPosition: 0.5, + offset: { x: 0, y: 0 }, + appearance: { + style: 'pill', + theme: 'auto', + icon: '', + text: 'Login', + iconOnly: false + }, + behavior: { + hideWhenAuthenticated: true, + showUserInfo: true, + autoSlide: true, + persistent: false + }, + getUserInfo: false, + getUserRelay: [] + }, + ...options + }; + + // Apply the selected theme (CSS-only) + this.switchTheme(this.options.theme); + + // Always set up window.nostr facade to handle multiple extensions properly + console.log('🔍 NOSTR_LOGIN_LITE: Setting up facade before other initialization...'); + await this._setupWindowNostrFacade(); + console.log('🔍 NOSTR_LOGIN_LITE: Facade setup complete, continuing initialization...'); + + // Create modal during init (matching original git architecture) + this.modal = new Modal(this.options); + console.log('NOSTR_LOGIN_LITE: Modal created during init'); + + // Initialize floating tab if enabled + if (this.options.floatingTab.enabled) { + this.floatingTab = new FloatingTab(this.modal, this.options.floatingTab); + console.log('NOSTR_LOGIN_LITE: Floating tab initialized'); + } + + // Attempt to restore authentication state if persistence is enabled (AFTER facade is ready) + if (this.options.persistence) { + console.log('🔍 NOSTR_LOGIN_LITE: Persistence enabled, attempting auth restoration...'); + await this._attemptAuthRestore(); + } else { + console.log('🔍 NOSTR_LOGIN_LITE: Persistence disabled in options'); + } + + this.initialized = true; + console.log('NOSTR_LOGIN_LITE: Initialization complete'); + + return this; + } + + async _setupWindowNostrFacade() { + if (typeof window !== 'undefined') { + console.log('🔍 NOSTR_LOGIN_LITE: === EXTENSION-FIRST FACADE SETUP ==='); + console.log('🔍 NOSTR_LOGIN_LITE: Current window.nostr:', window.nostr); + console.log('🔍 NOSTR_LOGIN_LITE: Constructor:', window.nostr?.constructor?.name); + + // EXTENSION-FIRST ARCHITECTURE: Never interfere with real extensions + if (this._isRealExtension(window.nostr)) { + console.log('🔍 NOSTR_LOGIN_LITE: ✅ REAL EXTENSION DETECTED - WILL NOT INSTALL FACADE'); + console.log('🔍 NOSTR_LOGIN_LITE: Extension constructor:', window.nostr.constructor?.name); + console.log('🔍 NOSTR_LOGIN_LITE: Extensions will handle window.nostr directly'); + + // Store reference for persistence verification + this.detectedExtension = window.nostr; + this.hasExtension = true; + this.facadeInstalled = false; // We deliberately don't install facade for extensions + + console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - no facade interference'); + return; // Don't install facade at all for extensions + } + + // NO EXTENSION: Install facade for local/NIP-46/readonly methods + console.log('🔍 NOSTR_LOGIN_LITE: ❌ No real extension detected'); + console.log('🔍 NOSTR_LOGIN_LITE: Installing facade for non-extension authentication'); + + this.hasExtension = false; + this._installFacade(window.nostr); // Install facade with any existing nostr object + + console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade installed for local/NIP-46/readonly methods'); + + // CRITICAL FIX: Immediately attempt to restore auth state after facade installation + if (this.facadeInstalled && window.nostr?.restoreAuthState) { + console.log('🔍 NOSTR_LOGIN_LITE: 🔄 IMMEDIATELY attempting auth restoration after facade installation'); + try { + const restoredAuth = await window.nostr.restoreAuthState(); + if (restoredAuth) { + console.log('🔍 NOSTR_LOGIN_LITE: ✅ Auth state restored immediately during facade setup!'); + console.log('🔍 NOSTR_LOGIN_LITE: Method:', restoredAuth.method); + console.log('🔍 NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); + + // Update facade's authState immediately + window.nostr.authState = restoredAuth; + } else { + console.log('🔍 NOSTR_LOGIN_LITE: ❌ No auth state to restore during facade setup'); + } + } catch (error) { + console.error('🔍 NOSTR_LOGIN_LITE: ❌ Error restoring auth during facade setup:', error); + } + } + } + } + + _installFacade(existingNostr = null, forceInstall = false) { + if (typeof window !== 'undefined' && (!this.facadeInstalled || forceInstall)) { + console.log('🔍 NOSTR_LOGIN_LITE: === _installFacade CALLED ==='); + console.log('🔍 NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr); + console.log('🔍 NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name); + console.log('🔍 NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr); + console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name); + console.log('🔍 NOSTR_LOGIN_LITE: forceInstall flag:', forceInstall); + + const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession }); + window.nostr = facade; + this.facadeInstalled = true; + + console.log('🔍 NOSTR_LOGIN_LITE: === FACADE INSTALLED FOR PERSISTENCE ==='); + console.log('🔍 NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr); + console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name); + console.log('🔍 NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr); + } else if (typeof window !== 'undefined') { + console.log('🔍 NOSTR_LOGIN_LITE: _installFacade skipped - facadeInstalled:', this.facadeInstalled, 'forceInstall:', forceInstall); + } + } + + // Conservative method to identify real browser extensions + _isRealExtension(obj) { + console.log('NOSTR_LOGIN_LITE: === _isRealExtension (Conservative) ==='); + console.log('NOSTR_LOGIN_LITE: obj:', obj); + console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj); + + if (!obj || typeof obj !== 'object') { + console.log('NOSTR_LOGIN_LITE: ✗ Not an object'); + return false; + } + + // Must have required Nostr methods + if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { + console.log('NOSTR_LOGIN_LITE: ✗ Missing required NIP-07 methods'); + return false; + } + + // Exclude our own library classes + const constructorName = obj.constructor?.name; + console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName); + + if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') { + console.log('NOSTR_LOGIN_LITE: ✗ Is our library class - NOT an extension'); + return false; + } + + // Exclude NostrTools library object + if (obj === window.NostrTools) { + console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object - NOT an extension'); + return false; + } + + // Conservative check: Look for common extension characteristics + // Real extensions usually have some of these internal properties + const extensionIndicators = [ + '_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope', + '_requests', '_pubkey', 'name', 'version', 'description' + ]; + + const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop)); + + // Additional check: Extensions often have specific constructor patterns + const hasExtensionConstructor = constructorName && + constructorName !== 'Object' && + constructorName !== 'Function'; + + const isExtension = hasIndicators || hasExtensionConstructor; + + console.log('NOSTR_LOGIN_LITE: Extension indicators found:', hasIndicators); + console.log('NOSTR_LOGIN_LITE: Has extension constructor:', hasExtensionConstructor); + console.log('NOSTR_LOGIN_LITE: Final result for', constructorName, ':', isExtension); + + return isExtension; + } + + launch(startScreen = 'login') { + console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen); + + if (this.modal) { + this.modal.open({ startScreen }); + } else { + console.error('NOSTR_LOGIN_LITE: Modal not initialized - call init() first'); + } + } + + // Attempt to restore authentication state + async _attemptAuthRestore() { + try { + console.log('🔍 NOSTR_LOGIN_LITE: === _attemptAuthRestore START ==='); + console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension); + console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled); + console.log('🔍 NOSTR_LOGIN_LITE: window.nostr:', window.nostr?.constructor?.name); + + if (this.hasExtension) { + // EXTENSION MODE: Use custom extension persistence logic + console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - using extension-specific restore'); + const restoredAuth = await this._attemptExtensionRestore(); + + if (restoredAuth) { + console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth restored successfully!'); + return restoredAuth; + } else { + console.log('🔍 NOSTR_LOGIN_LITE: ❌ Extension auth could not be restored'); + return null; + } + } else if (this.facadeInstalled && window.nostr?.restoreAuthState) { + // NON-EXTENSION MODE: Use facade persistence logic + console.log('🔍 NOSTR_LOGIN_LITE: Non-extension mode - using facade restore'); + const restoredAuth = await window.nostr.restoreAuthState(); + + if (restoredAuth) { + console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade auth restored successfully!'); + console.log('🔍 NOSTR_LOGIN_LITE: Method:', restoredAuth.method); + console.log('🔍 NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); + + // CRITICAL FIX: Activate facade resilience system for non-extension methods + // Extensions like nos2x can override our facade after page refresh + if (restoredAuth.method === 'local' || restoredAuth.method === 'nip46') { + console.log('🔍 NOSTR_LOGIN_LITE: 🛡️ Activating facade resilience system for page refresh'); + this._activateResilienceProtection(restoredAuth.method); + } + + // Handle NIP-46 reconnection requirement + if (restoredAuth.requiresReconnection) { + console.log('🔍 NOSTR_LOGIN_LITE: NIP-46 connection requires user reconnection'); + this._showReconnectionPrompt(restoredAuth); + } + + return restoredAuth; + } else { + console.log('🔍 NOSTR_LOGIN_LITE: ❌ Facade auth could not be restored'); + return null; + } + } else { + console.log('🔍 NOSTR_LOGIN_LITE: ❌ No restoration method available'); + console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension); + console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled); + console.log('🔍 NOSTR_LOGIN_LITE: window.nostr.restoreAuthState:', typeof window.nostr?.restoreAuthState); + return null; + } + + } catch (error) { + console.error('🔍 NOSTR_LOGIN_LITE: Auth restoration failed with error:', error); + console.error('🔍 NOSTR_LOGIN_LITE: Error stack:', error.stack); + return null; + } + } + + // Activate facade resilience protection against extension overrides + _activateResilienceProtection(method) { + console.log('🛡️ NOSTR_LOGIN_LITE: === ACTIVATING RESILIENCE PROTECTION ==='); + console.log('🛡️ NOSTR_LOGIN_LITE: Protecting facade for method:', method); + + // Store the current extension if any (for potential restoration later) + const preservedExtension = this.preservedExtension || + ((window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null); + + // DELAYED FACADE RESILIENCE - Reinstall after extension override attempts + const forceReinstallFacade = () => { + console.log('🛡️ NOSTR_LOGIN_LITE: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name); + + // If facade was overridden by extension, reinstall it + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('🛡️ NOSTR_LOGIN_LITE: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method); + this._installFacade(preservedExtension, true); + console.log('🛡️ NOSTR_LOGIN_LITE: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name); + + // Schedule another check in case of persistent extension override + setTimeout(() => { + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('🛡️ NOSTR_LOGIN_LITE: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method); + this._installFacade(preservedExtension, true); + } + }, 1000); + } else { + console.log('🛡️ NOSTR_LOGIN_LITE: Facade persistence verified - no override detected'); + } + }; + + // Schedule resilience checks at multiple intervals (same as Modal) + setTimeout(forceReinstallFacade, 100); // Quick check + setTimeout(forceReinstallFacade, 500); // Main check + setTimeout(forceReinstallFacade, 1500); // Final check + + console.log('🛡️ NOSTR_LOGIN_LITE: Resilience protection scheduled for method:', method); + } + + // Extension-specific authentication restoration + async _attemptExtensionRestore() { + try { + console.log('🔍 NOSTR_LOGIN_LITE: === _attemptExtensionRestore START ==='); + + // Use a simple AuthManager instance for extension persistence + const authManager = new AuthManager({ isolateSession: this.options?.isolateSession }); + const storedAuth = await authManager.restoreAuthState(); + + if (!storedAuth || storedAuth.method !== 'extension') { + console.log('🔍 NOSTR_LOGIN_LITE: No extension auth state stored'); + return null; + } + + // Verify the extension is still available and working + if (!window.nostr || !this._isRealExtension(window.nostr)) { + console.log('🔍 NOSTR_LOGIN_LITE: Extension no longer available'); + authManager.clearAuthState(); // Clear invalid state + return null; + } + + try { + // Test that the extension still works with the same pubkey + const currentPubkey = await window.nostr.getPublicKey(); + if (currentPubkey !== storedAuth.pubkey) { + console.log('🔍 NOSTR_LOGIN_LITE: Extension pubkey changed, clearing state'); + authManager.clearAuthState(); + return null; + } + + console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth verification successful'); + + // Create extension auth data for UI restoration + const extensionAuth = { + method: 'extension', + pubkey: storedAuth.pubkey, + extension: window.nostr + }; + + // Dispatch restoration event so UI can update + if (typeof window !== 'undefined') { + console.log('🔍 NOSTR_LOGIN_LITE: Dispatching nlAuthRestored event for extension'); + window.dispatchEvent(new CustomEvent('nlAuthRestored', { + detail: extensionAuth + })); + } + + return extensionAuth; + + } catch (error) { + console.log('🔍 NOSTR_LOGIN_LITE: Extension verification failed:', error); + authManager.clearAuthState(); // Clear invalid state + return null; + } + + } catch (error) { + console.error('🔍 NOSTR_LOGIN_LITE: Extension restore failed:', error); + return null; + } + } + + // Show prompt for NIP-46 reconnection + _showReconnectionPrompt(authData) { + console.log('NOSTR_LOGIN_LITE: Showing reconnection prompt for NIP-46'); + + // Dispatch event that UI can listen to + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('nlReconnectionRequired', { + detail: { + method: authData.method, + pubkey: authData.pubkey, + connectionData: authData.connectionData, + message: 'Your NIP-46 session has expired. Please reconnect to continue.' + } + })); + } + } + + logout() { + console.log('NOSTR_LOGIN_LITE: Logout called'); + + // Clear legacy stored data + if (typeof localStorage !== 'undefined') { + localStorage.removeItem('nl_current'); + } + + // Clear current authentication state directly from storage + // This works for ALL methods including extensions (fixes the bug) + clearAuthState(); + + // Dispatch logout event for UI updates + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('nlLogout', { + detail: { timestamp: Date.now() } + })); + } + } + + // CSS-only theme switching + switchTheme(themeName) { + console.log('NOSTR_LOGIN_LITE: Switching to ' + themeName + ' theme'); + + if (THEME_CSS[themeName]) { + injectThemeCSS(themeName); + this.currentTheme = themeName; + + // Dispatch theme change event + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('nlThemeChanged', { + detail: { theme: themeName } + })); + } + + return { theme: themeName }; + } else { + console.warn("Theme '" + themeName + "' not found, using default"); + injectThemeCSS('default'); + this.currentTheme = 'default'; + return { theme: 'default' }; + } + } + + getCurrentTheme() { + return this.currentTheme; + } + + getAvailableThemes() { + return Object.keys(THEME_CSS); + } + + embed(container, options = {}) { + console.log('NOSTR_LOGIN_LITE: Creating embedded modal in container:', container); + + const embedOptions = { + ...this.options, + ...options, + embedded: container + }; + + // Create new modal instance for embedding + const embeddedModal = new Modal(embedOptions); + embeddedModal.open(); + + return embeddedModal; + } + + // Floating tab management methods + showFloatingTab() { + if (this.floatingTab) { + this.floatingTab.show(); + } else { + console.warn('NOSTR_LOGIN_LITE: Floating tab not enabled'); + } + } + + hideFloatingTab() { + if (this.floatingTab) { + this.floatingTab.hide(); + } + } + + toggleFloatingTab() { + if (this.floatingTab) { + if (this.floatingTab.isVisible) { + this.floatingTab.hide(); + } else { + this.floatingTab.show(); + } + } + } + + updateFloatingTab(options) { + if (this.floatingTab) { + this.floatingTab.updateOptions(options); + } + } + + getFloatingTabState() { + return this.floatingTab ? this.floatingTab.getState() : null; + } +} + +// ====================================== +// Simplified Authentication Manager (Unified Plaintext Storage) +// ====================================== + +// Simple authentication state manager - plaintext storage for maximum usability +class AuthManager { + constructor(options = {}) { + this.storageKey = 'nostr_login_lite_auth'; + this.currentAuthState = null; + + // Configure storage type based on isolateSession option + if (options.isolateSession) { + this.storage = sessionStorage; + console.log('🔐 AuthManager: Using sessionStorage for per-window isolation'); + } else { + this.storage = localStorage; + console.log('🔐 AuthManager: Using localStorage for cross-window persistence'); + } + + console.warn('🔐 SECURITY: Private keys stored unencrypted in browser storage'); + console.warn('🔐 For production apps, implement your own secure storage'); + } + + // Save authentication state using unified plaintext approach + async saveAuthState(authData) { + try { + console.log('🔐 AuthManager: Saving auth state with plaintext storage'); + console.warn('🔐 SECURITY: Private key will be stored unencrypted for maximum usability'); + + const authState = { + method: authData.method, + timestamp: Date.now(), + pubkey: authData.pubkey + }; + + switch (authData.method) { + case 'extension': + // For extensions, only store verification data - no secrets + authState.extensionVerification = { + constructor: authData.extension?.constructor?.name, + hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function', + hasSignEvent: typeof authData.extension?.signEvent === 'function' + }; + console.log('🔐 AuthManager: Extension method - storing verification data only'); + break; + + case 'local': + // UNIFIED PLAINTEXT: Store secret key directly for maximum compatibility + if (authData.secret) { + authState.secret = authData.secret; + console.log('🔐 AuthManager: Local method - storing secret key in plaintext'); + console.warn('🔐 SECURITY: Secret key stored unencrypted for developer convenience'); + } + break; + + case 'nip46': + // For NIP-46, store connection parameters (no secrets) + if (authData.signer) { + authState.nip46 = { + remotePubkey: authData.signer.remotePubkey, + relays: authData.signer.relays, + // Don't store secret - user will need to reconnect + }; + console.log('🔐 AuthManager: NIP-46 method - storing connection parameters'); + } + break; + + case 'readonly': + // Read-only mode has no secrets to store + console.log('🔐 AuthManager: Read-only method - storing basic auth state'); + break; + + default: + throw new Error('Unknown auth method: ' + authData.method); + } + + this.storage.setItem(this.storageKey, JSON.stringify(authState)); + this.currentAuthState = authState; + console.log('🔐 AuthManager: Auth state saved successfully for method:', authData.method); + + } catch (error) { + console.error('🔐 AuthManager: Failed to save auth state:', error); + throw error; + } + } + + // Restore authentication state on page load + async restoreAuthState() { + try { + console.log('🔍 AuthManager: === restoreAuthState START ==='); + console.log('🔍 AuthManager: storageKey:', this.storageKey); + + const stored = this.storage.getItem(this.storageKey); + console.log('🔍 AuthManager: Storage raw value:', stored); + + if (!stored) { + console.log('🔍 AuthManager: ❌ No stored auth state found'); + return null; + } + + const authState = JSON.parse(stored); + console.log('🔍 AuthManager: ✅ Parsed stored auth state:', authState); + console.log('🔍 AuthManager: Method:', authState.method); + console.log('🔍 AuthManager: Timestamp:', authState.timestamp); + console.log('🔍 AuthManager: Age (ms):', Date.now() - authState.timestamp); + + // Check if stored state is too old (24 hours for most methods, 1 hour for extensions) + const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000; + console.log('🔍 AuthManager: Max age for method:', maxAge, 'ms'); + + if (Date.now() - authState.timestamp > maxAge) { + console.log('🔍 AuthManager: ❌ Stored auth state expired, clearing'); + this.clearAuthState(); + return null; + } + + console.log('🔍 AuthManager: ✅ Auth state not expired, attempting restore for method:', authState.method); + + let result; + switch (authState.method) { + case 'extension': + console.log('🔍 AuthManager: Calling _restoreExtensionAuth...'); + result = await this._restoreExtensionAuth(authState); + break; + + case 'local': + console.log('🔍 AuthManager: Calling _restoreLocalAuth...'); + result = await this._restoreLocalAuth(authState); + break; + + case 'nip46': + console.log('🔍 AuthManager: Calling _restoreNip46Auth...'); + result = await this._restoreNip46Auth(authState); + break; + + case 'readonly': + console.log('🔍 AuthManager: Calling _restoreReadonlyAuth...'); + result = await this._restoreReadonlyAuth(authState); + break; + + default: + console.warn('🔍 AuthManager: ❌ Unknown auth method in stored state:', authState.method); + return null; + } + + console.log('🔍 AuthManager: Restore method result:', result); + console.log('🔍 AuthManager: === restoreAuthState END ==='); + return result; + + } catch (error) { + console.error('🔍 AuthManager: ❌ Failed to restore auth state:', error); + console.error('🔍 AuthManager: Error stack:', error.stack); + this.clearAuthState(); // Clear corrupted state + return null; + } + } + + async _restoreExtensionAuth(authState) { + console.log('🔍 AuthManager: === _restoreExtensionAuth START ==='); + console.log('🔍 AuthManager: authState:', authState); + console.log('🔍 AuthManager: window.nostr available:', !!window.nostr); + console.log('🔍 AuthManager: window.nostr constructor:', window.nostr?.constructor?.name); + + // SMART EXTENSION WAITING SYSTEM + // Extensions often load after our library, so we need to wait for them + const extension = await this._waitForExtension(authState, 3000); // Wait up to 3 seconds + + if (!extension) { + console.log('🔍 AuthManager: ❌ No extension found after waiting'); + return null; + } + + console.log('🔍 AuthManager: ✅ Extension found:', extension.constructor?.name); + + try { + // Verify extension still works and has same pubkey + const currentPubkey = await extension.getPublicKey(); + if (currentPubkey !== authState.pubkey) { + console.log('🔍 AuthManager: ❌ Extension pubkey changed, not restoring'); + console.log('🔍 AuthManager: Expected:', authState.pubkey); + console.log('🔍 AuthManager: Got:', currentPubkey); + return null; + } + + console.log('🔍 AuthManager: ✅ Extension auth restored successfully'); + return { + method: 'extension', + pubkey: authState.pubkey, + extension: extension + }; + + } catch (error) { + console.log('🔍 AuthManager: ❌ Extension verification failed:', error); + return null; + } + } + + // Smart extension waiting system - polls multiple locations for extensions + async _waitForExtension(authState, maxWaitMs = 3000) { + console.log('🔍 AuthManager: === _waitForExtension START ==='); + console.log('🔍 AuthManager: maxWaitMs:', maxWaitMs); + console.log('🔍 AuthManager: Looking for extension with constructor:', authState.extensionVerification?.constructor); + + const startTime = Date.now(); + const pollInterval = 100; // Check every 100ms + + // Extension locations to check (in priority order) + const extensionLocations = [ + { path: 'window.nostr', getter: () => window.nostr }, + { path: 'navigator.nostr', getter: () => navigator?.nostr }, + { path: 'window.navigator?.nostr', getter: () => window.navigator?.nostr }, + { path: 'window.alby?.nostr', getter: () => window.alby?.nostr }, + { path: 'window.webln?.nostr', getter: () => window.webln?.nostr }, + { path: 'window.nos2x', getter: () => window.nos2x }, + { path: 'window.flamingo?.nostr', getter: () => window.flamingo?.nostr }, + { path: 'window.mutiny?.nostr', getter: () => window.mutiny?.nostr } + ]; + + while (Date.now() - startTime < maxWaitMs) { + console.log('🔍 AuthManager: Polling for extensions... (elapsed:', Date.now() - startTime, 'ms)'); + + // If our facade is currently installed and blocking, temporarily remove it + let facadeRemoved = false; + let originalNostr = null; + if (window.nostr?.constructor?.name === 'WindowNostr') { + console.log('🔍 AuthManager: Temporarily removing our facade to check for real extensions'); + originalNostr = window.nostr; + window.nostr = window.nostr.existingNostr || undefined; + facadeRemoved = true; + } + + try { + // Check all extension locations + for (const location of extensionLocations) { + try { + const extension = location.getter(); + console.log('🔍 AuthManager: Checking', location.path, ':', !!extension, extension?.constructor?.name); + + if (this._isValidExtensionForRestore(extension, authState)) { + console.log('🔍 AuthManager: ✅ Found matching extension at', location.path); + + // Restore facade if we removed it + if (facadeRemoved && originalNostr) { + console.log('🔍 AuthManager: Restoring facade after finding extension'); + window.nostr = originalNostr; + } + + return extension; + } + } catch (error) { + console.log('🔍 AuthManager: Error checking', location.path, ':', error.message); + } + } + + // Restore facade if we removed it and haven't found an extension yet + if (facadeRemoved && originalNostr) { + window.nostr = originalNostr; + facadeRemoved = false; + } + + } catch (error) { + console.error('🔍 AuthManager: Error during extension polling:', error); + + // Restore facade if we removed it + if (facadeRemoved && originalNostr) { + window.nostr = originalNostr; + } + } + + // Wait before next poll + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + + console.log('🔍 AuthManager: ❌ Extension waiting timeout after', maxWaitMs, 'ms'); + return null; + } + + // Check if an extension is valid for restoration + _isValidExtensionForRestore(extension, authState) { + if (!extension || typeof extension !== 'object') { + return false; + } + + // Must have required Nostr methods + if (typeof extension.getPublicKey !== 'function' || + typeof extension.signEvent !== 'function') { + return false; + } + + // Must not be our own classes + const constructorName = extension.constructor?.name; + if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') { + return false; + } + + // Must not be NostrTools + if (extension === window.NostrTools) { + return false; + } + + // If we have stored verification data, check constructor match + const verification = authState.extensionVerification; + if (verification && verification.constructor) { + if (constructorName !== verification.constructor) { + console.log('🔍 AuthManager: Constructor mismatch -', + 'expected:', verification.constructor, + 'got:', constructorName); + return false; + } + } + + console.log('🔍 AuthManager: ✅ Extension validation passed for:', constructorName); + return true; + } + + async _restoreLocalAuth(authState) { + console.log('🔐 AuthManager: === _restoreLocalAuth (Unified Plaintext) ==='); + + // Check for legacy encrypted format first + if (authState.encrypted) { + console.log('🔐 AuthManager: Detected LEGACY encrypted format - migrating to plaintext'); + console.warn('🔐 SECURITY: Converting from encrypted to plaintext storage for compatibility'); + + // Try to decrypt legacy format + const sessionPassword = sessionStorage.getItem('nostr_session_key'); + if (!sessionPassword) { + console.log('🔐 AuthManager: Legacy session password not found - user must re-login'); + return null; + } + + try { + console.warn('🔐 AuthManager: Legacy encryption system no longer supported - user must re-login'); + this.clearAuthState(); // Clear legacy format + return null; + } catch (error) { + console.error('🔐 AuthManager: Legacy decryption failed:', error); + this.clearAuthState(); // Clear corrupted legacy format + return null; + } + } + + // NEW UNIFIED PLAINTEXT FORMAT + if (!authState.secret) { + console.log('🔐 AuthManager: No secret found in plaintext format'); + return null; + } + + console.log('🔐 AuthManager: ✅ Local auth restored from plaintext storage'); + console.warn('🔐 SECURITY: Secret key was stored unencrypted'); + + return { + method: 'local', + pubkey: authState.pubkey, + secret: authState.secret + }; + } + + async _restoreNip46Auth(authState) { + if (!authState.nip46) { + console.log('🔐 AuthManager: No NIP-46 data found'); + return null; + } + + // For NIP-46, we can't automatically restore the connection + // because it requires the user to re-authenticate with the remote signer + // Instead, we return the connection parameters so the UI can prompt for reconnection + console.log('🔐 AuthManager: NIP-46 connection data found, requires user reconnection'); + return { + method: 'nip46', + pubkey: authState.pubkey, + requiresReconnection: true, + connectionData: authState.nip46 + }; + } + + async _restoreReadonlyAuth(authState) { + console.log('🔐 AuthManager: Read-only auth restored successfully'); + return { + method: 'readonly', + pubkey: authState.pubkey + }; + } + + // Clear stored authentication state + clearAuthState() { + this.storage.removeItem(this.storageKey); + sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key + this.currentAuthState = null; + console.log('🔐 AuthManager: Auth state cleared from unified storage'); + } + + // Check if we have valid stored auth + hasStoredAuth() { + const stored = this.storage.getItem(this.storageKey); + return !!stored; + } + + // Get current auth method without full restoration + getStoredAuthMethod() { + try { + const stored = this.storage.getItem(this.storageKey); + if (!stored) return null; + + const authState = JSON.parse(stored); + return authState.method; + } catch { + return null; + } + } +} + +// ====================================== +// Global Authentication Functions (Single Source of Truth) +// ====================================== + +// Global authentication state (single source of truth) +let globalAuthState = null; +let globalAuthManager = null; + +// Initialize global auth manager (lazy initialization) +function getGlobalAuthManager() { + if (!globalAuthManager) { + // Default to localStorage for persistence across browser sessions + globalAuthManager = new AuthManager({ isolateSession: false }); + } + return globalAuthManager; +} + +// **UNIFIED GLOBAL FUNCTION**: Set authentication state (works for all methods) +function setAuthState(authData, options = {}) { + try { + console.log('🌐 setAuthState: Setting global auth state for method:', authData.method); + console.warn('🔐 SECURITY: Using unified plaintext storage for maximum compatibility'); + + // Store in memory + globalAuthState = authData; + + // Store in browser storage using AuthManager + const authManager = new AuthManager(options); + authManager.saveAuthState(authData); + + console.log('🌐 setAuthState: Auth state saved successfully'); + } catch (error) { + console.error('🌐 setAuthState: Failed to save auth state:', error); + throw error; + } +} + +// **UNIFIED GLOBAL FUNCTION**: Get authentication state (single source of truth) +function getAuthState() { + try { + // Always query from storage as the authoritative source + const authManager = getGlobalAuthManager(); + const storageKey = 'nostr_login_lite_auth'; + + // Check both session and local storage for compatibility + let stored = null; + if (sessionStorage.getItem(storageKey)) { + stored = sessionStorage.getItem(storageKey); + } else if (localStorage.getItem(storageKey)) { + stored = localStorage.getItem(storageKey); + } + + if (!stored) { + console.log('🌐 getAuthState: No auth state found in storage'); + globalAuthState = null; + return null; + } + + const authState = JSON.parse(stored); + console.log('🌐 getAuthState: Retrieved auth state:', authState.method); + + // Update in-memory cache + globalAuthState = authState; + return authState; + + } catch (error) { + console.error('🌐 getAuthState: Failed to get auth state:', error); + globalAuthState = null; + return null; + } +} + +// **UNIFIED GLOBAL FUNCTION**: Clear authentication state (works for all methods) +function clearAuthState() { + try { + console.log('🌐 clearAuthState: Clearing global auth state'); + + // Clear in-memory state + globalAuthState = null; + + // Clear from both storage types for thorough cleanup + const storageKey = 'nostr_login_lite_auth'; + localStorage.removeItem(storageKey); + sessionStorage.removeItem(storageKey); + sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key + + console.log('🌐 clearAuthState: Auth state cleared from all storage locations'); + } catch (error) { + console.error('🌐 clearAuthState: Failed to clear auth state:', error); + } +} + +// NIP-07 compliant window.nostr provider +class WindowNostr { + constructor(nostrLite, existingNostr = null, options = {}) { + this.nostrLite = nostrLite; + this.authState = null; + this.existingNostr = existingNostr; + this.authenticatedExtension = null; + this.options = options; + this._setupEventListeners(); + } + + // Restore authentication state on page load + async restoreAuthState() { + console.log('🔍 WindowNostr: === restoreAuthState ==='); + + try { + // Use simplified AuthManager for consistent restore logic + const authManager = new AuthManager(this.options); + const restoredAuth = await authManager.restoreAuthState(); + + if (restoredAuth) { + console.log('🔍 WindowNostr: ✅ Auth state restored:', restoredAuth.method); + this.authState = restoredAuth; + + // Update global state + globalAuthState = restoredAuth; + + // Dispatch restoration event + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('nlAuthRestored', { + detail: restoredAuth + })); + } + + return restoredAuth; + } else { + console.log('🔍 WindowNostr: ❌ No auth state to restore'); + return null; + } + + } catch (error) { + console.error('🔍 WindowNostr: Auth restoration failed:', error); + return null; + } + } + + _setupEventListeners() { + // Listen for authentication events to store auth state + if (typeof window !== 'undefined') { + window.addEventListener('nlMethodSelected', async (event) => { + console.log('🔍 WindowNostr: nlMethodSelected event received:', event.detail); + this.authState = event.detail; + + // If extension method, capture the specific extension the user chose + if (event.detail.method === 'extension') { + this.authenticatedExtension = event.detail.extension; + console.log('🔍 WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); + } + + // Use unified global setAuthState function for all methods + try { + setAuthState(event.detail, this.options); + console.log('🔍 WindowNostr: Auth state saved via unified setAuthState'); + } catch (error) { + console.error('🔍 WindowNostr: Failed to save auth state:', error); + } + }); + + window.addEventListener('nlLogout', () => { + console.log('🔍 WindowNostr: nlLogout event received'); + this.authState = null; + this.authenticatedExtension = null; + + // Clear from unified storage + clearAuthState(); + console.log('🔍 WindowNostr: Auth state cleared via unified clearAuthState'); + }); + } + } + + async getPublicKey() { + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + switch (this.authState.method) { + case 'extension': + // Use the captured authenticated extension, not current window.nostr + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + if (!ext) throw new Error('Extension not available'); + return await ext.getPublicKey(); + + case 'local': + case 'nip46': + return this.authState.pubkey; + + case 'readonly': + throw new Error('Read-only mode - cannot get public key'); + + default: + throw new Error('Unsupported auth method: ' + this.authState.method); + } + } + + async signEvent(event) { + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + if (this.authState.method === 'readonly') { + throw new Error('Read-only mode - cannot sign events'); + } + + switch (this.authState.method) { + case 'extension': + // Use the captured authenticated extension, not current window.nostr + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + if (!ext) throw new Error('Extension not available'); + return await ext.signEvent(event); + + case 'local': { + // Use nostr-tools to sign with local secret key + const { nip19, finalizeEvent } = window.NostrTools; + let secretKey; + + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); + secretKey = decoded.data; + } else { + // Convert hex to Uint8Array + secretKey = this._hexToUint8Array(this.authState.secret); + } + + return finalizeEvent(event, secretKey); + } + + case 'nip46': { + // Use BunkerSigner for NIP-46 + if (!this.authState.signer?.bunkerSigner) { + throw new Error('NIP-46 signer not available'); + } + return await this.authState.signer.bunkerSigner.signEvent(event); + } + + default: + throw new Error('Unsupported auth method: ' + this.authState.method); + } + } + + async getRelays() { + // Return configured relays from nostr-lite options + return this.nostrLite.options?.relays || ['wss://relay.damus.io']; + } + + get nip04() { + return { + encrypt: async (pubkey, plaintext) => { + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + if (this.authState.method === 'readonly') { + throw new Error('Read-only mode - cannot encrypt'); + } + + switch (this.authState.method) { + case 'extension': { + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + if (!ext) throw new Error('Extension not available'); + return await ext.nip04.encrypt(pubkey, plaintext); + } + + case 'local': { + const { nip04, nip19 } = window.NostrTools; + let secretKey; + + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); + secretKey = decoded.data; + } else { + secretKey = this._hexToUint8Array(this.authState.secret); + } + + return await nip04.encrypt(secretKey, pubkey, plaintext); + } + + case 'nip46': { + if (!this.authState.signer?.bunkerSigner) { + throw new Error('NIP-46 signer not available'); + } + return await this.authState.signer.bunkerSigner.nip04Encrypt(pubkey, plaintext); + } + + default: + throw new Error('Unsupported auth method: ' + this.authState.method); + } + }, + + decrypt: async (pubkey, ciphertext) => { + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + if (this.authState.method === 'readonly') { + throw new Error('Read-only mode - cannot decrypt'); + } + + switch (this.authState.method) { + case 'extension': { + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + if (!ext) throw new Error('Extension not available'); + return await ext.nip04.decrypt(pubkey, ciphertext); + } + + case 'local': { + const { nip04, nip19 } = window.NostrTools; + let secretKey; + + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); + secretKey = decoded.data; + } else { + secretKey = this._hexToUint8Array(this.authState.secret); + } + + return await nip04.decrypt(secretKey, pubkey, ciphertext); + } + + case 'nip46': { + if (!this.authState.signer?.bunkerSigner) { + throw new Error('NIP-46 signer not available'); + } + return await this.authState.signer.bunkerSigner.nip04Decrypt(pubkey, ciphertext); + } + + default: + throw new Error('Unsupported auth method: ' + this.authState.method); + } + } + }; + } + + get nip44() { + return { + encrypt: async (pubkey, plaintext) => { + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + if (this.authState.method === 'readonly') { + throw new Error('Read-only mode - cannot encrypt'); + } + + switch (this.authState.method) { + case 'extension': { + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + if (!ext) throw new Error('Extension not available'); + return await ext.nip44.encrypt(pubkey, plaintext); + } + + case 'local': { + const { nip44, nip19 } = window.NostrTools; + let secretKey; + + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); + secretKey = decoded.data; + } else { + secretKey = this._hexToUint8Array(this.authState.secret); + } + + return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey)); + } + + case 'nip46': { + if (!this.authState.signer?.bunkerSigner) { + throw new Error('NIP-46 signer not available'); + } + return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext); + } + + default: + throw new Error('Unsupported auth method: ' + this.authState.method); + } + }, + + decrypt: async (pubkey, ciphertext) => { + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + if (this.authState.method === 'readonly') { + throw new Error('Read-only mode - cannot decrypt'); + } + + switch (this.authState.method) { + case 'extension': { + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + if (!ext) throw new Error('Extension not available'); + return await ext.nip44.decrypt(pubkey, ciphertext); + } + + case 'local': { + const { nip44, nip19 } = window.NostrTools; + let secretKey; + + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); + secretKey = decoded.data; + } else { + secretKey = this._hexToUint8Array(this.authState.secret); + } + + return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey)); + } + + case 'nip46': { + if (!this.authState.signer?.bunkerSigner) { + throw new Error('NIP-46 signer not available'); + } + return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext); + } + + default: + throw new Error('Unsupported auth method: ' + this.authState.method); + } + } + }; + } + + _hexToUint8Array(hex) { + if (hex.length % 2 !== 0) { + throw new Error('Invalid hex string length'); + } + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return bytes; + } +} + +// Initialize and export +if (typeof window !== 'undefined') { + const nostrLite = new NostrLite(); + + // Export main API + window.NOSTR_LOGIN_LITE = { + init: (options) => nostrLite.init(options), + launch: (startScreen) => nostrLite.launch(startScreen), + logout: () => nostrLite.logout(), + + // Embedded modal method + embed: (container, options) => nostrLite.embed(container, options), + + // CSS-only theme management API + switchTheme: (themeName) => nostrLite.switchTheme(themeName), + getCurrentTheme: () => nostrLite.getCurrentTheme(), + getAvailableThemes: () => nostrLite.getAvailableThemes(), + + // Floating tab management API + showFloatingTab: () => nostrLite.showFloatingTab(), + hideFloatingTab: () => nostrLite.hideFloatingTab(), + toggleFloatingTab: () => nostrLite.toggleFloatingTab(), + updateFloatingTab: (options) => nostrLite.updateFloatingTab(options), + getFloatingTabState: () => nostrLite.getFloatingTabState(), + + // Global authentication state management (single source of truth) + setAuthState: setAuthState, + getAuthState: getAuthState, + clearAuthState: clearAuthState, + + // Expose for debugging + _extensionBridge: nostrLite.extensionBridge, + _instance: nostrLite + }; + + console.log('NOSTR_LOGIN_LITE: Library loaded and ready'); + console.log('NOSTR_LOGIN_LITE: Use window.NOSTR_LOGIN_LITE.init(options) to initialize'); + console.log('NOSTR_LOGIN_LITE: Detected', nostrLite.extensionBridge.getExtensionCount(), 'browser extensions'); + console.warn('🔐 SECURITY: Unified plaintext storage enabled for maximum developer usability'); +} else { + // Node.js environment + module.exports = { NostrLite }; +} + diff --git a/api/nostr.bundle.js b/api/nostr.bundle.js new file mode 100644 index 0000000..b555c53 --- /dev/null +++ b/api/nostr.bundle.js @@ -0,0 +1,11534 @@ +"use strict"; +var NostrTools = (() => { + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS = (mod3) => __copyProps(__defProp({}, "__esModule", { value: true }), mod3); + + // index.ts + var nostr_tools_exports = {}; + __export(nostr_tools_exports, { + Relay: () => Relay, + SimplePool: () => SimplePool, + finalizeEvent: () => finalizeEvent, + fj: () => fakejson_exports, + generateSecretKey: () => generateSecretKey, + getEventHash: () => getEventHash, + getFilterLimit: () => getFilterLimit, + getPublicKey: () => getPublicKey, + kinds: () => kinds_exports, + matchFilter: () => matchFilter, + matchFilters: () => matchFilters, + mergeFilters: () => mergeFilters, + nip04: () => nip04_exports, + nip05: () => nip05_exports, + nip06: () => nip06_exports, + nip10: () => nip10_exports, + nip11: () => nip11_exports, + nip13: () => nip13_exports, + nip17: () => nip17_exports, + nip18: () => nip18_exports, + nip19: () => nip19_exports, + nip21: () => nip21_exports, + nip25: () => nip25_exports, + nip27: () => nip27_exports, + nip28: () => nip28_exports, + nip30: () => nip30_exports, + nip39: () => nip39_exports, + nip42: () => nip42_exports, + nip44: () => nip44_exports, + nip46: () => nip46_exports, + nip47: () => nip47_exports, + nip54: () => nip54_exports, + nip57: () => nip57_exports, + nip59: () => nip59_exports, + nip98: () => nip98_exports, + parseReferences: () => parseReferences, + serializeEvent: () => serializeEvent, + sortEvents: () => sortEvents, + utils: () => utils_exports2, + validateEvent: () => validateEvent, + verifiedSymbol: () => verifiedSymbol, + verifyEvent: () => verifyEvent + }); + + // node_modules/@noble/curves/node_modules/@noble/hashes/esm/_assert.js + function number(n) { + if (!Number.isSafeInteger(n) || n < 0) + throw new Error(`Wrong positive integer: ${n}`); + } + function bytes(b, ...lengths) { + if (!(b instanceof Uint8Array)) + throw new Error("Expected Uint8Array"); + if (lengths.length > 0 && !lengths.includes(b.length)) + throw new Error(`Expected Uint8Array of length ${lengths}, not of length=${b.length}`); + } + function hash(hash3) { + if (typeof hash3 !== "function" || typeof hash3.create !== "function") + throw new Error("Hash should be wrapped by utils.wrapConstructor"); + number(hash3.outputLen); + number(hash3.blockLen); + } + function exists(instance, checkFinished = true) { + if (instance.destroyed) + throw new Error("Hash instance has been destroyed"); + if (checkFinished && instance.finished) + throw new Error("Hash#digest() has already been called"); + } + function output(out, instance) { + bytes(out); + const min = instance.outputLen; + if (out.length < min) { + throw new Error(`digestInto() expects output buffer of length at least ${min}`); + } + } + + // node_modules/@noble/curves/node_modules/@noble/hashes/esm/crypto.js + var crypto = typeof globalThis === "object" && "crypto" in globalThis ? globalThis.crypto : void 0; + + // node_modules/@noble/curves/node_modules/@noble/hashes/esm/utils.js + var u8a = (a) => a instanceof Uint8Array; + var createView = (arr) => new DataView(arr.buffer, arr.byteOffset, arr.byteLength); + var rotr = (word, shift) => word << 32 - shift | word >>> shift; + var isLE = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68; + if (!isLE) + throw new Error("Non little-endian hardware is not supported"); + function utf8ToBytes(str) { + if (typeof str !== "string") + throw new Error(`utf8ToBytes expected string, got ${typeof str}`); + return new Uint8Array(new TextEncoder().encode(str)); + } + function toBytes(data) { + if (typeof data === "string") + data = utf8ToBytes(data); + if (!u8a(data)) + throw new Error(`expected Uint8Array, got ${typeof data}`); + return data; + } + function concatBytes(...arrays) { + const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0)); + let pad2 = 0; + arrays.forEach((a) => { + if (!u8a(a)) + throw new Error("Uint8Array expected"); + r.set(a, pad2); + pad2 += a.length; + }); + return r; + } + var Hash = class { + clone() { + return this._cloneInto(); + } + }; + var toStr = {}.toString; + function wrapConstructor(hashCons) { + const hashC = (msg) => hashCons().update(toBytes(msg)).digest(); + const tmp = hashCons(); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.create = () => hashCons(); + return hashC; + } + function randomBytes(bytesLength = 32) { + if (crypto && typeof crypto.getRandomValues === "function") { + return crypto.getRandomValues(new Uint8Array(bytesLength)); + } + throw new Error("crypto.getRandomValues must be defined"); + } + + // node_modules/@noble/curves/node_modules/@noble/hashes/esm/_sha2.js + function setBigUint64(view, byteOffset, value, isLE4) { + if (typeof view.setBigUint64 === "function") + return view.setBigUint64(byteOffset, value, isLE4); + const _32n2 = BigInt(32); + const _u32_max = BigInt(4294967295); + const wh = Number(value >> _32n2 & _u32_max); + const wl = Number(value & _u32_max); + const h = isLE4 ? 4 : 0; + const l = isLE4 ? 0 : 4; + view.setUint32(byteOffset + h, wh, isLE4); + view.setUint32(byteOffset + l, wl, isLE4); + } + var SHA2 = class extends Hash { + constructor(blockLen, outputLen, padOffset, isLE4) { + super(); + this.blockLen = blockLen; + this.outputLen = outputLen; + this.padOffset = padOffset; + this.isLE = isLE4; + this.finished = false; + this.length = 0; + this.pos = 0; + this.destroyed = false; + this.buffer = new Uint8Array(blockLen); + this.view = createView(this.buffer); + } + update(data) { + exists(this); + const { view, buffer, blockLen } = this; + data = toBytes(data); + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + if (take === blockLen) { + const dataView = createView(data); + for (; blockLen <= len - pos; pos += blockLen) + this.process(dataView, pos); + continue; + } + buffer.set(data.subarray(pos, pos + take), this.pos); + this.pos += take; + pos += take; + if (this.pos === blockLen) { + this.process(view, 0); + this.pos = 0; + } + } + this.length += data.length; + this.roundClean(); + return this; + } + digestInto(out) { + exists(this); + output(out, this); + this.finished = true; + const { buffer, view, blockLen, isLE: isLE4 } = this; + let { pos } = this; + buffer[pos++] = 128; + this.buffer.subarray(pos).fill(0); + if (this.padOffset > blockLen - pos) { + this.process(view, 0); + pos = 0; + } + for (let i2 = pos; i2 < blockLen; i2++) + buffer[i2] = 0; + setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE4); + this.process(view, 0); + const oview = createView(out); + const len = this.outputLen; + if (len % 4) + throw new Error("_sha2: outputLen should be aligned to 32bit"); + const outLen = len / 4; + const state = this.get(); + if (outLen > state.length) + throw new Error("_sha2: outputLen bigger than state"); + for (let i2 = 0; i2 < outLen; i2++) + oview.setUint32(4 * i2, state[i2], isLE4); + } + digest() { + const { buffer, outputLen } = this; + this.digestInto(buffer); + const res = buffer.slice(0, outputLen); + this.destroy(); + return res; + } + _cloneInto(to) { + to || (to = new this.constructor()); + to.set(...this.get()); + const { blockLen, buffer, length, finished, destroyed, pos } = this; + to.length = length; + to.pos = pos; + to.finished = finished; + to.destroyed = destroyed; + if (length % blockLen) + to.buffer.set(buffer); + return to; + } + }; + + // node_modules/@noble/curves/node_modules/@noble/hashes/esm/sha256.js + var Chi = (a, b, c) => a & b ^ ~a & c; + var Maj = (a, b, c) => a & b ^ a & c ^ b & c; + var SHA256_K = /* @__PURE__ */ new Uint32Array([ + 1116352408, + 1899447441, + 3049323471, + 3921009573, + 961987163, + 1508970993, + 2453635748, + 2870763221, + 3624381080, + 310598401, + 607225278, + 1426881987, + 1925078388, + 2162078206, + 2614888103, + 3248222580, + 3835390401, + 4022224774, + 264347078, + 604807628, + 770255983, + 1249150122, + 1555081692, + 1996064986, + 2554220882, + 2821834349, + 2952996808, + 3210313671, + 3336571891, + 3584528711, + 113926993, + 338241895, + 666307205, + 773529912, + 1294757372, + 1396182291, + 1695183700, + 1986661051, + 2177026350, + 2456956037, + 2730485921, + 2820302411, + 3259730800, + 3345764771, + 3516065817, + 3600352804, + 4094571909, + 275423344, + 430227734, + 506948616, + 659060556, + 883997877, + 958139571, + 1322822218, + 1537002063, + 1747873779, + 1955562222, + 2024104815, + 2227730452, + 2361852424, + 2428436474, + 2756734187, + 3204031479, + 3329325298 + ]); + var IV = /* @__PURE__ */ new Uint32Array([ + 1779033703, + 3144134277, + 1013904242, + 2773480762, + 1359893119, + 2600822924, + 528734635, + 1541459225 + ]); + var SHA256_W = /* @__PURE__ */ new Uint32Array(64); + var SHA256 = class extends SHA2 { + constructor() { + super(64, 32, 8, false); + this.A = IV[0] | 0; + this.B = IV[1] | 0; + this.C = IV[2] | 0; + this.D = IV[3] | 0; + this.E = IV[4] | 0; + this.F = IV[5] | 0; + this.G = IV[6] | 0; + this.H = IV[7] | 0; + } + get() { + const { A, B, C, D, E, F, G, H } = this; + return [A, B, C, D, E, F, G, H]; + } + set(A, B, C, D, E, F, G, H) { + this.A = A | 0; + this.B = B | 0; + this.C = C | 0; + this.D = D | 0; + this.E = E | 0; + this.F = F | 0; + this.G = G | 0; + this.H = H | 0; + } + process(view, offset) { + for (let i2 = 0; i2 < 16; i2++, offset += 4) + SHA256_W[i2] = view.getUint32(offset, false); + for (let i2 = 16; i2 < 64; i2++) { + const W15 = SHA256_W[i2 - 15]; + const W2 = SHA256_W[i2 - 2]; + const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3; + const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10; + SHA256_W[i2] = s1 + SHA256_W[i2 - 7] + s0 + SHA256_W[i2 - 16] | 0; + } + let { A, B, C, D, E, F, G, H } = this; + for (let i2 = 0; i2 < 64; i2++) { + const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25); + const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i2] + SHA256_W[i2] | 0; + const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22); + const T2 = sigma0 + Maj(A, B, C) | 0; + H = G; + G = F; + F = E; + E = D + T1 | 0; + D = C; + C = B; + B = A; + A = T1 + T2 | 0; + } + A = A + this.A | 0; + B = B + this.B | 0; + C = C + this.C | 0; + D = D + this.D | 0; + E = E + this.E | 0; + F = F + this.F | 0; + G = G + this.G | 0; + H = H + this.H | 0; + this.set(A, B, C, D, E, F, G, H); + } + roundClean() { + SHA256_W.fill(0); + } + destroy() { + this.set(0, 0, 0, 0, 0, 0, 0, 0); + this.buffer.fill(0); + } + }; + var sha256 = /* @__PURE__ */ wrapConstructor(() => new SHA256()); + + // node_modules/@noble/curves/esm/abstract/utils.js + var utils_exports = {}; + __export(utils_exports, { + bitGet: () => bitGet, + bitLen: () => bitLen, + bitMask: () => bitMask, + bitSet: () => bitSet, + bytesToHex: () => bytesToHex, + bytesToNumberBE: () => bytesToNumberBE, + bytesToNumberLE: () => bytesToNumberLE, + concatBytes: () => concatBytes2, + createHmacDrbg: () => createHmacDrbg, + ensureBytes: () => ensureBytes, + equalBytes: () => equalBytes, + hexToBytes: () => hexToBytes, + hexToNumber: () => hexToNumber, + numberToBytesBE: () => numberToBytesBE, + numberToBytesLE: () => numberToBytesLE, + numberToHexUnpadded: () => numberToHexUnpadded, + numberToVarBytesBE: () => numberToVarBytesBE, + utf8ToBytes: () => utf8ToBytes2, + validateObject: () => validateObject + }); + var _0n = BigInt(0); + var _1n = BigInt(1); + var _2n = BigInt(2); + var u8a2 = (a) => a instanceof Uint8Array; + var hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i2) => i2.toString(16).padStart(2, "0")); + function bytesToHex(bytes4) { + if (!u8a2(bytes4)) + throw new Error("Uint8Array expected"); + let hex2 = ""; + for (let i2 = 0; i2 < bytes4.length; i2++) { + hex2 += hexes[bytes4[i2]]; + } + return hex2; + } + function numberToHexUnpadded(num) { + const hex2 = num.toString(16); + return hex2.length & 1 ? `0${hex2}` : hex2; + } + function hexToNumber(hex2) { + if (typeof hex2 !== "string") + throw new Error("hex string expected, got " + typeof hex2); + return BigInt(hex2 === "" ? "0" : `0x${hex2}`); + } + function hexToBytes(hex2) { + if (typeof hex2 !== "string") + throw new Error("hex string expected, got " + typeof hex2); + const len = hex2.length; + if (len % 2) + throw new Error("padded hex string expected, got unpadded hex of length " + len); + const array = new Uint8Array(len / 2); + for (let i2 = 0; i2 < array.length; i2++) { + const j = i2 * 2; + const hexByte = hex2.slice(j, j + 2); + const byte = Number.parseInt(hexByte, 16); + if (Number.isNaN(byte) || byte < 0) + throw new Error("Invalid byte sequence"); + array[i2] = byte; + } + return array; + } + function bytesToNumberBE(bytes4) { + return hexToNumber(bytesToHex(bytes4)); + } + function bytesToNumberLE(bytes4) { + if (!u8a2(bytes4)) + throw new Error("Uint8Array expected"); + return hexToNumber(bytesToHex(Uint8Array.from(bytes4).reverse())); + } + function numberToBytesBE(n, len) { + return hexToBytes(n.toString(16).padStart(len * 2, "0")); + } + function numberToBytesLE(n, len) { + return numberToBytesBE(n, len).reverse(); + } + function numberToVarBytesBE(n) { + return hexToBytes(numberToHexUnpadded(n)); + } + function ensureBytes(title, hex2, expectedLength) { + let res; + if (typeof hex2 === "string") { + try { + res = hexToBytes(hex2); + } catch (e) { + throw new Error(`${title} must be valid hex string, got "${hex2}". Cause: ${e}`); + } + } else if (u8a2(hex2)) { + res = Uint8Array.from(hex2); + } else { + throw new Error(`${title} must be hex string or Uint8Array`); + } + const len = res.length; + if (typeof expectedLength === "number" && len !== expectedLength) + throw new Error(`${title} expected ${expectedLength} bytes, got ${len}`); + return res; + } + function concatBytes2(...arrays) { + const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0)); + let pad2 = 0; + arrays.forEach((a) => { + if (!u8a2(a)) + throw new Error("Uint8Array expected"); + r.set(a, pad2); + pad2 += a.length; + }); + return r; + } + function equalBytes(b1, b2) { + if (b1.length !== b2.length) + return false; + for (let i2 = 0; i2 < b1.length; i2++) + if (b1[i2] !== b2[i2]) + return false; + return true; + } + function utf8ToBytes2(str) { + if (typeof str !== "string") + throw new Error(`utf8ToBytes expected string, got ${typeof str}`); + return new Uint8Array(new TextEncoder().encode(str)); + } + function bitLen(n) { + let len; + for (len = 0; n > _0n; n >>= _1n, len += 1) + ; + return len; + } + function bitGet(n, pos) { + return n >> BigInt(pos) & _1n; + } + var bitSet = (n, pos, value) => { + return n | (value ? _1n : _0n) << BigInt(pos); + }; + var bitMask = (n) => (_2n << BigInt(n - 1)) - _1n; + var u8n = (data) => new Uint8Array(data); + var u8fr = (arr) => Uint8Array.from(arr); + function createHmacDrbg(hashLen, qByteLen, hmacFn) { + if (typeof hashLen !== "number" || hashLen < 2) + throw new Error("hashLen must be a number"); + if (typeof qByteLen !== "number" || qByteLen < 2) + throw new Error("qByteLen must be a number"); + if (typeof hmacFn !== "function") + throw new Error("hmacFn must be a function"); + let v = u8n(hashLen); + let k = u8n(hashLen); + let i2 = 0; + const reset = () => { + v.fill(1); + k.fill(0); + i2 = 0; + }; + const h = (...b) => hmacFn(k, v, ...b); + const reseed = (seed = u8n()) => { + k = h(u8fr([0]), seed); + v = h(); + if (seed.length === 0) + return; + k = h(u8fr([1]), seed); + v = h(); + }; + const gen = () => { + if (i2++ >= 1e3) + throw new Error("drbg: tried 1000 values"); + let len = 0; + const out = []; + while (len < qByteLen) { + v = h(); + const sl = v.slice(); + out.push(sl); + len += v.length; + } + return concatBytes2(...out); + }; + const genUntil = (seed, pred) => { + reset(); + reseed(seed); + let res = void 0; + while (!(res = pred(gen()))) + reseed(); + reset(); + return res; + }; + return genUntil; + } + var validatorFns = { + bigint: (val) => typeof val === "bigint", + function: (val) => typeof val === "function", + boolean: (val) => typeof val === "boolean", + string: (val) => typeof val === "string", + stringOrUint8Array: (val) => typeof val === "string" || val instanceof Uint8Array, + isSafeInteger: (val) => Number.isSafeInteger(val), + array: (val) => Array.isArray(val), + field: (val, object) => object.Fp.isValid(val), + hash: (val) => typeof val === "function" && Number.isSafeInteger(val.outputLen) + }; + function validateObject(object, validators, optValidators = {}) { + const checkField = (fieldName, type, isOptional) => { + const checkVal = validatorFns[type]; + if (typeof checkVal !== "function") + throw new Error(`Invalid validator "${type}", expected function`); + const val = object[fieldName]; + if (isOptional && val === void 0) + return; + if (!checkVal(val, object)) { + throw new Error(`Invalid param ${String(fieldName)}=${val} (${typeof val}), expected ${type}`); + } + }; + for (const [fieldName, type] of Object.entries(validators)) + checkField(fieldName, type, false); + for (const [fieldName, type] of Object.entries(optValidators)) + checkField(fieldName, type, true); + return object; + } + + // node_modules/@noble/curves/esm/abstract/modular.js + var _0n2 = BigInt(0); + var _1n2 = BigInt(1); + var _2n2 = BigInt(2); + var _3n = BigInt(3); + var _4n = BigInt(4); + var _5n = BigInt(5); + var _8n = BigInt(8); + var _9n = BigInt(9); + var _16n = BigInt(16); + function mod(a, b) { + const result = a % b; + return result >= _0n2 ? result : b + result; + } + function pow(num, power, modulo) { + if (modulo <= _0n2 || power < _0n2) + throw new Error("Expected power/modulo > 0"); + if (modulo === _1n2) + return _0n2; + let res = _1n2; + while (power > _0n2) { + if (power & _1n2) + res = res * num % modulo; + num = num * num % modulo; + power >>= _1n2; + } + return res; + } + function pow2(x, power, modulo) { + let res = x; + while (power-- > _0n2) { + res *= res; + res %= modulo; + } + return res; + } + function invert(number4, modulo) { + if (number4 === _0n2 || modulo <= _0n2) { + throw new Error(`invert: expected positive integers, got n=${number4} mod=${modulo}`); + } + let a = mod(number4, modulo); + let b = modulo; + let x = _0n2, y = _1n2, u = _1n2, v = _0n2; + while (a !== _0n2) { + const q = b / a; + const r = b % a; + const m = x - u * q; + const n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + const gcd2 = b; + if (gcd2 !== _1n2) + throw new Error("invert: does not exist"); + return mod(x, modulo); + } + function tonelliShanks(P) { + const legendreC = (P - _1n2) / _2n2; + let Q, S, Z; + for (Q = P - _1n2, S = 0; Q % _2n2 === _0n2; Q /= _2n2, S++) + ; + for (Z = _2n2; Z < P && pow(Z, legendreC, P) !== P - _1n2; Z++) + ; + if (S === 1) { + const p1div4 = (P + _1n2) / _4n; + return function tonelliFast(Fp3, n) { + const root = Fp3.pow(n, p1div4); + if (!Fp3.eql(Fp3.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + const Q1div2 = (Q + _1n2) / _2n2; + return function tonelliSlow(Fp3, n) { + if (Fp3.pow(n, legendreC) === Fp3.neg(Fp3.ONE)) + throw new Error("Cannot find square root"); + let r = S; + let g = Fp3.pow(Fp3.mul(Fp3.ONE, Z), Q); + let x = Fp3.pow(n, Q1div2); + let b = Fp3.pow(n, Q); + while (!Fp3.eql(b, Fp3.ONE)) { + if (Fp3.eql(b, Fp3.ZERO)) + return Fp3.ZERO; + let m = 1; + for (let t2 = Fp3.sqr(b); m < r; m++) { + if (Fp3.eql(t2, Fp3.ONE)) + break; + t2 = Fp3.sqr(t2); + } + const ge2 = Fp3.pow(g, _1n2 << BigInt(r - m - 1)); + g = Fp3.sqr(ge2); + x = Fp3.mul(x, ge2); + b = Fp3.mul(b, g); + r = m; + } + return x; + }; + } + function FpSqrt(P) { + if (P % _4n === _3n) { + const p1div4 = (P + _1n2) / _4n; + return function sqrt3mod4(Fp3, n) { + const root = Fp3.pow(n, p1div4); + if (!Fp3.eql(Fp3.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + if (P % _8n === _5n) { + const c1 = (P - _5n) / _8n; + return function sqrt5mod8(Fp3, n) { + const n2 = Fp3.mul(n, _2n2); + const v = Fp3.pow(n2, c1); + const nv = Fp3.mul(n, v); + const i2 = Fp3.mul(Fp3.mul(nv, _2n2), v); + const root = Fp3.mul(nv, Fp3.sub(i2, Fp3.ONE)); + if (!Fp3.eql(Fp3.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + if (P % _16n === _9n) { + } + return tonelliShanks(P); + } + var FIELD_FIELDS = [ + "create", + "isValid", + "is0", + "neg", + "inv", + "sqrt", + "sqr", + "eql", + "add", + "sub", + "mul", + "pow", + "div", + "addN", + "subN", + "mulN", + "sqrN" + ]; + function validateField(field) { + const initial = { + ORDER: "bigint", + MASK: "bigint", + BYTES: "isSafeInteger", + BITS: "isSafeInteger" + }; + const opts = FIELD_FIELDS.reduce((map, val) => { + map[val] = "function"; + return map; + }, initial); + return validateObject(field, opts); + } + function FpPow(f2, num, power) { + if (power < _0n2) + throw new Error("Expected power > 0"); + if (power === _0n2) + return f2.ONE; + if (power === _1n2) + return num; + let p = f2.ONE; + let d = num; + while (power > _0n2) { + if (power & _1n2) + p = f2.mul(p, d); + d = f2.sqr(d); + power >>= _1n2; + } + return p; + } + function FpInvertBatch(f2, nums) { + const tmp = new Array(nums.length); + const lastMultiplied = nums.reduce((acc, num, i2) => { + if (f2.is0(num)) + return acc; + tmp[i2] = acc; + return f2.mul(acc, num); + }, f2.ONE); + const inverted = f2.inv(lastMultiplied); + nums.reduceRight((acc, num, i2) => { + if (f2.is0(num)) + return acc; + tmp[i2] = f2.mul(acc, tmp[i2]); + return f2.mul(acc, num); + }, inverted); + return tmp; + } + function nLength(n, nBitLength) { + const _nBitLength = nBitLength !== void 0 ? nBitLength : n.toString(2).length; + const nByteLength = Math.ceil(_nBitLength / 8); + return { nBitLength: _nBitLength, nByteLength }; + } + function Field(ORDER, bitLen3, isLE4 = false, redef = {}) { + if (ORDER <= _0n2) + throw new Error(`Expected Field ORDER > 0, got ${ORDER}`); + const { nBitLength: BITS, nByteLength: BYTES } = nLength(ORDER, bitLen3); + if (BYTES > 2048) + throw new Error("Field lengths over 2048 bytes are not supported"); + const sqrtP = FpSqrt(ORDER); + const f2 = Object.freeze({ + ORDER, + BITS, + BYTES, + MASK: bitMask(BITS), + ZERO: _0n2, + ONE: _1n2, + create: (num) => mod(num, ORDER), + isValid: (num) => { + if (typeof num !== "bigint") + throw new Error(`Invalid field element: expected bigint, got ${typeof num}`); + return _0n2 <= num && num < ORDER; + }, + is0: (num) => num === _0n2, + isOdd: (num) => (num & _1n2) === _1n2, + neg: (num) => mod(-num, ORDER), + eql: (lhs, rhs) => lhs === rhs, + sqr: (num) => mod(num * num, ORDER), + add: (lhs, rhs) => mod(lhs + rhs, ORDER), + sub: (lhs, rhs) => mod(lhs - rhs, ORDER), + mul: (lhs, rhs) => mod(lhs * rhs, ORDER), + pow: (num, power) => FpPow(f2, num, power), + div: (lhs, rhs) => mod(lhs * invert(rhs, ORDER), ORDER), + sqrN: (num) => num * num, + addN: (lhs, rhs) => lhs + rhs, + subN: (lhs, rhs) => lhs - rhs, + mulN: (lhs, rhs) => lhs * rhs, + inv: (num) => invert(num, ORDER), + sqrt: redef.sqrt || ((n) => sqrtP(f2, n)), + invertBatch: (lst) => FpInvertBatch(f2, lst), + cmov: (a, b, c) => c ? b : a, + toBytes: (num) => isLE4 ? numberToBytesLE(num, BYTES) : numberToBytesBE(num, BYTES), + fromBytes: (bytes4) => { + if (bytes4.length !== BYTES) + throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes4.length}`); + return isLE4 ? bytesToNumberLE(bytes4) : bytesToNumberBE(bytes4); + } + }); + return Object.freeze(f2); + } + function getFieldBytesLength(fieldOrder) { + if (typeof fieldOrder !== "bigint") + throw new Error("field order must be bigint"); + const bitLength = fieldOrder.toString(2).length; + return Math.ceil(bitLength / 8); + } + function getMinHashLength(fieldOrder) { + const length = getFieldBytesLength(fieldOrder); + return length + Math.ceil(length / 2); + } + function mapHashToField(key, fieldOrder, isLE4 = false) { + const len = key.length; + const fieldLen = getFieldBytesLength(fieldOrder); + const minLen = getMinHashLength(fieldOrder); + if (len < 16 || len < minLen || len > 1024) + throw new Error(`expected ${minLen}-1024 bytes of input, got ${len}`); + const num = isLE4 ? bytesToNumberBE(key) : bytesToNumberLE(key); + const reduced = mod(num, fieldOrder - _1n2) + _1n2; + return isLE4 ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen); + } + + // node_modules/@noble/curves/esm/abstract/curve.js + var _0n3 = BigInt(0); + var _1n3 = BigInt(1); + function wNAF(c, bits) { + const constTimeNegate = (condition, item) => { + const neg = item.negate(); + return condition ? neg : item; + }; + const opts = (W) => { + const windows = Math.ceil(bits / W) + 1; + const windowSize = 2 ** (W - 1); + return { windows, windowSize }; + }; + return { + constTimeNegate, + unsafeLadder(elm, n) { + let p = c.ZERO; + let d = elm; + while (n > _0n3) { + if (n & _1n3) + p = p.add(d); + d = d.double(); + n >>= _1n3; + } + return p; + }, + precomputeWindow(elm, W) { + const { windows, windowSize } = opts(W); + const points = []; + let p = elm; + let base = p; + for (let window = 0; window < windows; window++) { + base = p; + points.push(base); + for (let i2 = 1; i2 < windowSize; i2++) { + base = base.add(p); + points.push(base); + } + p = base.double(); + } + return points; + }, + wNAF(W, precomputes, n) { + const { windows, windowSize } = opts(W); + let p = c.ZERO; + let f2 = c.BASE; + const mask = BigInt(2 ** W - 1); + const maxNumber = 2 ** W; + const shiftBy = BigInt(W); + for (let window = 0; window < windows; window++) { + const offset = window * windowSize; + let wbits = Number(n & mask); + n >>= shiftBy; + if (wbits > windowSize) { + wbits -= maxNumber; + n += _1n3; + } + const offset1 = offset; + const offset2 = offset + Math.abs(wbits) - 1; + const cond1 = window % 2 !== 0; + const cond2 = wbits < 0; + if (wbits === 0) { + f2 = f2.add(constTimeNegate(cond1, precomputes[offset1])); + } else { + p = p.add(constTimeNegate(cond2, precomputes[offset2])); + } + } + return { p, f: f2 }; + }, + wNAFCached(P, precomputesMap, n, transform) { + const W = P._WINDOW_SIZE || 1; + let comp = precomputesMap.get(P); + if (!comp) { + comp = this.precomputeWindow(P, W); + if (W !== 1) { + precomputesMap.set(P, transform(comp)); + } + } + return this.wNAF(W, comp, n); + } + }; + } + function validateBasic(curve) { + validateField(curve.Fp); + validateObject(curve, { + n: "bigint", + h: "bigint", + Gx: "field", + Gy: "field" + }, { + nBitLength: "isSafeInteger", + nByteLength: "isSafeInteger" + }); + return Object.freeze({ + ...nLength(curve.n, curve.nBitLength), + ...curve, + ...{ p: curve.Fp.ORDER } + }); + } + + // node_modules/@noble/curves/esm/abstract/weierstrass.js + function validatePointOpts(curve) { + const opts = validateBasic(curve); + validateObject(opts, { + a: "field", + b: "field" + }, { + allowedPrivateKeyLengths: "array", + wrapPrivateKey: "boolean", + isTorsionFree: "function", + clearCofactor: "function", + allowInfinityPoint: "boolean", + fromBytes: "function", + toBytes: "function" + }); + const { endo, Fp: Fp3, a } = opts; + if (endo) { + if (!Fp3.eql(a, Fp3.ZERO)) { + throw new Error("Endomorphism can only be defined for Koblitz curves that have a=0"); + } + if (typeof endo !== "object" || typeof endo.beta !== "bigint" || typeof endo.splitScalar !== "function") { + throw new Error("Expected endomorphism with beta: bigint and splitScalar: function"); + } + } + return Object.freeze({ ...opts }); + } + var { bytesToNumberBE: b2n, hexToBytes: h2b } = utils_exports; + var DER = { + Err: class DERErr extends Error { + constructor(m = "") { + super(m); + } + }, + _parseInt(data) { + const { Err: E } = DER; + if (data.length < 2 || data[0] !== 2) + throw new E("Invalid signature integer tag"); + const len = data[1]; + const res = data.subarray(2, len + 2); + if (!len || res.length !== len) + throw new E("Invalid signature integer: wrong length"); + if (res[0] & 128) + throw new E("Invalid signature integer: negative"); + if (res[0] === 0 && !(res[1] & 128)) + throw new E("Invalid signature integer: unnecessary leading zero"); + return { d: b2n(res), l: data.subarray(len + 2) }; + }, + toSig(hex2) { + const { Err: E } = DER; + const data = typeof hex2 === "string" ? h2b(hex2) : hex2; + if (!(data instanceof Uint8Array)) + throw new Error("ui8a expected"); + let l = data.length; + if (l < 2 || data[0] != 48) + throw new E("Invalid signature tag"); + if (data[1] !== l - 2) + throw new E("Invalid signature: incorrect length"); + const { d: r, l: sBytes } = DER._parseInt(data.subarray(2)); + const { d: s, l: rBytesLeft } = DER._parseInt(sBytes); + if (rBytesLeft.length) + throw new E("Invalid signature: left bytes after parsing"); + return { r, s }; + }, + hexFromSig(sig) { + const slice = (s2) => Number.parseInt(s2[0], 16) & 8 ? "00" + s2 : s2; + const h = (num) => { + const hex2 = num.toString(16); + return hex2.length & 1 ? `0${hex2}` : hex2; + }; + const s = slice(h(sig.s)); + const r = slice(h(sig.r)); + const shl = s.length / 2; + const rhl = r.length / 2; + const sl = h(shl); + const rl = h(rhl); + return `30${h(rhl + shl + 4)}02${rl}${r}02${sl}${s}`; + } + }; + var _0n4 = BigInt(0); + var _1n4 = BigInt(1); + var _2n3 = BigInt(2); + var _3n2 = BigInt(3); + var _4n2 = BigInt(4); + function weierstrassPoints(opts) { + const CURVE = validatePointOpts(opts); + const { Fp: Fp3 } = CURVE; + const toBytes4 = CURVE.toBytes || ((_c, point, _isCompressed) => { + const a = point.toAffine(); + return concatBytes2(Uint8Array.from([4]), Fp3.toBytes(a.x), Fp3.toBytes(a.y)); + }); + const fromBytes = CURVE.fromBytes || ((bytes4) => { + const tail = bytes4.subarray(1); + const x = Fp3.fromBytes(tail.subarray(0, Fp3.BYTES)); + const y = Fp3.fromBytes(tail.subarray(Fp3.BYTES, 2 * Fp3.BYTES)); + return { x, y }; + }); + function weierstrassEquation(x) { + const { a, b } = CURVE; + const x2 = Fp3.sqr(x); + const x3 = Fp3.mul(x2, x); + return Fp3.add(Fp3.add(x3, Fp3.mul(x, a)), b); + } + if (!Fp3.eql(Fp3.sqr(CURVE.Gy), weierstrassEquation(CURVE.Gx))) + throw new Error("bad generator point: equation left != right"); + function isWithinCurveOrder(num) { + return typeof num === "bigint" && _0n4 < num && num < CURVE.n; + } + function assertGE(num) { + if (!isWithinCurveOrder(num)) + throw new Error("Expected valid bigint: 0 < bigint < curve.n"); + } + function normPrivateKeyToScalar(key) { + const { allowedPrivateKeyLengths: lengths, nByteLength, wrapPrivateKey, n } = CURVE; + if (lengths && typeof key !== "bigint") { + if (key instanceof Uint8Array) + key = bytesToHex(key); + if (typeof key !== "string" || !lengths.includes(key.length)) + throw new Error("Invalid key"); + key = key.padStart(nByteLength * 2, "0"); + } + let num; + try { + num = typeof key === "bigint" ? key : bytesToNumberBE(ensureBytes("private key", key, nByteLength)); + } catch (error) { + throw new Error(`private key must be ${nByteLength} bytes, hex or bigint, not ${typeof key}`); + } + if (wrapPrivateKey) + num = mod(num, n); + assertGE(num); + return num; + } + const pointPrecomputes = /* @__PURE__ */ new Map(); + function assertPrjPoint(other) { + if (!(other instanceof Point4)) + throw new Error("ProjectivePoint expected"); + } + class Point4 { + constructor(px, py, pz) { + this.px = px; + this.py = py; + this.pz = pz; + if (px == null || !Fp3.isValid(px)) + throw new Error("x required"); + if (py == null || !Fp3.isValid(py)) + throw new Error("y required"); + if (pz == null || !Fp3.isValid(pz)) + throw new Error("z required"); + } + static fromAffine(p) { + const { x, y } = p || {}; + if (!p || !Fp3.isValid(x) || !Fp3.isValid(y)) + throw new Error("invalid affine point"); + if (p instanceof Point4) + throw new Error("projective point not allowed"); + const is0 = (i2) => Fp3.eql(i2, Fp3.ZERO); + if (is0(x) && is0(y)) + return Point4.ZERO; + return new Point4(x, y, Fp3.ONE); + } + get x() { + return this.toAffine().x; + } + get y() { + return this.toAffine().y; + } + static normalizeZ(points) { + const toInv = Fp3.invertBatch(points.map((p) => p.pz)); + return points.map((p, i2) => p.toAffine(toInv[i2])).map(Point4.fromAffine); + } + static fromHex(hex2) { + const P = Point4.fromAffine(fromBytes(ensureBytes("pointHex", hex2))); + P.assertValidity(); + return P; + } + static fromPrivateKey(privateKey) { + return Point4.BASE.multiply(normPrivateKeyToScalar(privateKey)); + } + _setWindowSize(windowSize) { + this._WINDOW_SIZE = windowSize; + pointPrecomputes.delete(this); + } + assertValidity() { + if (this.is0()) { + if (CURVE.allowInfinityPoint && !Fp3.is0(this.py)) + return; + throw new Error("bad point: ZERO"); + } + const { x, y } = this.toAffine(); + if (!Fp3.isValid(x) || !Fp3.isValid(y)) + throw new Error("bad point: x or y not FE"); + const left = Fp3.sqr(y); + const right = weierstrassEquation(x); + if (!Fp3.eql(left, right)) + throw new Error("bad point: equation left != right"); + if (!this.isTorsionFree()) + throw new Error("bad point: not in prime-order subgroup"); + } + hasEvenY() { + const { y } = this.toAffine(); + if (Fp3.isOdd) + return !Fp3.isOdd(y); + throw new Error("Field doesn't support isOdd"); + } + equals(other) { + assertPrjPoint(other); + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = other; + const U1 = Fp3.eql(Fp3.mul(X1, Z2), Fp3.mul(X2, Z1)); + const U2 = Fp3.eql(Fp3.mul(Y1, Z2), Fp3.mul(Y2, Z1)); + return U1 && U2; + } + negate() { + return new Point4(this.px, Fp3.neg(this.py), this.pz); + } + double() { + const { a, b } = CURVE; + const b3 = Fp3.mul(b, _3n2); + const { px: X1, py: Y1, pz: Z1 } = this; + let X3 = Fp3.ZERO, Y3 = Fp3.ZERO, Z3 = Fp3.ZERO; + let t0 = Fp3.mul(X1, X1); + let t1 = Fp3.mul(Y1, Y1); + let t2 = Fp3.mul(Z1, Z1); + let t3 = Fp3.mul(X1, Y1); + t3 = Fp3.add(t3, t3); + Z3 = Fp3.mul(X1, Z1); + Z3 = Fp3.add(Z3, Z3); + X3 = Fp3.mul(a, Z3); + Y3 = Fp3.mul(b3, t2); + Y3 = Fp3.add(X3, Y3); + X3 = Fp3.sub(t1, Y3); + Y3 = Fp3.add(t1, Y3); + Y3 = Fp3.mul(X3, Y3); + X3 = Fp3.mul(t3, X3); + Z3 = Fp3.mul(b3, Z3); + t2 = Fp3.mul(a, t2); + t3 = Fp3.sub(t0, t2); + t3 = Fp3.mul(a, t3); + t3 = Fp3.add(t3, Z3); + Z3 = Fp3.add(t0, t0); + t0 = Fp3.add(Z3, t0); + t0 = Fp3.add(t0, t2); + t0 = Fp3.mul(t0, t3); + Y3 = Fp3.add(Y3, t0); + t2 = Fp3.mul(Y1, Z1); + t2 = Fp3.add(t2, t2); + t0 = Fp3.mul(t2, t3); + X3 = Fp3.sub(X3, t0); + Z3 = Fp3.mul(t2, t1); + Z3 = Fp3.add(Z3, Z3); + Z3 = Fp3.add(Z3, Z3); + return new Point4(X3, Y3, Z3); + } + add(other) { + assertPrjPoint(other); + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = other; + let X3 = Fp3.ZERO, Y3 = Fp3.ZERO, Z3 = Fp3.ZERO; + const a = CURVE.a; + const b3 = Fp3.mul(CURVE.b, _3n2); + let t0 = Fp3.mul(X1, X2); + let t1 = Fp3.mul(Y1, Y2); + let t2 = Fp3.mul(Z1, Z2); + let t3 = Fp3.add(X1, Y1); + let t4 = Fp3.add(X2, Y2); + t3 = Fp3.mul(t3, t4); + t4 = Fp3.add(t0, t1); + t3 = Fp3.sub(t3, t4); + t4 = Fp3.add(X1, Z1); + let t5 = Fp3.add(X2, Z2); + t4 = Fp3.mul(t4, t5); + t5 = Fp3.add(t0, t2); + t4 = Fp3.sub(t4, t5); + t5 = Fp3.add(Y1, Z1); + X3 = Fp3.add(Y2, Z2); + t5 = Fp3.mul(t5, X3); + X3 = Fp3.add(t1, t2); + t5 = Fp3.sub(t5, X3); + Z3 = Fp3.mul(a, t4); + X3 = Fp3.mul(b3, t2); + Z3 = Fp3.add(X3, Z3); + X3 = Fp3.sub(t1, Z3); + Z3 = Fp3.add(t1, Z3); + Y3 = Fp3.mul(X3, Z3); + t1 = Fp3.add(t0, t0); + t1 = Fp3.add(t1, t0); + t2 = Fp3.mul(a, t2); + t4 = Fp3.mul(b3, t4); + t1 = Fp3.add(t1, t2); + t2 = Fp3.sub(t0, t2); + t2 = Fp3.mul(a, t2); + t4 = Fp3.add(t4, t2); + t0 = Fp3.mul(t1, t4); + Y3 = Fp3.add(Y3, t0); + t0 = Fp3.mul(t5, t4); + X3 = Fp3.mul(t3, X3); + X3 = Fp3.sub(X3, t0); + t0 = Fp3.mul(t3, t1); + Z3 = Fp3.mul(t5, Z3); + Z3 = Fp3.add(Z3, t0); + return new Point4(X3, Y3, Z3); + } + subtract(other) { + return this.add(other.negate()); + } + is0() { + return this.equals(Point4.ZERO); + } + wNAF(n) { + return wnaf.wNAFCached(this, pointPrecomputes, n, (comp) => { + const toInv = Fp3.invertBatch(comp.map((p) => p.pz)); + return comp.map((p, i2) => p.toAffine(toInv[i2])).map(Point4.fromAffine); + }); + } + multiplyUnsafe(n) { + const I = Point4.ZERO; + if (n === _0n4) + return I; + assertGE(n); + if (n === _1n4) + return this; + const { endo } = CURVE; + if (!endo) + return wnaf.unsafeLadder(this, n); + let { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); + let k1p = I; + let k2p = I; + let d = this; + while (k1 > _0n4 || k2 > _0n4) { + if (k1 & _1n4) + k1p = k1p.add(d); + if (k2 & _1n4) + k2p = k2p.add(d); + d = d.double(); + k1 >>= _1n4; + k2 >>= _1n4; + } + if (k1neg) + k1p = k1p.negate(); + if (k2neg) + k2p = k2p.negate(); + k2p = new Point4(Fp3.mul(k2p.px, endo.beta), k2p.py, k2p.pz); + return k1p.add(k2p); + } + multiply(scalar) { + assertGE(scalar); + let n = scalar; + let point, fake; + const { endo } = CURVE; + if (endo) { + const { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); + let { p: k1p, f: f1p } = this.wNAF(k1); + let { p: k2p, f: f2p } = this.wNAF(k2); + k1p = wnaf.constTimeNegate(k1neg, k1p); + k2p = wnaf.constTimeNegate(k2neg, k2p); + k2p = new Point4(Fp3.mul(k2p.px, endo.beta), k2p.py, k2p.pz); + point = k1p.add(k2p); + fake = f1p.add(f2p); + } else { + const { p, f: f2 } = this.wNAF(n); + point = p; + fake = f2; + } + return Point4.normalizeZ([point, fake])[0]; + } + multiplyAndAddUnsafe(Q, a, b) { + const G = Point4.BASE; + const mul3 = (P, a2) => a2 === _0n4 || a2 === _1n4 || !P.equals(G) ? P.multiplyUnsafe(a2) : P.multiply(a2); + const sum = mul3(this, a).add(mul3(Q, b)); + return sum.is0() ? void 0 : sum; + } + toAffine(iz) { + const { px: x, py: y, pz: z } = this; + const is0 = this.is0(); + if (iz == null) + iz = is0 ? Fp3.ONE : Fp3.inv(z); + const ax = Fp3.mul(x, iz); + const ay = Fp3.mul(y, iz); + const zz = Fp3.mul(z, iz); + if (is0) + return { x: Fp3.ZERO, y: Fp3.ZERO }; + if (!Fp3.eql(zz, Fp3.ONE)) + throw new Error("invZ was invalid"); + return { x: ax, y: ay }; + } + isTorsionFree() { + const { h: cofactor, isTorsionFree } = CURVE; + if (cofactor === _1n4) + return true; + if (isTorsionFree) + return isTorsionFree(Point4, this); + throw new Error("isTorsionFree() has not been declared for the elliptic curve"); + } + clearCofactor() { + const { h: cofactor, clearCofactor } = CURVE; + if (cofactor === _1n4) + return this; + if (clearCofactor) + return clearCofactor(Point4, this); + return this.multiplyUnsafe(CURVE.h); + } + toRawBytes(isCompressed = true) { + this.assertValidity(); + return toBytes4(Point4, this, isCompressed); + } + toHex(isCompressed = true) { + return bytesToHex(this.toRawBytes(isCompressed)); + } + } + Point4.BASE = new Point4(CURVE.Gx, CURVE.Gy, Fp3.ONE); + Point4.ZERO = new Point4(Fp3.ZERO, Fp3.ONE, Fp3.ZERO); + const _bits = CURVE.nBitLength; + const wnaf = wNAF(Point4, CURVE.endo ? Math.ceil(_bits / 2) : _bits); + return { + CURVE, + ProjectivePoint: Point4, + normPrivateKeyToScalar, + weierstrassEquation, + isWithinCurveOrder + }; + } + function validateOpts(curve) { + const opts = validateBasic(curve); + validateObject(opts, { + hash: "hash", + hmac: "function", + randomBytes: "function" + }, { + bits2int: "function", + bits2int_modN: "function", + lowS: "boolean" + }); + return Object.freeze({ lowS: true, ...opts }); + } + function weierstrass(curveDef) { + const CURVE = validateOpts(curveDef); + const { Fp: Fp3, n: CURVE_ORDER } = CURVE; + const compressedLen = Fp3.BYTES + 1; + const uncompressedLen = 2 * Fp3.BYTES + 1; + function isValidFieldElement(num) { + return _0n4 < num && num < Fp3.ORDER; + } + function modN2(a) { + return mod(a, CURVE_ORDER); + } + function invN(a) { + return invert(a, CURVE_ORDER); + } + const { ProjectivePoint: Point4, normPrivateKeyToScalar, weierstrassEquation, isWithinCurveOrder } = weierstrassPoints({ + ...CURVE, + toBytes(_c, point, isCompressed) { + const a = point.toAffine(); + const x = Fp3.toBytes(a.x); + const cat = concatBytes2; + if (isCompressed) { + return cat(Uint8Array.from([point.hasEvenY() ? 2 : 3]), x); + } else { + return cat(Uint8Array.from([4]), x, Fp3.toBytes(a.y)); + } + }, + fromBytes(bytes4) { + const len = bytes4.length; + const head = bytes4[0]; + const tail = bytes4.subarray(1); + if (len === compressedLen && (head === 2 || head === 3)) { + const x = bytesToNumberBE(tail); + if (!isValidFieldElement(x)) + throw new Error("Point is not on curve"); + const y2 = weierstrassEquation(x); + let y = Fp3.sqrt(y2); + const isYOdd = (y & _1n4) === _1n4; + const isHeadOdd = (head & 1) === 1; + if (isHeadOdd !== isYOdd) + y = Fp3.neg(y); + return { x, y }; + } else if (len === uncompressedLen && head === 4) { + const x = Fp3.fromBytes(tail.subarray(0, Fp3.BYTES)); + const y = Fp3.fromBytes(tail.subarray(Fp3.BYTES, 2 * Fp3.BYTES)); + return { x, y }; + } else { + throw new Error(`Point of length ${len} was invalid. Expected ${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes`); + } + } + }); + const numToNByteStr = (num) => bytesToHex(numberToBytesBE(num, CURVE.nByteLength)); + function isBiggerThanHalfOrder(number4) { + const HALF = CURVE_ORDER >> _1n4; + return number4 > HALF; + } + function normalizeS(s) { + return isBiggerThanHalfOrder(s) ? modN2(-s) : s; + } + const slcNum = (b, from, to) => bytesToNumberBE(b.slice(from, to)); + class Signature { + constructor(r, s, recovery) { + this.r = r; + this.s = s; + this.recovery = recovery; + this.assertValidity(); + } + static fromCompact(hex2) { + const l = CURVE.nByteLength; + hex2 = ensureBytes("compactSignature", hex2, l * 2); + return new Signature(slcNum(hex2, 0, l), slcNum(hex2, l, 2 * l)); + } + static fromDER(hex2) { + const { r, s } = DER.toSig(ensureBytes("DER", hex2)); + return new Signature(r, s); + } + assertValidity() { + if (!isWithinCurveOrder(this.r)) + throw new Error("r must be 0 < r < CURVE.n"); + if (!isWithinCurveOrder(this.s)) + throw new Error("s must be 0 < s < CURVE.n"); + } + addRecoveryBit(recovery) { + return new Signature(this.r, this.s, recovery); + } + recoverPublicKey(msgHash) { + const { r, s, recovery: rec } = this; + const h = bits2int_modN(ensureBytes("msgHash", msgHash)); + if (rec == null || ![0, 1, 2, 3].includes(rec)) + throw new Error("recovery id invalid"); + const radj = rec === 2 || rec === 3 ? r + CURVE.n : r; + if (radj >= Fp3.ORDER) + throw new Error("recovery id 2 or 3 invalid"); + const prefix = (rec & 1) === 0 ? "02" : "03"; + const R = Point4.fromHex(prefix + numToNByteStr(radj)); + const ir = invN(radj); + const u1 = modN2(-h * ir); + const u2 = modN2(s * ir); + const Q = Point4.BASE.multiplyAndAddUnsafe(R, u1, u2); + if (!Q) + throw new Error("point at infinify"); + Q.assertValidity(); + return Q; + } + hasHighS() { + return isBiggerThanHalfOrder(this.s); + } + normalizeS() { + return this.hasHighS() ? new Signature(this.r, modN2(-this.s), this.recovery) : this; + } + toDERRawBytes() { + return hexToBytes(this.toDERHex()); + } + toDERHex() { + return DER.hexFromSig({ r: this.r, s: this.s }); + } + toCompactRawBytes() { + return hexToBytes(this.toCompactHex()); + } + toCompactHex() { + return numToNByteStr(this.r) + numToNByteStr(this.s); + } + } + const utils2 = { + isValidPrivateKey(privateKey) { + try { + normPrivateKeyToScalar(privateKey); + return true; + } catch (error) { + return false; + } + }, + normPrivateKeyToScalar, + randomPrivateKey: () => { + const length = getMinHashLength(CURVE.n); + return mapHashToField(CURVE.randomBytes(length), CURVE.n); + }, + precompute(windowSize = 8, point = Point4.BASE) { + point._setWindowSize(windowSize); + point.multiply(BigInt(3)); + return point; + } + }; + function getPublicKey2(privateKey, isCompressed = true) { + return Point4.fromPrivateKey(privateKey).toRawBytes(isCompressed); + } + function isProbPub(item) { + const arr = item instanceof Uint8Array; + const str = typeof item === "string"; + const len = (arr || str) && item.length; + if (arr) + return len === compressedLen || len === uncompressedLen; + if (str) + return len === 2 * compressedLen || len === 2 * uncompressedLen; + if (item instanceof Point4) + return true; + return false; + } + function getSharedSecret(privateA, publicB, isCompressed = true) { + if (isProbPub(privateA)) + throw new Error("first arg must be private key"); + if (!isProbPub(publicB)) + throw new Error("second arg must be public key"); + const b = Point4.fromHex(publicB); + return b.multiply(normPrivateKeyToScalar(privateA)).toRawBytes(isCompressed); + } + const bits2int = CURVE.bits2int || function(bytes4) { + const num = bytesToNumberBE(bytes4); + const delta = bytes4.length * 8 - CURVE.nBitLength; + return delta > 0 ? num >> BigInt(delta) : num; + }; + const bits2int_modN = CURVE.bits2int_modN || function(bytes4) { + return modN2(bits2int(bytes4)); + }; + const ORDER_MASK = bitMask(CURVE.nBitLength); + function int2octets(num) { + if (typeof num !== "bigint") + throw new Error("bigint expected"); + if (!(_0n4 <= num && num < ORDER_MASK)) + throw new Error(`bigint expected < 2^${CURVE.nBitLength}`); + return numberToBytesBE(num, CURVE.nByteLength); + } + function prepSig(msgHash, privateKey, opts = defaultSigOpts) { + if (["recovered", "canonical"].some((k) => k in opts)) + throw new Error("sign() legacy options not supported"); + const { hash: hash3, randomBytes: randomBytes3 } = CURVE; + let { lowS, prehash, extraEntropy: ent } = opts; + if (lowS == null) + lowS = true; + msgHash = ensureBytes("msgHash", msgHash); + if (prehash) + msgHash = ensureBytes("prehashed msgHash", hash3(msgHash)); + const h1int = bits2int_modN(msgHash); + const d = normPrivateKeyToScalar(privateKey); + const seedArgs = [int2octets(d), int2octets(h1int)]; + if (ent != null) { + const e = ent === true ? randomBytes3(Fp3.BYTES) : ent; + seedArgs.push(ensureBytes("extraEntropy", e)); + } + const seed = concatBytes2(...seedArgs); + const m = h1int; + function k2sig(kBytes) { + const k = bits2int(kBytes); + if (!isWithinCurveOrder(k)) + return; + const ik = invN(k); + const q = Point4.BASE.multiply(k).toAffine(); + const r = modN2(q.x); + if (r === _0n4) + return; + const s = modN2(ik * modN2(m + r * d)); + if (s === _0n4) + return; + let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n4); + let normS = s; + if (lowS && isBiggerThanHalfOrder(s)) { + normS = normalizeS(s); + recovery ^= 1; + } + return new Signature(r, normS, recovery); + } + return { seed, k2sig }; + } + const defaultSigOpts = { lowS: CURVE.lowS, prehash: false }; + const defaultVerOpts = { lowS: CURVE.lowS, prehash: false }; + function sign(msgHash, privKey, opts = defaultSigOpts) { + const { seed, k2sig } = prepSig(msgHash, privKey, opts); + const C = CURVE; + const drbg = createHmacDrbg(C.hash.outputLen, C.nByteLength, C.hmac); + return drbg(seed, k2sig); + } + Point4.BASE._setWindowSize(8); + function verify(signature, msgHash, publicKey, opts = defaultVerOpts) { + const sg = signature; + msgHash = ensureBytes("msgHash", msgHash); + publicKey = ensureBytes("publicKey", publicKey); + if ("strict" in opts) + throw new Error("options.strict was renamed to lowS"); + const { lowS, prehash } = opts; + let _sig = void 0; + let P; + try { + if (typeof sg === "string" || sg instanceof Uint8Array) { + try { + _sig = Signature.fromDER(sg); + } catch (derError) { + if (!(derError instanceof DER.Err)) + throw derError; + _sig = Signature.fromCompact(sg); + } + } else if (typeof sg === "object" && typeof sg.r === "bigint" && typeof sg.s === "bigint") { + const { r: r2, s: s2 } = sg; + _sig = new Signature(r2, s2); + } else { + throw new Error("PARSE"); + } + P = Point4.fromHex(publicKey); + } catch (error) { + if (error.message === "PARSE") + throw new Error(`signature must be Signature instance, Uint8Array or hex string`); + return false; + } + if (lowS && _sig.hasHighS()) + return false; + if (prehash) + msgHash = CURVE.hash(msgHash); + const { r, s } = _sig; + const h = bits2int_modN(msgHash); + const is = invN(s); + const u1 = modN2(h * is); + const u2 = modN2(r * is); + const R = Point4.BASE.multiplyAndAddUnsafe(P, u1, u2)?.toAffine(); + if (!R) + return false; + const v = modN2(R.x); + return v === r; + } + return { + CURVE, + getPublicKey: getPublicKey2, + getSharedSecret, + sign, + verify, + ProjectivePoint: Point4, + Signature, + utils: utils2 + }; + } + + // node_modules/@noble/curves/node_modules/@noble/hashes/esm/hmac.js + var HMAC = class extends Hash { + constructor(hash3, _key) { + super(); + this.finished = false; + this.destroyed = false; + hash(hash3); + const key = toBytes(_key); + this.iHash = hash3.create(); + if (typeof this.iHash.update !== "function") + throw new Error("Expected instance of class which extends utils.Hash"); + this.blockLen = this.iHash.blockLen; + this.outputLen = this.iHash.outputLen; + const blockLen = this.blockLen; + const pad2 = new Uint8Array(blockLen); + pad2.set(key.length > blockLen ? hash3.create().update(key).digest() : key); + for (let i2 = 0; i2 < pad2.length; i2++) + pad2[i2] ^= 54; + this.iHash.update(pad2); + this.oHash = hash3.create(); + for (let i2 = 0; i2 < pad2.length; i2++) + pad2[i2] ^= 54 ^ 92; + this.oHash.update(pad2); + pad2.fill(0); + } + update(buf) { + exists(this); + this.iHash.update(buf); + return this; + } + digestInto(out) { + exists(this); + bytes(out, this.outputLen); + this.finished = true; + this.iHash.digestInto(out); + this.oHash.update(out); + this.oHash.digestInto(out); + this.destroy(); + } + digest() { + const out = new Uint8Array(this.oHash.outputLen); + this.digestInto(out); + return out; + } + _cloneInto(to) { + to || (to = Object.create(Object.getPrototypeOf(this), {})); + const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this; + to = to; + to.finished = finished; + to.destroyed = destroyed; + to.blockLen = blockLen; + to.outputLen = outputLen; + to.oHash = oHash._cloneInto(to.oHash); + to.iHash = iHash._cloneInto(to.iHash); + return to; + } + destroy() { + this.destroyed = true; + this.oHash.destroy(); + this.iHash.destroy(); + } + }; + var hmac = (hash3, key, message) => new HMAC(hash3, key).update(message).digest(); + hmac.create = (hash3, key) => new HMAC(hash3, key); + + // node_modules/@noble/curves/esm/_shortw_utils.js + function getHash(hash3) { + return { + hash: hash3, + hmac: (key, ...msgs) => hmac(hash3, key, concatBytes(...msgs)), + randomBytes + }; + } + function createCurve(curveDef, defHash) { + const create = (hash3) => weierstrass({ ...curveDef, ...getHash(hash3) }); + return Object.freeze({ ...create(defHash), create }); + } + + // node_modules/@noble/curves/esm/secp256k1.js + var secp256k1P = BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); + var secp256k1N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + var _1n5 = BigInt(1); + var _2n4 = BigInt(2); + var divNearest = (a, b) => (a + b / _2n4) / b; + function sqrtMod(y) { + const P = secp256k1P; + const _3n5 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22); + const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88); + const b2 = y * y * y % P; + const b3 = b2 * b2 * y % P; + const b6 = pow2(b3, _3n5, P) * b3 % P; + const b9 = pow2(b6, _3n5, P) * b3 % P; + const b11 = pow2(b9, _2n4, P) * b2 % P; + const b22 = pow2(b11, _11n, P) * b11 % P; + const b44 = pow2(b22, _22n, P) * b22 % P; + const b88 = pow2(b44, _44n, P) * b44 % P; + const b176 = pow2(b88, _88n, P) * b88 % P; + const b220 = pow2(b176, _44n, P) * b44 % P; + const b223 = pow2(b220, _3n5, P) * b3 % P; + const t1 = pow2(b223, _23n, P) * b22 % P; + const t2 = pow2(t1, _6n, P) * b2 % P; + const root = pow2(t2, _2n4, P); + if (!Fp.eql(Fp.sqr(root), y)) + throw new Error("Cannot find square root"); + return root; + } + var Fp = Field(secp256k1P, void 0, void 0, { sqrt: sqrtMod }); + var secp256k1 = createCurve({ + a: BigInt(0), + b: BigInt(7), + Fp, + n: secp256k1N, + Gx: BigInt("55066263022277343669578718895168534326250603453777594175500187360389116729240"), + Gy: BigInt("32670510020758816978083085130507043184471273380659243275938904335757337482424"), + h: BigInt(1), + lowS: true, + endo: { + beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"), + splitScalar: (k) => { + const n = secp256k1N; + const a1 = BigInt("0x3086d221a7d46bcde86c90e49284eb15"); + const b1 = -_1n5 * BigInt("0xe4437ed6010e88286f547fa90abfe4c3"); + const a2 = BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"); + const b2 = a1; + const POW_2_128 = BigInt("0x100000000000000000000000000000000"); + const c1 = divNearest(b2 * k, n); + const c2 = divNearest(-b1 * k, n); + let k1 = mod(k - c1 * a1 - c2 * a2, n); + let k2 = mod(-c1 * b1 - c2 * b2, n); + const k1neg = k1 > POW_2_128; + const k2neg = k2 > POW_2_128; + if (k1neg) + k1 = n - k1; + if (k2neg) + k2 = n - k2; + if (k1 > POW_2_128 || k2 > POW_2_128) { + throw new Error("splitScalar: Endomorphism failed, k=" + k); + } + return { k1neg, k1, k2neg, k2 }; + } + } + }, sha256); + var _0n5 = BigInt(0); + var fe = (x) => typeof x === "bigint" && _0n5 < x && x < secp256k1P; + var ge = (x) => typeof x === "bigint" && _0n5 < x && x < secp256k1N; + var TAGGED_HASH_PREFIXES = {}; + function taggedHash(tag, ...messages) { + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === void 0) { + const tagH = sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0))); + tagP = concatBytes2(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return sha256(concatBytes2(tagP, ...messages)); + } + var pointToBytes = (point) => point.toRawBytes(true).slice(1); + var numTo32b = (n) => numberToBytesBE(n, 32); + var modP = (x) => mod(x, secp256k1P); + var modN = (x) => mod(x, secp256k1N); + var Point = secp256k1.ProjectivePoint; + var GmulAdd = (Q, a, b) => Point.BASE.multiplyAndAddUnsafe(Q, a, b); + function schnorrGetExtPubKey(priv) { + let d_ = secp256k1.utils.normPrivateKeyToScalar(priv); + let p = Point.fromPrivateKey(d_); + const scalar = p.hasEvenY() ? d_ : modN(-d_); + return { scalar, bytes: pointToBytes(p) }; + } + function lift_x(x) { + if (!fe(x)) + throw new Error("bad x: need 0 < x < p"); + const xx = modP(x * x); + const c = modP(xx * x + BigInt(7)); + let y = sqrtMod(c); + if (y % _2n4 !== _0n5) + y = modP(-y); + const p = new Point(x, y, _1n5); + p.assertValidity(); + return p; + } + function challenge(...args) { + return modN(bytesToNumberBE(taggedHash("BIP0340/challenge", ...args))); + } + function schnorrGetPublicKey(privateKey) { + return schnorrGetExtPubKey(privateKey).bytes; + } + function schnorrSign(message, privateKey, auxRand = randomBytes(32)) { + const m = ensureBytes("message", message); + const { bytes: px, scalar: d } = schnorrGetExtPubKey(privateKey); + const a = ensureBytes("auxRand", auxRand, 32); + const t = numTo32b(d ^ bytesToNumberBE(taggedHash("BIP0340/aux", a))); + const rand = taggedHash("BIP0340/nonce", t, px, m); + const k_ = modN(bytesToNumberBE(rand)); + if (k_ === _0n5) + throw new Error("sign failed: k is zero"); + const { bytes: rx, scalar: k } = schnorrGetExtPubKey(k_); + const e = challenge(rx, px, m); + const sig = new Uint8Array(64); + sig.set(rx, 0); + sig.set(numTo32b(modN(k + e * d)), 32); + if (!schnorrVerify(sig, m, px)) + throw new Error("sign: Invalid signature produced"); + return sig; + } + function schnorrVerify(signature, message, publicKey) { + const sig = ensureBytes("signature", signature, 64); + const m = ensureBytes("message", message); + const pub = ensureBytes("publicKey", publicKey, 32); + try { + const P = lift_x(bytesToNumberBE(pub)); + const r = bytesToNumberBE(sig.subarray(0, 32)); + if (!fe(r)) + return false; + const s = bytesToNumberBE(sig.subarray(32, 64)); + if (!ge(s)) + return false; + const e = challenge(numTo32b(r), pointToBytes(P), m); + const R = GmulAdd(P, s, modN(-e)); + if (!R || !R.hasEvenY() || R.toAffine().x !== r) + return false; + return true; + } catch (error) { + return false; + } + } + var schnorr = /* @__PURE__ */ (() => ({ + getPublicKey: schnorrGetPublicKey, + sign: schnorrSign, + verify: schnorrVerify, + utils: { + randomPrivateKey: secp256k1.utils.randomPrivateKey, + lift_x, + pointToBytes, + numberToBytesBE, + bytesToNumberBE, + taggedHash, + mod + } + }))(); + + // node_modules/@noble/hashes/esm/crypto.js + var crypto2 = typeof globalThis === "object" && "crypto" in globalThis ? globalThis.crypto : void 0; + + // node_modules/@noble/hashes/esm/utils.js + var u8a3 = (a) => a instanceof Uint8Array; + var createView2 = (arr) => new DataView(arr.buffer, arr.byteOffset, arr.byteLength); + var rotr2 = (word, shift) => word << 32 - shift | word >>> shift; + var isLE2 = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68; + if (!isLE2) + throw new Error("Non little-endian hardware is not supported"); + var hexes2 = Array.from({ length: 256 }, (v, i2) => i2.toString(16).padStart(2, "0")); + function bytesToHex2(bytes4) { + if (!u8a3(bytes4)) + throw new Error("Uint8Array expected"); + let hex2 = ""; + for (let i2 = 0; i2 < bytes4.length; i2++) { + hex2 += hexes2[bytes4[i2]]; + } + return hex2; + } + function hexToBytes2(hex2) { + if (typeof hex2 !== "string") + throw new Error("hex string expected, got " + typeof hex2); + const len = hex2.length; + if (len % 2) + throw new Error("padded hex string expected, got unpadded hex of length " + len); + const array = new Uint8Array(len / 2); + for (let i2 = 0; i2 < array.length; i2++) { + const j = i2 * 2; + const hexByte = hex2.slice(j, j + 2); + const byte = Number.parseInt(hexByte, 16); + if (Number.isNaN(byte) || byte < 0) + throw new Error("Invalid byte sequence"); + array[i2] = byte; + } + return array; + } + function utf8ToBytes3(str) { + if (typeof str !== "string") + throw new Error(`utf8ToBytes expected string, got ${typeof str}`); + return new Uint8Array(new TextEncoder().encode(str)); + } + function toBytes2(data) { + if (typeof data === "string") + data = utf8ToBytes3(data); + if (!u8a3(data)) + throw new Error(`expected Uint8Array, got ${typeof data}`); + return data; + } + function concatBytes3(...arrays) { + const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0)); + let pad2 = 0; + arrays.forEach((a) => { + if (!u8a3(a)) + throw new Error("Uint8Array expected"); + r.set(a, pad2); + pad2 += a.length; + }); + return r; + } + var Hash2 = class { + clone() { + return this._cloneInto(); + } + }; + var isPlainObject = (obj) => Object.prototype.toString.call(obj) === "[object Object]" && obj.constructor === Object; + function checkOpts(defaults, opts) { + if (opts !== void 0 && (typeof opts !== "object" || !isPlainObject(opts))) + throw new Error("Options should be object or undefined"); + const merged = Object.assign(defaults, opts); + return merged; + } + function wrapConstructor2(hashCons) { + const hashC = (msg) => hashCons().update(toBytes2(msg)).digest(); + const tmp = hashCons(); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.create = () => hashCons(); + return hashC; + } + function randomBytes2(bytesLength = 32) { + if (crypto2 && typeof crypto2.getRandomValues === "function") { + return crypto2.getRandomValues(new Uint8Array(bytesLength)); + } + throw new Error("crypto.getRandomValues must be defined"); + } + + // core.ts + var verifiedSymbol = Symbol("verified"); + var isRecord = (obj) => obj instanceof Object; + function validateEvent(event) { + if (!isRecord(event)) + return false; + if (typeof event.kind !== "number") + return false; + if (typeof event.content !== "string") + return false; + if (typeof event.created_at !== "number") + return false; + if (typeof event.pubkey !== "string") + return false; + if (!event.pubkey.match(/^[a-f0-9]{64}$/)) + return false; + if (!Array.isArray(event.tags)) + return false; + for (let i2 = 0; i2 < event.tags.length; i2++) { + let tag = event.tags[i2]; + if (!Array.isArray(tag)) + return false; + for (let j = 0; j < tag.length; j++) { + if (typeof tag[j] !== "string") + return false; + } + } + return true; + } + function sortEvents(events) { + return events.sort((a, b) => { + if (a.created_at !== b.created_at) { + return b.created_at - a.created_at; + } + return a.id.localeCompare(b.id); + }); + } + + // node_modules/@noble/hashes/esm/_assert.js + function number2(n) { + if (!Number.isSafeInteger(n) || n < 0) + throw new Error(`Wrong positive integer: ${n}`); + } + function bool(b) { + if (typeof b !== "boolean") + throw new Error(`Expected boolean, not ${b}`); + } + function bytes2(b, ...lengths) { + if (!(b instanceof Uint8Array)) + throw new Error("Expected Uint8Array"); + if (lengths.length > 0 && !lengths.includes(b.length)) + throw new Error(`Expected Uint8Array of length ${lengths}, not of length=${b.length}`); + } + function hash2(hash3) { + if (typeof hash3 !== "function" || typeof hash3.create !== "function") + throw new Error("Hash should be wrapped by utils.wrapConstructor"); + number2(hash3.outputLen); + number2(hash3.blockLen); + } + function exists2(instance, checkFinished = true) { + if (instance.destroyed) + throw new Error("Hash instance has been destroyed"); + if (checkFinished && instance.finished) + throw new Error("Hash#digest() has already been called"); + } + function output2(out, instance) { + bytes2(out); + const min = instance.outputLen; + if (out.length < min) { + throw new Error(`digestInto() expects output buffer of length at least ${min}`); + } + } + var assert = { + number: number2, + bool, + bytes: bytes2, + hash: hash2, + exists: exists2, + output: output2 + }; + var assert_default = assert; + + // node_modules/@noble/hashes/esm/_sha2.js + function setBigUint642(view, byteOffset, value, isLE4) { + if (typeof view.setBigUint64 === "function") + return view.setBigUint64(byteOffset, value, isLE4); + const _32n2 = BigInt(32); + const _u32_max = BigInt(4294967295); + const wh = Number(value >> _32n2 & _u32_max); + const wl = Number(value & _u32_max); + const h = isLE4 ? 4 : 0; + const l = isLE4 ? 0 : 4; + view.setUint32(byteOffset + h, wh, isLE4); + view.setUint32(byteOffset + l, wl, isLE4); + } + var SHA22 = class extends Hash2 { + constructor(blockLen, outputLen, padOffset, isLE4) { + super(); + this.blockLen = blockLen; + this.outputLen = outputLen; + this.padOffset = padOffset; + this.isLE = isLE4; + this.finished = false; + this.length = 0; + this.pos = 0; + this.destroyed = false; + this.buffer = new Uint8Array(blockLen); + this.view = createView2(this.buffer); + } + update(data) { + assert_default.exists(this); + const { view, buffer, blockLen } = this; + data = toBytes2(data); + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + if (take === blockLen) { + const dataView = createView2(data); + for (; blockLen <= len - pos; pos += blockLen) + this.process(dataView, pos); + continue; + } + buffer.set(data.subarray(pos, pos + take), this.pos); + this.pos += take; + pos += take; + if (this.pos === blockLen) { + this.process(view, 0); + this.pos = 0; + } + } + this.length += data.length; + this.roundClean(); + return this; + } + digestInto(out) { + assert_default.exists(this); + assert_default.output(out, this); + this.finished = true; + const { buffer, view, blockLen, isLE: isLE4 } = this; + let { pos } = this; + buffer[pos++] = 128; + this.buffer.subarray(pos).fill(0); + if (this.padOffset > blockLen - pos) { + this.process(view, 0); + pos = 0; + } + for (let i2 = pos; i2 < blockLen; i2++) + buffer[i2] = 0; + setBigUint642(view, blockLen - 8, BigInt(this.length * 8), isLE4); + this.process(view, 0); + const oview = createView2(out); + const len = this.outputLen; + if (len % 4) + throw new Error("_sha2: outputLen should be aligned to 32bit"); + const outLen = len / 4; + const state = this.get(); + if (outLen > state.length) + throw new Error("_sha2: outputLen bigger than state"); + for (let i2 = 0; i2 < outLen; i2++) + oview.setUint32(4 * i2, state[i2], isLE4); + } + digest() { + const { buffer, outputLen } = this; + this.digestInto(buffer); + const res = buffer.slice(0, outputLen); + this.destroy(); + return res; + } + _cloneInto(to) { + to || (to = new this.constructor()); + to.set(...this.get()); + const { blockLen, buffer, length, finished, destroyed, pos } = this; + to.length = length; + to.pos = pos; + to.finished = finished; + to.destroyed = destroyed; + if (length % blockLen) + to.buffer.set(buffer); + return to; + } + }; + + // node_modules/@noble/hashes/esm/sha256.js + var Chi2 = (a, b, c) => a & b ^ ~a & c; + var Maj2 = (a, b, c) => a & b ^ a & c ^ b & c; + var SHA256_K2 = new Uint32Array([ + 1116352408, + 1899447441, + 3049323471, + 3921009573, + 961987163, + 1508970993, + 2453635748, + 2870763221, + 3624381080, + 310598401, + 607225278, + 1426881987, + 1925078388, + 2162078206, + 2614888103, + 3248222580, + 3835390401, + 4022224774, + 264347078, + 604807628, + 770255983, + 1249150122, + 1555081692, + 1996064986, + 2554220882, + 2821834349, + 2952996808, + 3210313671, + 3336571891, + 3584528711, + 113926993, + 338241895, + 666307205, + 773529912, + 1294757372, + 1396182291, + 1695183700, + 1986661051, + 2177026350, + 2456956037, + 2730485921, + 2820302411, + 3259730800, + 3345764771, + 3516065817, + 3600352804, + 4094571909, + 275423344, + 430227734, + 506948616, + 659060556, + 883997877, + 958139571, + 1322822218, + 1537002063, + 1747873779, + 1955562222, + 2024104815, + 2227730452, + 2361852424, + 2428436474, + 2756734187, + 3204031479, + 3329325298 + ]); + var IV2 = new Uint32Array([ + 1779033703, + 3144134277, + 1013904242, + 2773480762, + 1359893119, + 2600822924, + 528734635, + 1541459225 + ]); + var SHA256_W2 = new Uint32Array(64); + var SHA2562 = class extends SHA22 { + constructor() { + super(64, 32, 8, false); + this.A = IV2[0] | 0; + this.B = IV2[1] | 0; + this.C = IV2[2] | 0; + this.D = IV2[3] | 0; + this.E = IV2[4] | 0; + this.F = IV2[5] | 0; + this.G = IV2[6] | 0; + this.H = IV2[7] | 0; + } + get() { + const { A, B, C, D, E, F, G, H } = this; + return [A, B, C, D, E, F, G, H]; + } + set(A, B, C, D, E, F, G, H) { + this.A = A | 0; + this.B = B | 0; + this.C = C | 0; + this.D = D | 0; + this.E = E | 0; + this.F = F | 0; + this.G = G | 0; + this.H = H | 0; + } + process(view, offset) { + for (let i2 = 0; i2 < 16; i2++, offset += 4) + SHA256_W2[i2] = view.getUint32(offset, false); + for (let i2 = 16; i2 < 64; i2++) { + const W15 = SHA256_W2[i2 - 15]; + const W2 = SHA256_W2[i2 - 2]; + const s0 = rotr2(W15, 7) ^ rotr2(W15, 18) ^ W15 >>> 3; + const s1 = rotr2(W2, 17) ^ rotr2(W2, 19) ^ W2 >>> 10; + SHA256_W2[i2] = s1 + SHA256_W2[i2 - 7] + s0 + SHA256_W2[i2 - 16] | 0; + } + let { A, B, C, D, E, F, G, H } = this; + for (let i2 = 0; i2 < 64; i2++) { + const sigma1 = rotr2(E, 6) ^ rotr2(E, 11) ^ rotr2(E, 25); + const T1 = H + sigma1 + Chi2(E, F, G) + SHA256_K2[i2] + SHA256_W2[i2] | 0; + const sigma0 = rotr2(A, 2) ^ rotr2(A, 13) ^ rotr2(A, 22); + const T2 = sigma0 + Maj2(A, B, C) | 0; + H = G; + G = F; + F = E; + E = D + T1 | 0; + D = C; + C = B; + B = A; + A = T1 + T2 | 0; + } + A = A + this.A | 0; + B = B + this.B | 0; + C = C + this.C | 0; + D = D + this.D | 0; + E = E + this.E | 0; + F = F + this.F | 0; + G = G + this.G | 0; + H = H + this.H | 0; + this.set(A, B, C, D, E, F, G, H); + } + roundClean() { + SHA256_W2.fill(0); + } + destroy() { + this.set(0, 0, 0, 0, 0, 0, 0, 0); + this.buffer.fill(0); + } + }; + var SHA224 = class extends SHA2562 { + constructor() { + super(); + this.A = 3238371032 | 0; + this.B = 914150663 | 0; + this.C = 812702999 | 0; + this.D = 4144912697 | 0; + this.E = 4290775857 | 0; + this.F = 1750603025 | 0; + this.G = 1694076839 | 0; + this.H = 3204075428 | 0; + this.outputLen = 28; + } + }; + var sha2562 = wrapConstructor2(() => new SHA2562()); + var sha224 = wrapConstructor2(() => new SHA224()); + + // utils.ts + var utils_exports2 = {}; + __export(utils_exports2, { + Queue: () => Queue, + QueueNode: () => QueueNode, + binarySearch: () => binarySearch, + bytesToHex: () => bytesToHex2, + hexToBytes: () => hexToBytes2, + insertEventIntoAscendingList: () => insertEventIntoAscendingList, + insertEventIntoDescendingList: () => insertEventIntoDescendingList, + normalizeURL: () => normalizeURL, + utf8Decoder: () => utf8Decoder, + utf8Encoder: () => utf8Encoder + }); + var utf8Decoder = new TextDecoder("utf-8"); + var utf8Encoder = new TextEncoder(); + function normalizeURL(url) { + try { + if (url.indexOf("://") === -1) + url = "wss://" + url; + let p = new URL(url); + p.pathname = p.pathname.replace(/\/+/g, "/"); + if (p.pathname.endsWith("/")) + p.pathname = p.pathname.slice(0, -1); + if (p.port === "80" && p.protocol === "ws:" || p.port === "443" && p.protocol === "wss:") + p.port = ""; + p.searchParams.sort(); + p.hash = ""; + return p.toString(); + } catch (e) { + throw new Error(`Invalid URL: ${url}`); + } + } + function insertEventIntoDescendingList(sortedArray, event) { + const [idx, found] = binarySearch(sortedArray, (b) => { + if (event.id === b.id) + return 0; + if (event.created_at === b.created_at) + return -1; + return b.created_at - event.created_at; + }); + if (!found) { + sortedArray.splice(idx, 0, event); + } + return sortedArray; + } + function insertEventIntoAscendingList(sortedArray, event) { + const [idx, found] = binarySearch(sortedArray, (b) => { + if (event.id === b.id) + return 0; + if (event.created_at === b.created_at) + return -1; + return event.created_at - b.created_at; + }); + if (!found) { + sortedArray.splice(idx, 0, event); + } + return sortedArray; + } + function binarySearch(arr, compare) { + let start = 0; + let end = arr.length - 1; + while (start <= end) { + const mid = Math.floor((start + end) / 2); + const cmp = compare(arr[mid]); + if (cmp === 0) { + return [mid, true]; + } + if (cmp < 0) { + end = mid - 1; + } else { + start = mid + 1; + } + } + return [start, false]; + } + var QueueNode = class { + value; + next = null; + prev = null; + constructor(message) { + this.value = message; + } + }; + var Queue = class { + first; + last; + constructor() { + this.first = null; + this.last = null; + } + enqueue(value) { + const newNode = new QueueNode(value); + if (!this.last) { + this.first = newNode; + this.last = newNode; + } else if (this.last === this.first) { + this.last = newNode; + this.last.prev = this.first; + this.first.next = newNode; + } else { + newNode.prev = this.last; + this.last.next = newNode; + this.last = newNode; + } + return true; + } + dequeue() { + if (!this.first) + return null; + if (this.first === this.last) { + const target2 = this.first; + this.first = null; + this.last = null; + return target2.value; + } + const target = this.first; + this.first = target.next; + if (this.first) { + this.first.prev = null; + } + return target.value; + } + }; + + // pure.ts + var JS = class { + generateSecretKey() { + return schnorr.utils.randomPrivateKey(); + } + getPublicKey(secretKey) { + return bytesToHex2(schnorr.getPublicKey(secretKey)); + } + finalizeEvent(t, secretKey) { + const event = t; + event.pubkey = bytesToHex2(schnorr.getPublicKey(secretKey)); + event.id = getEventHash(event); + event.sig = bytesToHex2(schnorr.sign(getEventHash(event), secretKey)); + event[verifiedSymbol] = true; + return event; + } + verifyEvent(event) { + if (typeof event[verifiedSymbol] === "boolean") + return event[verifiedSymbol]; + const hash3 = getEventHash(event); + if (hash3 !== event.id) { + event[verifiedSymbol] = false; + return false; + } + try { + const valid = schnorr.verify(event.sig, hash3, event.pubkey); + event[verifiedSymbol] = valid; + return valid; + } catch (err) { + event[verifiedSymbol] = false; + return false; + } + } + }; + function serializeEvent(evt) { + if (!validateEvent(evt)) + throw new Error("can't serialize event with wrong or missing properties"); + return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]); + } + function getEventHash(event) { + let eventHash = sha2562(utf8Encoder.encode(serializeEvent(event))); + return bytesToHex2(eventHash); + } + var i = new JS(); + var generateSecretKey = i.generateSecretKey; + var getPublicKey = i.getPublicKey; + var finalizeEvent = i.finalizeEvent; + var verifyEvent = i.verifyEvent; + + // kinds.ts + var kinds_exports = {}; + __export(kinds_exports, { + Application: () => Application, + BadgeAward: () => BadgeAward, + BadgeDefinition: () => BadgeDefinition, + BlockedRelaysList: () => BlockedRelaysList, + BookmarkList: () => BookmarkList, + Bookmarksets: () => Bookmarksets, + Calendar: () => Calendar, + CalendarEventRSVP: () => CalendarEventRSVP, + ChannelCreation: () => ChannelCreation, + ChannelHideMessage: () => ChannelHideMessage, + ChannelMessage: () => ChannelMessage, + ChannelMetadata: () => ChannelMetadata, + ChannelMuteUser: () => ChannelMuteUser, + ClassifiedListing: () => ClassifiedListing, + ClientAuth: () => ClientAuth, + CommunitiesList: () => CommunitiesList, + CommunityDefinition: () => CommunityDefinition, + CommunityPostApproval: () => CommunityPostApproval, + Contacts: () => Contacts, + CreateOrUpdateProduct: () => CreateOrUpdateProduct, + CreateOrUpdateStall: () => CreateOrUpdateStall, + Curationsets: () => Curationsets, + Date: () => Date2, + DirectMessageRelaysList: () => DirectMessageRelaysList, + DraftClassifiedListing: () => DraftClassifiedListing, + DraftLong: () => DraftLong, + Emojisets: () => Emojisets, + EncryptedDirectMessage: () => EncryptedDirectMessage, + EventDeletion: () => EventDeletion, + FileMetadata: () => FileMetadata, + FileServerPreference: () => FileServerPreference, + Followsets: () => Followsets, + GenericRepost: () => GenericRepost, + Genericlists: () => Genericlists, + GiftWrap: () => GiftWrap, + HTTPAuth: () => HTTPAuth, + Handlerinformation: () => Handlerinformation, + Handlerrecommendation: () => Handlerrecommendation, + Highlights: () => Highlights, + InterestsList: () => InterestsList, + Interestsets: () => Interestsets, + JobFeedback: () => JobFeedback, + JobRequest: () => JobRequest, + JobResult: () => JobResult, + Label: () => Label, + LightningPubRPC: () => LightningPubRPC, + LiveChatMessage: () => LiveChatMessage, + LiveEvent: () => LiveEvent, + LongFormArticle: () => LongFormArticle, + Metadata: () => Metadata, + Mutelist: () => Mutelist, + NWCWalletInfo: () => NWCWalletInfo, + NWCWalletRequest: () => NWCWalletRequest, + NWCWalletResponse: () => NWCWalletResponse, + NostrConnect: () => NostrConnect, + OpenTimestamps: () => OpenTimestamps, + Pinlist: () => Pinlist, + PrivateDirectMessage: () => PrivateDirectMessage, + ProblemTracker: () => ProblemTracker, + ProfileBadges: () => ProfileBadges, + PublicChatsList: () => PublicChatsList, + Reaction: () => Reaction, + RecommendRelay: () => RecommendRelay, + RelayList: () => RelayList, + Relaysets: () => Relaysets, + Report: () => Report, + Reporting: () => Reporting, + Repost: () => Repost, + Seal: () => Seal, + SearchRelaysList: () => SearchRelaysList, + ShortTextNote: () => ShortTextNote, + Time: () => Time, + UserEmojiList: () => UserEmojiList, + UserStatuses: () => UserStatuses, + Zap: () => Zap, + ZapGoal: () => ZapGoal, + ZapRequest: () => ZapRequest, + classifyKind: () => classifyKind, + isAddressableKind: () => isAddressableKind, + isEphemeralKind: () => isEphemeralKind, + isKind: () => isKind, + isRegularKind: () => isRegularKind, + isReplaceableKind: () => isReplaceableKind + }); + function isRegularKind(kind) { + return 1e3 <= kind && kind < 1e4 || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind); + } + function isReplaceableKind(kind) { + return [0, 3].includes(kind) || 1e4 <= kind && kind < 2e4; + } + function isEphemeralKind(kind) { + return 2e4 <= kind && kind < 3e4; + } + function isAddressableKind(kind) { + return 3e4 <= kind && kind < 4e4; + } + function classifyKind(kind) { + if (isRegularKind(kind)) + return "regular"; + if (isReplaceableKind(kind)) + return "replaceable"; + if (isEphemeralKind(kind)) + return "ephemeral"; + if (isAddressableKind(kind)) + return "parameterized"; + return "unknown"; + } + function isKind(event, kind) { + const kindAsArray = kind instanceof Array ? kind : [kind]; + return validateEvent(event) && kindAsArray.includes(event.kind) || false; + } + var Metadata = 0; + var ShortTextNote = 1; + var RecommendRelay = 2; + var Contacts = 3; + var EncryptedDirectMessage = 4; + var EventDeletion = 5; + var Repost = 6; + var Reaction = 7; + var BadgeAward = 8; + var Seal = 13; + var PrivateDirectMessage = 14; + var GenericRepost = 16; + var ChannelCreation = 40; + var ChannelMetadata = 41; + var ChannelMessage = 42; + var ChannelHideMessage = 43; + var ChannelMuteUser = 44; + var OpenTimestamps = 1040; + var GiftWrap = 1059; + var FileMetadata = 1063; + var LiveChatMessage = 1311; + var ProblemTracker = 1971; + var Report = 1984; + var Reporting = 1984; + var Label = 1985; + var CommunityPostApproval = 4550; + var JobRequest = 5999; + var JobResult = 6999; + var JobFeedback = 7e3; + var ZapGoal = 9041; + var ZapRequest = 9734; + var Zap = 9735; + var Highlights = 9802; + var Mutelist = 1e4; + var Pinlist = 10001; + var RelayList = 10002; + var BookmarkList = 10003; + var CommunitiesList = 10004; + var PublicChatsList = 10005; + var BlockedRelaysList = 10006; + var SearchRelaysList = 10007; + var InterestsList = 10015; + var UserEmojiList = 10030; + var DirectMessageRelaysList = 10050; + var FileServerPreference = 10096; + var NWCWalletInfo = 13194; + var LightningPubRPC = 21e3; + var ClientAuth = 22242; + var NWCWalletRequest = 23194; + var NWCWalletResponse = 23195; + var NostrConnect = 24133; + var HTTPAuth = 27235; + var Followsets = 3e4; + var Genericlists = 30001; + var Relaysets = 30002; + var Bookmarksets = 30003; + var Curationsets = 30004; + var ProfileBadges = 30008; + var BadgeDefinition = 30009; + var Interestsets = 30015; + var CreateOrUpdateStall = 30017; + var CreateOrUpdateProduct = 30018; + var LongFormArticle = 30023; + var DraftLong = 30024; + var Emojisets = 30030; + var Application = 30078; + var LiveEvent = 30311; + var UserStatuses = 30315; + var ClassifiedListing = 30402; + var DraftClassifiedListing = 30403; + var Date2 = 31922; + var Time = 31923; + var Calendar = 31924; + var CalendarEventRSVP = 31925; + var Handlerrecommendation = 31989; + var Handlerinformation = 31990; + var CommunityDefinition = 34550; + + // filter.ts + function matchFilter(filter, event) { + if (filter.ids && filter.ids.indexOf(event.id) === -1) { + return false; + } + if (filter.kinds && filter.kinds.indexOf(event.kind) === -1) { + return false; + } + if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) { + return false; + } + for (let f2 in filter) { + if (f2[0] === "#") { + let tagName = f2.slice(1); + let values = filter[`#${tagName}`]; + if (values && !event.tags.find(([t, v]) => t === f2.slice(1) && values.indexOf(v) !== -1)) + return false; + } + } + if (filter.since && event.created_at < filter.since) + return false; + if (filter.until && event.created_at > filter.until) + return false; + return true; + } + function matchFilters(filters, event) { + for (let i2 = 0; i2 < filters.length; i2++) { + if (matchFilter(filters[i2], event)) { + return true; + } + } + return false; + } + function mergeFilters(...filters) { + let result = {}; + for (let i2 = 0; i2 < filters.length; i2++) { + let filter = filters[i2]; + Object.entries(filter).forEach(([property, values]) => { + if (property === "kinds" || property === "ids" || property === "authors" || property[0] === "#") { + result[property] = result[property] || []; + for (let v = 0; v < values.length; v++) { + let value = values[v]; + if (!result[property].includes(value)) + result[property].push(value); + } + } + }); + if (filter.limit && (!result.limit || filter.limit > result.limit)) + result.limit = filter.limit; + if (filter.until && (!result.until || filter.until > result.until)) + result.until = filter.until; + if (filter.since && (!result.since || filter.since < result.since)) + result.since = filter.since; + } + return result; + } + function getFilterLimit(filter) { + if (filter.ids && !filter.ids.length) + return 0; + if (filter.kinds && !filter.kinds.length) + return 0; + if (filter.authors && !filter.authors.length) + return 0; + for (const [key, value] of Object.entries(filter)) { + if (key[0] === "#" && Array.isArray(value) && !value.length) + return 0; + } + return Math.min( + Math.max(0, filter.limit ?? Infinity), + filter.ids?.length ?? Infinity, + filter.authors?.length && filter.kinds?.every((kind) => isReplaceableKind(kind)) ? filter.authors.length * filter.kinds.length : Infinity, + filter.authors?.length && filter.kinds?.every((kind) => isAddressableKind(kind)) && filter["#d"]?.length ? filter.authors.length * filter.kinds.length * filter["#d"].length : Infinity + ); + } + + // fakejson.ts + var fakejson_exports = {}; + __export(fakejson_exports, { + getHex64: () => getHex64, + getInt: () => getInt, + getSubscriptionId: () => getSubscriptionId, + matchEventId: () => matchEventId, + matchEventKind: () => matchEventKind, + matchEventPubkey: () => matchEventPubkey + }); + function getHex64(json, field) { + let len = field.length + 3; + let idx = json.indexOf(`"${field}":`) + len; + let s = json.slice(idx).indexOf(`"`) + idx + 1; + return json.slice(s, s + 64); + } + function getInt(json, field) { + let len = field.length; + let idx = json.indexOf(`"${field}":`) + len + 3; + let sliced = json.slice(idx); + let end = Math.min(sliced.indexOf(","), sliced.indexOf("}")); + return parseInt(sliced.slice(0, end), 10); + } + function getSubscriptionId(json) { + let idx = json.slice(0, 22).indexOf(`"EVENT"`); + if (idx === -1) + return null; + let pstart = json.slice(idx + 7 + 1).indexOf(`"`); + if (pstart === -1) + return null; + let start = idx + 7 + 1 + pstart; + let pend = json.slice(start + 1, 80).indexOf(`"`); + if (pend === -1) + return null; + let end = start + 1 + pend; + return json.slice(start + 1, end); + } + function matchEventId(json, id) { + return id === getHex64(json, "id"); + } + function matchEventPubkey(json, pubkey) { + return pubkey === getHex64(json, "pubkey"); + } + function matchEventKind(json, kind) { + return kind === getInt(json, "kind"); + } + + // nip42.ts + var nip42_exports = {}; + __export(nip42_exports, { + makeAuthEvent: () => makeAuthEvent + }); + function makeAuthEvent(relayURL, challenge2) { + return { + kind: ClientAuth, + created_at: Math.floor(Date.now() / 1e3), + tags: [ + ["relay", relayURL], + ["challenge", challenge2] + ], + content: "" + }; + } + + // helpers.ts + async function yieldThread() { + return new Promise((resolve) => { + const ch = new MessageChannel(); + const handler = () => { + ch.port1.removeEventListener("message", handler); + resolve(); + }; + ch.port1.addEventListener("message", handler); + ch.port2.postMessage(0); + ch.port1.start(); + }); + } + var alwaysTrue = (t) => { + t[verifiedSymbol] = true; + return true; + }; + + // abstract-relay.ts + var SendingOnClosedConnection = class extends Error { + constructor(message, relay) { + super(`Tried to send message '${message} on a closed connection to ${relay}.`); + this.name = "SendingOnClosedConnection"; + } + }; + var AbstractRelay = class { + url; + _connected = false; + onclose = null; + onnotice = (msg) => console.debug(`NOTICE from ${this.url}: ${msg}`); + baseEoseTimeout = 4400; + connectionTimeout = 4400; + publishTimeout = 4400; + pingFrequency = 2e4; + pingTimeout = 2e4; + openSubs = /* @__PURE__ */ new Map(); + enablePing; + connectionTimeoutHandle; + connectionPromise; + openCountRequests = /* @__PURE__ */ new Map(); + openEventPublishes = /* @__PURE__ */ new Map(); + ws; + incomingMessageQueue = new Queue(); + queueRunning = false; + challenge; + authPromise; + serial = 0; + verifyEvent; + _WebSocket; + constructor(url, opts) { + this.url = normalizeURL(url); + this.verifyEvent = opts.verifyEvent; + this._WebSocket = opts.websocketImplementation || WebSocket; + this.enablePing = opts.enablePing; + } + static async connect(url, opts) { + const relay = new AbstractRelay(url, opts); + await relay.connect(); + return relay; + } + closeAllSubscriptions(reason) { + for (let [_, sub] of this.openSubs) { + sub.close(reason); + } + this.openSubs.clear(); + for (let [_, ep] of this.openEventPublishes) { + ep.reject(new Error(reason)); + } + this.openEventPublishes.clear(); + for (let [_, cr] of this.openCountRequests) { + cr.reject(new Error(reason)); + } + this.openCountRequests.clear(); + } + get connected() { + return this._connected; + } + async connect() { + if (this.connectionPromise) + return this.connectionPromise; + this.challenge = void 0; + this.authPromise = void 0; + this.connectionPromise = new Promise((resolve, reject) => { + this.connectionTimeoutHandle = setTimeout(() => { + reject("connection timed out"); + this.connectionPromise = void 0; + this.onclose?.(); + this.closeAllSubscriptions("relay connection timed out"); + }, this.connectionTimeout); + try { + this.ws = new this._WebSocket(this.url); + } catch (err) { + clearTimeout(this.connectionTimeoutHandle); + reject(err); + return; + } + this.ws.onopen = () => { + clearTimeout(this.connectionTimeoutHandle); + this._connected = true; + if (this.enablePing) { + this.pingpong(); + } + resolve(); + }; + this.ws.onerror = (ev) => { + clearTimeout(this.connectionTimeoutHandle); + reject(ev.message || "websocket error"); + this._connected = false; + this.connectionPromise = void 0; + this.onclose?.(); + this.closeAllSubscriptions("relay connection errored"); + }; + this.ws.onclose = (ev) => { + clearTimeout(this.connectionTimeoutHandle); + reject(ev.message || "websocket closed"); + this._connected = false; + this.connectionPromise = void 0; + this.onclose?.(); + this.closeAllSubscriptions("relay connection closed"); + }; + this.ws.onmessage = this._onmessage.bind(this); + }); + return this.connectionPromise; + } + async waitForPingPong() { + return new Promise((res, err) => { + ; + this.ws && this.ws.on && this.ws.on("pong", () => res(true)) || err("ws can't listen for pong"); + this.ws && this.ws.ping && this.ws.ping(); + }); + } + async waitForDummyReq() { + return new Promise((resolve, _) => { + const sub = this.subscribe([{ ids: ["a".repeat(64)] }], { + oneose: () => { + sub.close(); + resolve(true); + }, + eoseTimeout: this.pingTimeout + 1e3 + }); + }); + } + async pingpong() { + if (this.ws?.readyState === 1) { + const result = await Promise.any([ + this.ws && this.ws.ping && this.ws.on ? this.waitForPingPong() : this.waitForDummyReq(), + new Promise((res) => setTimeout(() => res(false), this.pingTimeout)) + ]); + if (result) { + setTimeout(() => this.pingpong(), this.pingFrequency); + } else { + this.closeAllSubscriptions("pingpong timed out"); + this._connected = false; + this.onclose?.(); + this.ws?.close(); + } + } + } + async runQueue() { + this.queueRunning = true; + while (true) { + if (false === this.handleNext()) { + break; + } + await yieldThread(); + } + this.queueRunning = false; + } + handleNext() { + const json = this.incomingMessageQueue.dequeue(); + if (!json) { + return false; + } + const subid = getSubscriptionId(json); + if (subid) { + const so = this.openSubs.get(subid); + if (!so) { + return; + } + const id = getHex64(json, "id"); + const alreadyHave = so.alreadyHaveEvent?.(id); + so.receivedEvent?.(this, id); + if (alreadyHave) { + return; + } + } + try { + let data = JSON.parse(json); + switch (data[0]) { + case "EVENT": { + const so = this.openSubs.get(data[1]); + const event = data[2]; + if (this.verifyEvent(event) && matchFilters(so.filters, event)) { + so.onevent(event); + } + return; + } + case "COUNT": { + const id = data[1]; + const payload = data[2]; + const cr = this.openCountRequests.get(id); + if (cr) { + cr.resolve(payload.count); + this.openCountRequests.delete(id); + } + return; + } + case "EOSE": { + const so = this.openSubs.get(data[1]); + if (!so) + return; + so.receivedEose(); + return; + } + case "OK": { + const id = data[1]; + const ok = data[2]; + const reason = data[3]; + const ep = this.openEventPublishes.get(id); + if (ep) { + clearTimeout(ep.timeout); + if (ok) + ep.resolve(reason); + else + ep.reject(new Error(reason)); + this.openEventPublishes.delete(id); + } + return; + } + case "CLOSED": { + const id = data[1]; + const so = this.openSubs.get(id); + if (!so) + return; + so.closed = true; + so.close(data[2]); + return; + } + case "NOTICE": + this.onnotice(data[1]); + return; + case "AUTH": { + this.challenge = data[1]; + return; + } + } + } catch (err) { + return; + } + } + async send(message) { + if (!this.connectionPromise) + throw new SendingOnClosedConnection(message, this.url); + this.connectionPromise.then(() => { + this.ws?.send(message); + }); + } + async auth(signAuthEvent) { + const challenge2 = this.challenge; + if (!challenge2) + throw new Error("can't perform auth, no challenge was received"); + if (this.authPromise) + return this.authPromise; + this.authPromise = new Promise(async (resolve, reject) => { + try { + let evt = await signAuthEvent(makeAuthEvent(this.url, challenge2)); + let timeout = setTimeout(() => { + let ep = this.openEventPublishes.get(evt.id); + if (ep) { + ep.reject(new Error("auth timed out")); + this.openEventPublishes.delete(evt.id); + } + }, this.publishTimeout); + this.openEventPublishes.set(evt.id, { resolve, reject, timeout }); + this.send('["AUTH",' + JSON.stringify(evt) + "]"); + } catch (err) { + console.warn("subscribe auth function failed:", err); + } + }); + return this.authPromise; + } + async publish(event) { + const ret = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const ep = this.openEventPublishes.get(event.id); + if (ep) { + ep.reject(new Error("publish timed out")); + this.openEventPublishes.delete(event.id); + } + }, this.publishTimeout); + this.openEventPublishes.set(event.id, { resolve, reject, timeout }); + }); + this.send('["EVENT",' + JSON.stringify(event) + "]"); + return ret; + } + async count(filters, params) { + this.serial++; + const id = params?.id || "count:" + this.serial; + const ret = new Promise((resolve, reject) => { + this.openCountRequests.set(id, { resolve, reject }); + }); + this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1)); + return ret; + } + subscribe(filters, params) { + const subscription = this.prepareSubscription(filters, params); + subscription.fire(); + return subscription; + } + prepareSubscription(filters, params) { + this.serial++; + const id = params.id || (params.label ? params.label + ":" : "sub:") + this.serial; + const subscription = new Subscription(this, id, filters, params); + this.openSubs.set(id, subscription); + return subscription; + } + close() { + this.closeAllSubscriptions("relay connection closed by us"); + this._connected = false; + this.onclose?.(); + this.ws?.close(); + } + _onmessage(ev) { + this.incomingMessageQueue.enqueue(ev.data); + if (!this.queueRunning) { + this.runQueue(); + } + } + }; + var Subscription = class { + relay; + id; + closed = false; + eosed = false; + filters; + alreadyHaveEvent; + receivedEvent; + onevent; + oneose; + onclose; + eoseTimeout; + eoseTimeoutHandle; + constructor(relay, id, filters, params) { + this.relay = relay; + this.filters = filters; + this.id = id; + this.alreadyHaveEvent = params.alreadyHaveEvent; + this.receivedEvent = params.receivedEvent; + this.eoseTimeout = params.eoseTimeout || relay.baseEoseTimeout; + this.oneose = params.oneose; + this.onclose = params.onclose; + this.onevent = params.onevent || ((event) => { + console.warn( + `onevent() callback not defined for subscription '${this.id}' in relay ${this.relay.url}. event received:`, + event + ); + }); + } + fire() { + this.relay.send('["REQ","' + this.id + '",' + JSON.stringify(this.filters).substring(1)); + this.eoseTimeoutHandle = setTimeout(this.receivedEose.bind(this), this.eoseTimeout); + } + receivedEose() { + if (this.eosed) + return; + clearTimeout(this.eoseTimeoutHandle); + this.eosed = true; + this.oneose?.(); + } + close(reason = "closed by caller") { + if (!this.closed && this.relay.connected) { + try { + this.relay.send('["CLOSE",' + JSON.stringify(this.id) + "]"); + } catch (err) { + if (err instanceof SendingOnClosedConnection) { + } else { + throw err; + } + } + this.closed = true; + } + this.relay.openSubs.delete(this.id); + this.onclose?.(reason); + } + }; + + // relay.ts + var _WebSocket; + try { + _WebSocket = WebSocket; + } catch { + } + var Relay = class extends AbstractRelay { + constructor(url) { + super(url, { verifyEvent, websocketImplementation: _WebSocket }); + } + static async connect(url) { + const relay = new Relay(url); + await relay.connect(); + return relay; + } + }; + + // abstract-pool.ts + var AbstractSimplePool = class { + relays = /* @__PURE__ */ new Map(); + seenOn = /* @__PURE__ */ new Map(); + trackRelays = false; + verifyEvent; + enablePing; + trustedRelayURLs = /* @__PURE__ */ new Set(); + _WebSocket; + constructor(opts) { + this.verifyEvent = opts.verifyEvent; + this._WebSocket = opts.websocketImplementation; + this.enablePing = opts.enablePing; + } + async ensureRelay(url, params) { + url = normalizeURL(url); + let relay = this.relays.get(url); + if (!relay) { + relay = new AbstractRelay(url, { + verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent, + websocketImplementation: this._WebSocket, + enablePing: this.enablePing + }); + relay.onclose = () => { + this.relays.delete(url); + }; + if (params?.connectionTimeout) + relay.connectionTimeout = params.connectionTimeout; + this.relays.set(url, relay); + } + await relay.connect(); + return relay; + } + close(relays) { + relays.map(normalizeURL).forEach((url) => { + this.relays.get(url)?.close(); + this.relays.delete(url); + }); + } + subscribe(relays, filter, params) { + params.onauth = params.onauth || params.doauth; + const request = []; + for (let i2 = 0; i2 < relays.length; i2++) { + const url = normalizeURL(relays[i2]); + if (!request.find((r) => r.url === url)) { + request.push({ url, filter }); + } + } + return this.subscribeMap(request, params); + } + subscribeMany(relays, filters, params) { + params.onauth = params.onauth || params.doauth; + const request = []; + const uniqUrls = []; + for (let i2 = 0; i2 < relays.length; i2++) { + const url = normalizeURL(relays[i2]); + if (uniqUrls.indexOf(url) === -1) { + for (let f2 = 0; f2 < filters.length; f2++) { + request.push({ url, filter: filters[f2] }); + } + } + } + return this.subscribeMap(request, params); + } + subscribeMap(requests, params) { + params.onauth = params.onauth || params.doauth; + if (this.trackRelays) { + params.receivedEvent = (relay, id) => { + let set = this.seenOn.get(id); + if (!set) { + set = /* @__PURE__ */ new Set(); + this.seenOn.set(id, set); + } + set.add(relay); + }; + } + const _knownIds = /* @__PURE__ */ new Set(); + const subs = []; + const eosesReceived = []; + let handleEose = (i2) => { + if (eosesReceived[i2]) + return; + eosesReceived[i2] = true; + if (eosesReceived.filter((a) => a).length === requests.length) { + params.oneose?.(); + handleEose = () => { + }; + } + }; + const closesReceived = []; + let handleClose = (i2, reason) => { + if (closesReceived[i2]) + return; + handleEose(i2); + closesReceived[i2] = reason; + if (closesReceived.filter((a) => a).length === requests.length) { + params.onclose?.(closesReceived); + handleClose = () => { + }; + } + }; + const localAlreadyHaveEventHandler = (id) => { + if (params.alreadyHaveEvent?.(id)) { + return true; + } + const have = _knownIds.has(id); + _knownIds.add(id); + return have; + }; + const allOpened = Promise.all( + requests.map(async ({ url, filter }, i2) => { + let relay; + try { + relay = await this.ensureRelay(url, { + connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1e3) : void 0 + }); + } catch (err) { + handleClose(i2, err?.message || String(err)); + return; + } + let subscription = relay.subscribe([filter], { + ...params, + oneose: () => handleEose(i2), + onclose: (reason) => { + if (reason.startsWith("auth-required: ") && params.onauth) { + relay.auth(params.onauth).then(() => { + relay.subscribe([filter], { + ...params, + oneose: () => handleEose(i2), + onclose: (reason2) => { + handleClose(i2, reason2); + }, + alreadyHaveEvent: localAlreadyHaveEventHandler, + eoseTimeout: params.maxWait + }); + }).catch((err) => { + handleClose(i2, `auth was required and attempted, but failed with: ${err}`); + }); + } else { + handleClose(i2, reason); + } + }, + alreadyHaveEvent: localAlreadyHaveEventHandler, + eoseTimeout: params.maxWait + }); + subs.push(subscription); + }) + ); + return { + async close(reason) { + await allOpened; + subs.forEach((sub) => { + sub.close(reason); + }); + } + }; + } + subscribeEose(relays, filter, params) { + params.onauth = params.onauth || params.doauth; + const subcloser = this.subscribe(relays, filter, { + ...params, + oneose() { + subcloser.close("closed automatically on eose"); + } + }); + return subcloser; + } + subscribeManyEose(relays, filters, params) { + params.onauth = params.onauth || params.doauth; + const subcloser = this.subscribeMany(relays, filters, { + ...params, + oneose() { + subcloser.close("closed automatically on eose"); + } + }); + return subcloser; + } + async querySync(relays, filter, params) { + return new Promise(async (resolve) => { + const events = []; + this.subscribeEose(relays, filter, { + ...params, + onevent(event) { + events.push(event); + }, + onclose(_) { + resolve(events); + } + }); + }); + } + async get(relays, filter, params) { + filter.limit = 1; + const events = await this.querySync(relays, filter, params); + events.sort((a, b) => b.created_at - a.created_at); + return events[0] || null; + } + publish(relays, event, options) { + return relays.map(normalizeURL).map(async (url, i2, arr) => { + if (arr.indexOf(url) !== i2) { + return Promise.reject("duplicate url"); + } + let r = await this.ensureRelay(url); + return r.publish(event).catch(async (err) => { + if (err instanceof Error && err.message.startsWith("auth-required: ") && options?.onauth) { + await r.auth(options.onauth); + return r.publish(event); + } + throw err; + }).then((reason) => { + if (this.trackRelays) { + let set = this.seenOn.get(event.id); + if (!set) { + set = /* @__PURE__ */ new Set(); + this.seenOn.set(event.id, set); + } + set.add(r); + } + return reason; + }); + }); + } + listConnectionStatus() { + const map = /* @__PURE__ */ new Map(); + this.relays.forEach((relay, url) => map.set(url, relay.connected)); + return map; + } + destroy() { + this.relays.forEach((conn) => conn.close()); + this.relays = /* @__PURE__ */ new Map(); + } + }; + + // pool.ts + var _WebSocket2; + try { + _WebSocket2 = WebSocket; + } catch { + } + var SimplePool = class extends AbstractSimplePool { + constructor(options) { + super({ verifyEvent, websocketImplementation: _WebSocket2, ...options }); + } + }; + + // nip19.ts + var nip19_exports = {}; + __export(nip19_exports, { + BECH32_REGEX: () => BECH32_REGEX, + Bech32MaxSize: () => Bech32MaxSize, + NostrTypeGuard: () => NostrTypeGuard, + decode: () => decode, + decodeNostrURI: () => decodeNostrURI, + encodeBytes: () => encodeBytes, + naddrEncode: () => naddrEncode, + neventEncode: () => neventEncode, + noteEncode: () => noteEncode, + nprofileEncode: () => nprofileEncode, + npubEncode: () => npubEncode, + nsecEncode: () => nsecEncode + }); + + // node_modules/@scure/base/lib/esm/index.js + function assertNumber(n) { + if (!Number.isSafeInteger(n)) + throw new Error(`Wrong integer: ${n}`); + } + function chain(...args) { + const wrap = (a, b) => (c) => a(b(c)); + const encode = Array.from(args).reverse().reduce((acc, i2) => acc ? wrap(acc, i2.encode) : i2.encode, void 0); + const decode2 = args.reduce((acc, i2) => acc ? wrap(acc, i2.decode) : i2.decode, void 0); + return { encode, decode: decode2 }; + } + function alphabet(alphabet2) { + return { + encode: (digits) => { + if (!Array.isArray(digits) || digits.length && typeof digits[0] !== "number") + throw new Error("alphabet.encode input should be an array of numbers"); + return digits.map((i2) => { + assertNumber(i2); + if (i2 < 0 || i2 >= alphabet2.length) + throw new Error(`Digit index outside alphabet: ${i2} (alphabet: ${alphabet2.length})`); + return alphabet2[i2]; + }); + }, + decode: (input) => { + if (!Array.isArray(input) || input.length && typeof input[0] !== "string") + throw new Error("alphabet.decode input should be array of strings"); + return input.map((letter) => { + if (typeof letter !== "string") + throw new Error(`alphabet.decode: not string element=${letter}`); + const index = alphabet2.indexOf(letter); + if (index === -1) + throw new Error(`Unknown letter: "${letter}". Allowed: ${alphabet2}`); + return index; + }); + } + }; + } + function join(separator = "") { + if (typeof separator !== "string") + throw new Error("join separator should be string"); + return { + encode: (from) => { + if (!Array.isArray(from) || from.length && typeof from[0] !== "string") + throw new Error("join.encode input should be array of strings"); + for (let i2 of from) + if (typeof i2 !== "string") + throw new Error(`join.encode: non-string input=${i2}`); + return from.join(separator); + }, + decode: (to) => { + if (typeof to !== "string") + throw new Error("join.decode input should be string"); + return to.split(separator); + } + }; + } + function padding(bits, chr = "=") { + assertNumber(bits); + if (typeof chr !== "string") + throw new Error("padding chr should be string"); + return { + encode(data) { + if (!Array.isArray(data) || data.length && typeof data[0] !== "string") + throw new Error("padding.encode input should be array of strings"); + for (let i2 of data) + if (typeof i2 !== "string") + throw new Error(`padding.encode: non-string input=${i2}`); + while (data.length * bits % 8) + data.push(chr); + return data; + }, + decode(input) { + if (!Array.isArray(input) || input.length && typeof input[0] !== "string") + throw new Error("padding.encode input should be array of strings"); + for (let i2 of input) + if (typeof i2 !== "string") + throw new Error(`padding.decode: non-string input=${i2}`); + let end = input.length; + if (end * bits % 8) + throw new Error("Invalid padding: string should have whole number of bytes"); + for (; end > 0 && input[end - 1] === chr; end--) { + if (!((end - 1) * bits % 8)) + throw new Error("Invalid padding: string has too much padding"); + } + return input.slice(0, end); + } + }; + } + function normalize(fn) { + if (typeof fn !== "function") + throw new Error("normalize fn should be function"); + return { encode: (from) => from, decode: (to) => fn(to) }; + } + function convertRadix(data, from, to) { + if (from < 2) + throw new Error(`convertRadix: wrong from=${from}, base cannot be less than 2`); + if (to < 2) + throw new Error(`convertRadix: wrong to=${to}, base cannot be less than 2`); + if (!Array.isArray(data)) + throw new Error("convertRadix: data should be array"); + if (!data.length) + return []; + let pos = 0; + const res = []; + const digits = Array.from(data); + digits.forEach((d) => { + assertNumber(d); + if (d < 0 || d >= from) + throw new Error(`Wrong integer: ${d}`); + }); + while (true) { + let carry = 0; + let done = true; + for (let i2 = pos; i2 < digits.length; i2++) { + const digit = digits[i2]; + const digitBase = from * carry + digit; + if (!Number.isSafeInteger(digitBase) || from * carry / from !== carry || digitBase - digit !== from * carry) { + throw new Error("convertRadix: carry overflow"); + } + carry = digitBase % to; + digits[i2] = Math.floor(digitBase / to); + if (!Number.isSafeInteger(digits[i2]) || digits[i2] * to + carry !== digitBase) + throw new Error("convertRadix: carry overflow"); + if (!done) + continue; + else if (!digits[i2]) + pos = i2; + else + done = false; + } + res.push(carry); + if (done) + break; + } + for (let i2 = 0; i2 < data.length - 1 && data[i2] === 0; i2++) + res.push(0); + return res.reverse(); + } + var gcd = (a, b) => !b ? a : gcd(b, a % b); + var radix2carry = (from, to) => from + (to - gcd(from, to)); + function convertRadix2(data, from, to, padding2) { + if (!Array.isArray(data)) + throw new Error("convertRadix2: data should be array"); + if (from <= 0 || from > 32) + throw new Error(`convertRadix2: wrong from=${from}`); + if (to <= 0 || to > 32) + throw new Error(`convertRadix2: wrong to=${to}`); + if (radix2carry(from, to) > 32) { + throw new Error(`convertRadix2: carry overflow from=${from} to=${to} carryBits=${radix2carry(from, to)}`); + } + let carry = 0; + let pos = 0; + const mask = 2 ** to - 1; + const res = []; + for (const n of data) { + assertNumber(n); + if (n >= 2 ** from) + throw new Error(`convertRadix2: invalid data word=${n} from=${from}`); + carry = carry << from | n; + if (pos + from > 32) + throw new Error(`convertRadix2: carry overflow pos=${pos} from=${from}`); + pos += from; + for (; pos >= to; pos -= to) + res.push((carry >> pos - to & mask) >>> 0); + carry &= 2 ** pos - 1; + } + carry = carry << to - pos & mask; + if (!padding2 && pos >= from) + throw new Error("Excess padding"); + if (!padding2 && carry) + throw new Error(`Non-zero padding: ${carry}`); + if (padding2 && pos > 0) + res.push(carry >>> 0); + return res; + } + function radix(num) { + assertNumber(num); + return { + encode: (bytes4) => { + if (!(bytes4 instanceof Uint8Array)) + throw new Error("radix.encode input should be Uint8Array"); + return convertRadix(Array.from(bytes4), 2 ** 8, num); + }, + decode: (digits) => { + if (!Array.isArray(digits) || digits.length && typeof digits[0] !== "number") + throw new Error("radix.decode input should be array of strings"); + return Uint8Array.from(convertRadix(digits, num, 2 ** 8)); + } + }; + } + function radix2(bits, revPadding = false) { + assertNumber(bits); + if (bits <= 0 || bits > 32) + throw new Error("radix2: bits should be in (0..32]"); + if (radix2carry(8, bits) > 32 || radix2carry(bits, 8) > 32) + throw new Error("radix2: carry overflow"); + return { + encode: (bytes4) => { + if (!(bytes4 instanceof Uint8Array)) + throw new Error("radix2.encode input should be Uint8Array"); + return convertRadix2(Array.from(bytes4), 8, bits, !revPadding); + }, + decode: (digits) => { + if (!Array.isArray(digits) || digits.length && typeof digits[0] !== "number") + throw new Error("radix2.decode input should be array of strings"); + return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding)); + } + }; + } + function unsafeWrapper(fn) { + if (typeof fn !== "function") + throw new Error("unsafeWrapper fn should be function"); + return function(...args) { + try { + return fn.apply(null, args); + } catch (e) { + } + }; + } + function checksum(len, fn) { + assertNumber(len); + if (typeof fn !== "function") + throw new Error("checksum fn should be function"); + return { + encode(data) { + if (!(data instanceof Uint8Array)) + throw new Error("checksum.encode: input should be Uint8Array"); + const checksum2 = fn(data).slice(0, len); + const res = new Uint8Array(data.length + len); + res.set(data); + res.set(checksum2, data.length); + return res; + }, + decode(data) { + if (!(data instanceof Uint8Array)) + throw new Error("checksum.decode: input should be Uint8Array"); + const payload = data.slice(0, -len); + const newChecksum = fn(payload).slice(0, len); + const oldChecksum = data.slice(-len); + for (let i2 = 0; i2 < len; i2++) + if (newChecksum[i2] !== oldChecksum[i2]) + throw new Error("Invalid checksum"); + return payload; + } + }; + } + var utils = { alphabet, chain, checksum, radix, radix2, join, padding }; + var base16 = chain(radix2(4), alphabet("0123456789ABCDEF"), join("")); + var base32 = chain(radix2(5), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"), padding(5), join("")); + var base32hex = chain(radix2(5), alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUV"), padding(5), join("")); + var base32crockford = chain(radix2(5), alphabet("0123456789ABCDEFGHJKMNPQRSTVWXYZ"), join(""), normalize((s) => s.toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1"))); + var base64 = chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), padding(6), join("")); + var base64url = chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"), padding(6), join("")); + var genBase58 = (abc) => chain(radix(58), alphabet(abc), join("")); + var base58 = genBase58("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); + var base58flickr = genBase58("123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"); + var base58xrp = genBase58("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"); + var XMR_BLOCK_LEN = [0, 2, 3, 5, 6, 7, 9, 10, 11]; + var base58xmr = { + encode(data) { + let res = ""; + for (let i2 = 0; i2 < data.length; i2 += 8) { + const block = data.subarray(i2, i2 + 8); + res += base58.encode(block).padStart(XMR_BLOCK_LEN[block.length], "1"); + } + return res; + }, + decode(str) { + let res = []; + for (let i2 = 0; i2 < str.length; i2 += 11) { + const slice = str.slice(i2, i2 + 11); + const blockLen = XMR_BLOCK_LEN.indexOf(slice.length); + const block = base58.decode(slice); + for (let j = 0; j < block.length - blockLen; j++) { + if (block[j] !== 0) + throw new Error("base58xmr: wrong padding"); + } + res = res.concat(Array.from(block.slice(block.length - blockLen))); + } + return Uint8Array.from(res); + } + }; + var base58check = (sha2563) => chain(checksum(4, (data) => sha2563(sha2563(data))), base58); + var BECH_ALPHABET = chain(alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), join("")); + var POLYMOD_GENERATORS = [996825010, 642813549, 513874426, 1027748829, 705979059]; + function bech32Polymod(pre) { + const b = pre >> 25; + let chk = (pre & 33554431) << 5; + for (let i2 = 0; i2 < POLYMOD_GENERATORS.length; i2++) { + if ((b >> i2 & 1) === 1) + chk ^= POLYMOD_GENERATORS[i2]; + } + return chk; + } + function bechChecksum(prefix, words, encodingConst = 1) { + const len = prefix.length; + let chk = 1; + for (let i2 = 0; i2 < len; i2++) { + const c = prefix.charCodeAt(i2); + if (c < 33 || c > 126) + throw new Error(`Invalid prefix (${prefix})`); + chk = bech32Polymod(chk) ^ c >> 5; + } + chk = bech32Polymod(chk); + for (let i2 = 0; i2 < len; i2++) + chk = bech32Polymod(chk) ^ prefix.charCodeAt(i2) & 31; + for (let v of words) + chk = bech32Polymod(chk) ^ v; + for (let i2 = 0; i2 < 6; i2++) + chk = bech32Polymod(chk); + chk ^= encodingConst; + return BECH_ALPHABET.encode(convertRadix2([chk % 2 ** 30], 30, 5, false)); + } + function genBech32(encoding) { + const ENCODING_CONST = encoding === "bech32" ? 1 : 734539939; + const _words = radix2(5); + const fromWords = _words.decode; + const toWords = _words.encode; + const fromWordsUnsafe = unsafeWrapper(fromWords); + function encode(prefix, words, limit2 = 90) { + if (typeof prefix !== "string") + throw new Error(`bech32.encode prefix should be string, not ${typeof prefix}`); + if (!Array.isArray(words) || words.length && typeof words[0] !== "number") + throw new Error(`bech32.encode words should be array of numbers, not ${typeof words}`); + const actualLength = prefix.length + 7 + words.length; + if (limit2 !== false && actualLength > limit2) + throw new TypeError(`Length ${actualLength} exceeds limit ${limit2}`); + prefix = prefix.toLowerCase(); + return `${prefix}1${BECH_ALPHABET.encode(words)}${bechChecksum(prefix, words, ENCODING_CONST)}`; + } + function decode2(str, limit2 = 90) { + if (typeof str !== "string") + throw new Error(`bech32.decode input should be string, not ${typeof str}`); + if (str.length < 8 || limit2 !== false && str.length > limit2) + throw new TypeError(`Wrong string length: ${str.length} (${str}). Expected (8..${limit2})`); + const lowered = str.toLowerCase(); + if (str !== lowered && str !== str.toUpperCase()) + throw new Error(`String must be lowercase or uppercase`); + str = lowered; + const sepIndex = str.lastIndexOf("1"); + if (sepIndex === 0 || sepIndex === -1) + throw new Error(`Letter "1" must be present between prefix and data only`); + const prefix = str.slice(0, sepIndex); + const _words2 = str.slice(sepIndex + 1); + if (_words2.length < 6) + throw new Error("Data must be at least 6 characters long"); + const words = BECH_ALPHABET.decode(_words2).slice(0, -6); + const sum = bechChecksum(prefix, words, ENCODING_CONST); + if (!_words2.endsWith(sum)) + throw new Error(`Invalid checksum in ${str}: expected "${sum}"`); + return { prefix, words }; + } + const decodeUnsafe = unsafeWrapper(decode2); + function decodeToBytes(str) { + const { prefix, words } = decode2(str, false); + return { prefix, words, bytes: fromWords(words) }; + } + return { encode, decode: decode2, decodeToBytes, decodeUnsafe, fromWords, fromWordsUnsafe, toWords }; + } + var bech32 = genBech32("bech32"); + var bech32m = genBech32("bech32m"); + var utf8 = { + encode: (data) => new TextDecoder().decode(data), + decode: (str) => new TextEncoder().encode(str) + }; + var hex = chain(radix2(4), alphabet("0123456789abcdef"), join(""), normalize((s) => { + if (typeof s !== "string" || s.length % 2) + throw new TypeError(`hex.decode: expected string, got ${typeof s} with length ${s.length}`); + return s.toLowerCase(); + })); + var CODERS = { + utf8, + hex, + base16, + base32, + base64, + base64url, + base58, + base58xmr + }; + var coderTypeError = `Invalid encoding type. Available types: ${Object.keys(CODERS).join(", ")}`; + + // nip19.ts + var NostrTypeGuard = { + isNProfile: (value) => /^nprofile1[a-z\d]+$/.test(value || ""), + isNEvent: (value) => /^nevent1[a-z\d]+$/.test(value || ""), + isNAddr: (value) => /^naddr1[a-z\d]+$/.test(value || ""), + isNSec: (value) => /^nsec1[a-z\d]{58}$/.test(value || ""), + isNPub: (value) => /^npub1[a-z\d]{58}$/.test(value || ""), + isNote: (value) => /^note1[a-z\d]+$/.test(value || ""), + isNcryptsec: (value) => /^ncryptsec1[a-z\d]+$/.test(value || "") + }; + var Bech32MaxSize = 5e3; + var BECH32_REGEX = /[\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,}/; + function integerToUint8Array(number4) { + const uint8Array = new Uint8Array(4); + uint8Array[0] = number4 >> 24 & 255; + uint8Array[1] = number4 >> 16 & 255; + uint8Array[2] = number4 >> 8 & 255; + uint8Array[3] = number4 & 255; + return uint8Array; + } + function decodeNostrURI(nip19code) { + try { + if (nip19code.startsWith("nostr:")) + nip19code = nip19code.substring(6); + return decode(nip19code); + } catch (_err) { + return { type: "invalid", data: null }; + } + } + function decode(code) { + let { prefix, words } = bech32.decode(code, Bech32MaxSize); + let data = new Uint8Array(bech32.fromWords(words)); + switch (prefix) { + case "nprofile": { + let tlv = parseTLV(data); + if (!tlv[0]?.[0]) + throw new Error("missing TLV 0 for nprofile"); + if (tlv[0][0].length !== 32) + throw new Error("TLV 0 should be 32 bytes"); + return { + type: "nprofile", + data: { + pubkey: bytesToHex2(tlv[0][0]), + relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [] + } + }; + } + case "nevent": { + let tlv = parseTLV(data); + if (!tlv[0]?.[0]) + throw new Error("missing TLV 0 for nevent"); + if (tlv[0][0].length !== 32) + throw new Error("TLV 0 should be 32 bytes"); + if (tlv[2] && tlv[2][0].length !== 32) + throw new Error("TLV 2 should be 32 bytes"); + if (tlv[3] && tlv[3][0].length !== 4) + throw new Error("TLV 3 should be 4 bytes"); + return { + type: "nevent", + data: { + id: bytesToHex2(tlv[0][0]), + relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [], + author: tlv[2]?.[0] ? bytesToHex2(tlv[2][0]) : void 0, + kind: tlv[3]?.[0] ? parseInt(bytesToHex2(tlv[3][0]), 16) : void 0 + } + }; + } + case "naddr": { + let tlv = parseTLV(data); + if (!tlv[0]?.[0]) + throw new Error("missing TLV 0 for naddr"); + if (!tlv[2]?.[0]) + throw new Error("missing TLV 2 for naddr"); + if (tlv[2][0].length !== 32) + throw new Error("TLV 2 should be 32 bytes"); + if (!tlv[3]?.[0]) + throw new Error("missing TLV 3 for naddr"); + if (tlv[3][0].length !== 4) + throw new Error("TLV 3 should be 4 bytes"); + return { + type: "naddr", + data: { + identifier: utf8Decoder.decode(tlv[0][0]), + pubkey: bytesToHex2(tlv[2][0]), + kind: parseInt(bytesToHex2(tlv[3][0]), 16), + relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [] + } + }; + } + case "nsec": + return { type: prefix, data }; + case "npub": + case "note": + return { type: prefix, data: bytesToHex2(data) }; + default: + throw new Error(`unknown prefix ${prefix}`); + } + } + function parseTLV(data) { + let result = {}; + let rest = data; + while (rest.length > 0) { + let t = rest[0]; + let l = rest[1]; + let v = rest.slice(2, 2 + l); + rest = rest.slice(2 + l); + if (v.length < l) + throw new Error(`not enough data to read on TLV ${t}`); + result[t] = result[t] || []; + result[t].push(v); + } + return result; + } + function nsecEncode(key) { + return encodeBytes("nsec", key); + } + function npubEncode(hex2) { + return encodeBytes("npub", hexToBytes2(hex2)); + } + function noteEncode(hex2) { + return encodeBytes("note", hexToBytes2(hex2)); + } + function encodeBech32(prefix, data) { + let words = bech32.toWords(data); + return bech32.encode(prefix, words, Bech32MaxSize); + } + function encodeBytes(prefix, bytes4) { + return encodeBech32(prefix, bytes4); + } + function nprofileEncode(profile) { + let data = encodeTLV({ + 0: [hexToBytes2(profile.pubkey)], + 1: (profile.relays || []).map((url) => utf8Encoder.encode(url)) + }); + return encodeBech32("nprofile", data); + } + function neventEncode(event) { + let kindArray; + if (event.kind !== void 0) { + kindArray = integerToUint8Array(event.kind); + } + let data = encodeTLV({ + 0: [hexToBytes2(event.id)], + 1: (event.relays || []).map((url) => utf8Encoder.encode(url)), + 2: event.author ? [hexToBytes2(event.author)] : [], + 3: kindArray ? [new Uint8Array(kindArray)] : [] + }); + return encodeBech32("nevent", data); + } + function naddrEncode(addr) { + let kind = new ArrayBuffer(4); + new DataView(kind).setUint32(0, addr.kind, false); + let data = encodeTLV({ + 0: [utf8Encoder.encode(addr.identifier)], + 1: (addr.relays || []).map((url) => utf8Encoder.encode(url)), + 2: [hexToBytes2(addr.pubkey)], + 3: [new Uint8Array(kind)] + }); + return encodeBech32("naddr", data); + } + function encodeTLV(tlv) { + let entries = []; + Object.entries(tlv).reverse().forEach(([t, vs]) => { + vs.forEach((v) => { + let entry = new Uint8Array(v.length + 2); + entry.set([parseInt(t)], 0); + entry.set([v.length], 1); + entry.set(v, 2); + entries.push(entry); + }); + }); + return concatBytes3(...entries); + } + + // references.ts + var mentionRegex = /\bnostr:((note|npub|naddr|nevent|nprofile)1\w+)\b|#\[(\d+)\]/g; + function parseReferences(evt) { + let references = []; + for (let ref of evt.content.matchAll(mentionRegex)) { + if (ref[2]) { + try { + let { type, data } = decode(ref[1]); + switch (type) { + case "npub": { + references.push({ + text: ref[0], + profile: { pubkey: data, relays: [] } + }); + break; + } + case "nprofile": { + references.push({ + text: ref[0], + profile: data + }); + break; + } + case "note": { + references.push({ + text: ref[0], + event: { id: data, relays: [] } + }); + break; + } + case "nevent": { + references.push({ + text: ref[0], + event: data + }); + break; + } + case "naddr": { + references.push({ + text: ref[0], + address: data + }); + break; + } + } + } catch (err) { + } + } else if (ref[3]) { + let idx = parseInt(ref[3], 10); + let tag = evt.tags[idx]; + if (!tag) + continue; + switch (tag[0]) { + case "p": { + references.push({ + text: ref[0], + profile: { pubkey: tag[1], relays: tag[2] ? [tag[2]] : [] } + }); + break; + } + case "e": { + references.push({ + text: ref[0], + event: { id: tag[1], relays: tag[2] ? [tag[2]] : [] } + }); + break; + } + case "a": { + try { + let [kind, pubkey, identifier] = tag[1].split(":"); + references.push({ + text: ref[0], + address: { + identifier, + pubkey, + kind: parseInt(kind, 10), + relays: tag[2] ? [tag[2]] : [] + } + }); + } catch (err) { + } + break; + } + } + } + } + return references; + } + + // nip04.ts + var nip04_exports = {}; + __export(nip04_exports, { + decrypt: () => decrypt2, + encrypt: () => encrypt2 + }); + + // node_modules/@noble/ciphers/esm/_assert.js + function number3(n) { + if (!Number.isSafeInteger(n) || n < 0) + throw new Error(`positive integer expected, not ${n}`); + } + function bool2(b) { + if (typeof b !== "boolean") + throw new Error(`boolean expected, not ${b}`); + } + function isBytes(a) { + return a instanceof Uint8Array || a != null && typeof a === "object" && a.constructor.name === "Uint8Array"; + } + function bytes3(b, ...lengths) { + if (!isBytes(b)) + throw new Error("Uint8Array expected"); + if (lengths.length > 0 && !lengths.includes(b.length)) + throw new Error(`Uint8Array expected of length ${lengths}, not of length=${b.length}`); + } + function exists3(instance, checkFinished = true) { + if (instance.destroyed) + throw new Error("Hash instance has been destroyed"); + if (checkFinished && instance.finished) + throw new Error("Hash#digest() has already been called"); + } + function output3(out, instance) { + bytes3(out); + const min = instance.outputLen; + if (out.length < min) { + throw new Error(`digestInto() expects output buffer of length at least ${min}`); + } + } + + // node_modules/@noble/ciphers/esm/utils.js + var u8 = (arr) => new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); + var u32 = (arr) => new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); + var createView3 = (arr) => new DataView(arr.buffer, arr.byteOffset, arr.byteLength); + var isLE3 = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68; + if (!isLE3) + throw new Error("Non little-endian hardware is not supported"); + function utf8ToBytes4(str) { + if (typeof str !== "string") + throw new Error(`string expected, got ${typeof str}`); + return new Uint8Array(new TextEncoder().encode(str)); + } + function toBytes3(data) { + if (typeof data === "string") + data = utf8ToBytes4(data); + else if (isBytes(data)) + data = data.slice(); + else + throw new Error(`Uint8Array expected, got ${typeof data}`); + return data; + } + function checkOpts2(defaults, opts) { + if (opts == null || typeof opts !== "object") + throw new Error("options must be defined"); + const merged = Object.assign(defaults, opts); + return merged; + } + function equalBytes2(a, b) { + if (a.length !== b.length) + return false; + let diff = 0; + for (let i2 = 0; i2 < a.length; i2++) + diff |= a[i2] ^ b[i2]; + return diff === 0; + } + var wrapCipher = (params, c) => { + Object.assign(c, params); + return c; + }; + function setBigUint643(view, byteOffset, value, isLE4) { + if (typeof view.setBigUint64 === "function") + return view.setBigUint64(byteOffset, value, isLE4); + const _32n2 = BigInt(32); + const _u32_max = BigInt(4294967295); + const wh = Number(value >> _32n2 & _u32_max); + const wl = Number(value & _u32_max); + const h = isLE4 ? 4 : 0; + const l = isLE4 ? 0 : 4; + view.setUint32(byteOffset + h, wh, isLE4); + view.setUint32(byteOffset + l, wl, isLE4); + } + + // node_modules/@noble/ciphers/esm/_polyval.js + var BLOCK_SIZE = 16; + var ZEROS16 = /* @__PURE__ */ new Uint8Array(16); + var ZEROS32 = u32(ZEROS16); + var POLY = 225; + var mul2 = (s0, s1, s2, s3) => { + const hiBit = s3 & 1; + return { + s3: s2 << 31 | s3 >>> 1, + s2: s1 << 31 | s2 >>> 1, + s1: s0 << 31 | s1 >>> 1, + s0: s0 >>> 1 ^ POLY << 24 & -(hiBit & 1) + }; + }; + var swapLE = (n) => (n >>> 0 & 255) << 24 | (n >>> 8 & 255) << 16 | (n >>> 16 & 255) << 8 | n >>> 24 & 255 | 0; + function _toGHASHKey(k) { + k.reverse(); + const hiBit = k[15] & 1; + let carry = 0; + for (let i2 = 0; i2 < k.length; i2++) { + const t = k[i2]; + k[i2] = t >>> 1 | carry; + carry = (t & 1) << 7; + } + k[0] ^= -hiBit & 225; + return k; + } + var estimateWindow = (bytes4) => { + if (bytes4 > 64 * 1024) + return 8; + if (bytes4 > 1024) + return 4; + return 2; + }; + var GHASH = class { + constructor(key, expectedLength) { + this.blockLen = BLOCK_SIZE; + this.outputLen = BLOCK_SIZE; + this.s0 = 0; + this.s1 = 0; + this.s2 = 0; + this.s3 = 0; + this.finished = false; + key = toBytes3(key); + bytes3(key, 16); + const kView = createView3(key); + let k0 = kView.getUint32(0, false); + let k1 = kView.getUint32(4, false); + let k2 = kView.getUint32(8, false); + let k3 = kView.getUint32(12, false); + const doubles = []; + for (let i2 = 0; i2 < 128; i2++) { + doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) }); + ({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3)); + } + const W = estimateWindow(expectedLength || 1024); + if (![1, 2, 4, 8].includes(W)) + throw new Error(`ghash: wrong window size=${W}, should be 2, 4 or 8`); + this.W = W; + const bits = 128; + const windows = bits / W; + const windowSize = this.windowSize = 2 ** W; + const items = []; + for (let w = 0; w < windows; w++) { + for (let byte = 0; byte < windowSize; byte++) { + let s0 = 0, s1 = 0, s2 = 0, s3 = 0; + for (let j = 0; j < W; j++) { + const bit = byte >>> W - j - 1 & 1; + if (!bit) + continue; + const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j]; + s0 ^= d0, s1 ^= d1, s2 ^= d2, s3 ^= d3; + } + items.push({ s0, s1, s2, s3 }); + } + } + this.t = items; + } + _updateBlock(s0, s1, s2, s3) { + s0 ^= this.s0, s1 ^= this.s1, s2 ^= this.s2, s3 ^= this.s3; + const { W, t, windowSize } = this; + let o0 = 0, o1 = 0, o2 = 0, o3 = 0; + const mask = (1 << W) - 1; + let w = 0; + for (const num of [s0, s1, s2, s3]) { + for (let bytePos = 0; bytePos < 4; bytePos++) { + const byte = num >>> 8 * bytePos & 255; + for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) { + const bit = byte >>> W * bitPos & mask; + const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit]; + o0 ^= e0, o1 ^= e1, o2 ^= e2, o3 ^= e3; + w += 1; + } + } + } + this.s0 = o0; + this.s1 = o1; + this.s2 = o2; + this.s3 = o3; + } + update(data) { + data = toBytes3(data); + exists3(this); + const b32 = u32(data); + const blocks = Math.floor(data.length / BLOCK_SIZE); + const left = data.length % BLOCK_SIZE; + for (let i2 = 0; i2 < blocks; i2++) { + this._updateBlock(b32[i2 * 4 + 0], b32[i2 * 4 + 1], b32[i2 * 4 + 2], b32[i2 * 4 + 3]); + } + if (left) { + ZEROS16.set(data.subarray(blocks * BLOCK_SIZE)); + this._updateBlock(ZEROS32[0], ZEROS32[1], ZEROS32[2], ZEROS32[3]); + ZEROS32.fill(0); + } + return this; + } + destroy() { + const { t } = this; + for (const elm of t) { + elm.s0 = 0, elm.s1 = 0, elm.s2 = 0, elm.s3 = 0; + } + } + digestInto(out) { + exists3(this); + output3(out, this); + this.finished = true; + const { s0, s1, s2, s3 } = this; + const o32 = u32(out); + o32[0] = s0; + o32[1] = s1; + o32[2] = s2; + o32[3] = s3; + return out; + } + digest() { + const res = new Uint8Array(BLOCK_SIZE); + this.digestInto(res); + this.destroy(); + return res; + } + }; + var Polyval = class extends GHASH { + constructor(key, expectedLength) { + key = toBytes3(key); + const ghKey = _toGHASHKey(key.slice()); + super(ghKey, expectedLength); + ghKey.fill(0); + } + update(data) { + data = toBytes3(data); + exists3(this); + const b32 = u32(data); + const left = data.length % BLOCK_SIZE; + const blocks = Math.floor(data.length / BLOCK_SIZE); + for (let i2 = 0; i2 < blocks; i2++) { + this._updateBlock(swapLE(b32[i2 * 4 + 3]), swapLE(b32[i2 * 4 + 2]), swapLE(b32[i2 * 4 + 1]), swapLE(b32[i2 * 4 + 0])); + } + if (left) { + ZEROS16.set(data.subarray(blocks * BLOCK_SIZE)); + this._updateBlock(swapLE(ZEROS32[3]), swapLE(ZEROS32[2]), swapLE(ZEROS32[1]), swapLE(ZEROS32[0])); + ZEROS32.fill(0); + } + return this; + } + digestInto(out) { + exists3(this); + output3(out, this); + this.finished = true; + const { s0, s1, s2, s3 } = this; + const o32 = u32(out); + o32[0] = s0; + o32[1] = s1; + o32[2] = s2; + o32[3] = s3; + return out.reverse(); + } + }; + function wrapConstructorWithKey(hashCons) { + const hashC = (msg, key) => hashCons(key, msg.length).update(toBytes3(msg)).digest(); + const tmp = hashCons(new Uint8Array(16), 0); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.create = (key, expectedLength) => hashCons(key, expectedLength); + return hashC; + } + var ghash = wrapConstructorWithKey((key, expectedLength) => new GHASH(key, expectedLength)); + var polyval = wrapConstructorWithKey((key, expectedLength) => new Polyval(key, expectedLength)); + + // node_modules/@noble/ciphers/esm/aes.js + var BLOCK_SIZE2 = 16; + var BLOCK_SIZE32 = 4; + var EMPTY_BLOCK = new Uint8Array(BLOCK_SIZE2); + var POLY2 = 283; + function mul22(n) { + return n << 1 ^ POLY2 & -(n >> 7); + } + function mul(a, b) { + let res = 0; + for (; b > 0; b >>= 1) { + res ^= a & -(b & 1); + a = mul22(a); + } + return res; + } + var sbox = /* @__PURE__ */ (() => { + let t = new Uint8Array(256); + for (let i2 = 0, x = 1; i2 < 256; i2++, x ^= mul22(x)) + t[i2] = x; + const box = new Uint8Array(256); + box[0] = 99; + for (let i2 = 0; i2 < 255; i2++) { + let x = t[255 - i2]; + x |= x << 8; + box[t[i2]] = (x ^ x >> 4 ^ x >> 5 ^ x >> 6 ^ x >> 7 ^ 99) & 255; + } + return box; + })(); + var invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j)); + var rotr32_8 = (n) => n << 24 | n >>> 8; + var rotl32_8 = (n) => n << 8 | n >>> 24; + function genTtable(sbox2, fn) { + if (sbox2.length !== 256) + throw new Error("Wrong sbox length"); + const T0 = new Uint32Array(256).map((_, j) => fn(sbox2[j])); + const T1 = T0.map(rotl32_8); + const T2 = T1.map(rotl32_8); + const T3 = T2.map(rotl32_8); + const T01 = new Uint32Array(256 * 256); + const T23 = new Uint32Array(256 * 256); + const sbox22 = new Uint16Array(256 * 256); + for (let i2 = 0; i2 < 256; i2++) { + for (let j = 0; j < 256; j++) { + const idx = i2 * 256 + j; + T01[idx] = T0[i2] ^ T1[j]; + T23[idx] = T2[i2] ^ T3[j]; + sbox22[idx] = sbox2[i2] << 8 | sbox2[j]; + } + } + return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 }; + } + var tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => mul(s, 3) << 24 | s << 16 | s << 8 | mul(s, 2)); + var tableDecoding = /* @__PURE__ */ genTtable(invSbox, (s) => mul(s, 11) << 24 | mul(s, 13) << 16 | mul(s, 9) << 8 | mul(s, 14)); + var xPowers = /* @__PURE__ */ (() => { + const p = new Uint8Array(16); + for (let i2 = 0, x = 1; i2 < 16; i2++, x = mul22(x)) + p[i2] = x; + return p; + })(); + function expandKeyLE(key) { + bytes3(key); + const len = key.length; + if (![16, 24, 32].includes(len)) + throw new Error(`aes: wrong key size: should be 16, 24 or 32, got: ${len}`); + const { sbox2 } = tableEncoding; + const k32 = u32(key); + const Nk = k32.length; + const subByte = (n) => applySbox(sbox2, n, n, n, n); + const xk = new Uint32Array(len + 28); + xk.set(k32); + for (let i2 = Nk; i2 < xk.length; i2++) { + let t = xk[i2 - 1]; + if (i2 % Nk === 0) + t = subByte(rotr32_8(t)) ^ xPowers[i2 / Nk - 1]; + else if (Nk > 6 && i2 % Nk === 4) + t = subByte(t); + xk[i2] = xk[i2 - Nk] ^ t; + } + return xk; + } + function expandKeyDecLE(key) { + const encKey = expandKeyLE(key); + const xk = encKey.slice(); + const Nk = encKey.length; + const { sbox2 } = tableEncoding; + const { T0, T1, T2, T3 } = tableDecoding; + for (let i2 = 0; i2 < Nk; i2 += 4) { + for (let j = 0; j < 4; j++) + xk[i2 + j] = encKey[Nk - i2 - 4 + j]; + } + encKey.fill(0); + for (let i2 = 4; i2 < Nk - 4; i2++) { + const x = xk[i2]; + const w = applySbox(sbox2, x, x, x, x); + xk[i2] = T0[w & 255] ^ T1[w >>> 8 & 255] ^ T2[w >>> 16 & 255] ^ T3[w >>> 24]; + } + return xk; + } + function apply0123(T01, T23, s0, s1, s2, s3) { + return T01[s0 << 8 & 65280 | s1 >>> 8 & 255] ^ T23[s2 >>> 8 & 65280 | s3 >>> 24 & 255]; + } + function applySbox(sbox2, s0, s1, s2, s3) { + return sbox2[s0 & 255 | s1 & 65280] | sbox2[s2 >>> 16 & 255 | s3 >>> 16 & 65280] << 16; + } + function encrypt(xk, s0, s1, s2, s3) { + const { sbox2, T01, T23 } = tableEncoding; + let k = 0; + s0 ^= xk[k++], s1 ^= xk[k++], s2 ^= xk[k++], s3 ^= xk[k++]; + const rounds = xk.length / 4 - 2; + for (let i2 = 0; i2 < rounds; i2++) { + const t02 = xk[k++] ^ apply0123(T01, T23, s0, s1, s2, s3); + const t12 = xk[k++] ^ apply0123(T01, T23, s1, s2, s3, s0); + const t22 = xk[k++] ^ apply0123(T01, T23, s2, s3, s0, s1); + const t32 = xk[k++] ^ apply0123(T01, T23, s3, s0, s1, s2); + s0 = t02, s1 = t12, s2 = t22, s3 = t32; + } + const t0 = xk[k++] ^ applySbox(sbox2, s0, s1, s2, s3); + const t1 = xk[k++] ^ applySbox(sbox2, s1, s2, s3, s0); + const t2 = xk[k++] ^ applySbox(sbox2, s2, s3, s0, s1); + const t3 = xk[k++] ^ applySbox(sbox2, s3, s0, s1, s2); + return { s0: t0, s1: t1, s2: t2, s3: t3 }; + } + function decrypt(xk, s0, s1, s2, s3) { + const { sbox2, T01, T23 } = tableDecoding; + let k = 0; + s0 ^= xk[k++], s1 ^= xk[k++], s2 ^= xk[k++], s3 ^= xk[k++]; + const rounds = xk.length / 4 - 2; + for (let i2 = 0; i2 < rounds; i2++) { + const t02 = xk[k++] ^ apply0123(T01, T23, s0, s3, s2, s1); + const t12 = xk[k++] ^ apply0123(T01, T23, s1, s0, s3, s2); + const t22 = xk[k++] ^ apply0123(T01, T23, s2, s1, s0, s3); + const t32 = xk[k++] ^ apply0123(T01, T23, s3, s2, s1, s0); + s0 = t02, s1 = t12, s2 = t22, s3 = t32; + } + const t0 = xk[k++] ^ applySbox(sbox2, s0, s3, s2, s1); + const t1 = xk[k++] ^ applySbox(sbox2, s1, s0, s3, s2); + const t2 = xk[k++] ^ applySbox(sbox2, s2, s1, s0, s3); + const t3 = xk[k++] ^ applySbox(sbox2, s3, s2, s1, s0); + return { s0: t0, s1: t1, s2: t2, s3: t3 }; + } + function getDst(len, dst) { + if (!dst) + return new Uint8Array(len); + bytes3(dst); + if (dst.length < len) + throw new Error(`aes: wrong destination length, expected at least ${len}, got: ${dst.length}`); + return dst; + } + function ctrCounter(xk, nonce, src, dst) { + bytes3(nonce, BLOCK_SIZE2); + bytes3(src); + const srcLen = src.length; + dst = getDst(srcLen, dst); + const ctr3 = nonce; + const c32 = u32(ctr3); + let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]); + const src32 = u32(src); + const dst32 = u32(dst); + for (let i2 = 0; i2 + 4 <= src32.length; i2 += 4) { + dst32[i2 + 0] = src32[i2 + 0] ^ s0; + dst32[i2 + 1] = src32[i2 + 1] ^ s1; + dst32[i2 + 2] = src32[i2 + 2] ^ s2; + dst32[i2 + 3] = src32[i2 + 3] ^ s3; + let carry = 1; + for (let i3 = ctr3.length - 1; i3 >= 0; i3--) { + carry = carry + (ctr3[i3] & 255) | 0; + ctr3[i3] = carry & 255; + carry >>>= 8; + } + ({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3])); + } + const start = BLOCK_SIZE2 * Math.floor(src32.length / BLOCK_SIZE32); + if (start < srcLen) { + const b32 = new Uint32Array([s0, s1, s2, s3]); + const buf = u8(b32); + for (let i2 = start, pos = 0; i2 < srcLen; i2++, pos++) + dst[i2] = src[i2] ^ buf[pos]; + } + return dst; + } + function ctr32(xk, isLE4, nonce, src, dst) { + bytes3(nonce, BLOCK_SIZE2); + bytes3(src); + dst = getDst(src.length, dst); + const ctr3 = nonce; + const c32 = u32(ctr3); + const view = createView3(ctr3); + const src32 = u32(src); + const dst32 = u32(dst); + const ctrPos = isLE4 ? 0 : 12; + const srcLen = src.length; + let ctrNum = view.getUint32(ctrPos, isLE4); + let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]); + for (let i2 = 0; i2 + 4 <= src32.length; i2 += 4) { + dst32[i2 + 0] = src32[i2 + 0] ^ s0; + dst32[i2 + 1] = src32[i2 + 1] ^ s1; + dst32[i2 + 2] = src32[i2 + 2] ^ s2; + dst32[i2 + 3] = src32[i2 + 3] ^ s3; + ctrNum = ctrNum + 1 >>> 0; + view.setUint32(ctrPos, ctrNum, isLE4); + ({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3])); + } + const start = BLOCK_SIZE2 * Math.floor(src32.length / BLOCK_SIZE32); + if (start < srcLen) { + const b32 = new Uint32Array([s0, s1, s2, s3]); + const buf = u8(b32); + for (let i2 = start, pos = 0; i2 < srcLen; i2++, pos++) + dst[i2] = src[i2] ^ buf[pos]; + } + return dst; + } + var ctr = wrapCipher({ blockSize: 16, nonceLength: 16 }, function ctr2(key, nonce) { + bytes3(key); + bytes3(nonce, BLOCK_SIZE2); + function processCtr(buf, dst) { + const xk = expandKeyLE(key); + const n = nonce.slice(); + const out = ctrCounter(xk, n, buf, dst); + xk.fill(0); + n.fill(0); + return out; + } + return { + encrypt: (plaintext, dst) => processCtr(plaintext, dst), + decrypt: (ciphertext, dst) => processCtr(ciphertext, dst) + }; + }); + function validateBlockDecrypt(data) { + bytes3(data); + if (data.length % BLOCK_SIZE2 !== 0) { + throw new Error(`aes/(cbc-ecb).decrypt ciphertext should consist of blocks with size ${BLOCK_SIZE2}`); + } + } + function validateBlockEncrypt(plaintext, pcks5, dst) { + let outLen = plaintext.length; + const remaining = outLen % BLOCK_SIZE2; + if (!pcks5 && remaining !== 0) + throw new Error("aec/(cbc-ecb): unpadded plaintext with disabled padding"); + const b = u32(plaintext); + if (pcks5) { + let left = BLOCK_SIZE2 - remaining; + if (!left) + left = BLOCK_SIZE2; + outLen = outLen + left; + } + const out = getDst(outLen, dst); + const o = u32(out); + return { b, o, out }; + } + function validatePCKS(data, pcks5) { + if (!pcks5) + return data; + const len = data.length; + if (!len) + throw new Error(`aes/pcks5: empty ciphertext not allowed`); + const lastByte = data[len - 1]; + if (lastByte <= 0 || lastByte > 16) + throw new Error(`aes/pcks5: wrong padding byte: ${lastByte}`); + const out = data.subarray(0, -lastByte); + for (let i2 = 0; i2 < lastByte; i2++) + if (data[len - i2 - 1] !== lastByte) + throw new Error(`aes/pcks5: wrong padding`); + return out; + } + function padPCKS(left) { + const tmp = new Uint8Array(16); + const tmp32 = u32(tmp); + tmp.set(left); + const paddingByte = BLOCK_SIZE2 - left.length; + for (let i2 = BLOCK_SIZE2 - paddingByte; i2 < BLOCK_SIZE2; i2++) + tmp[i2] = paddingByte; + return tmp32; + } + var ecb = wrapCipher({ blockSize: 16 }, function ecb2(key, opts = {}) { + bytes3(key); + const pcks5 = !opts.disablePadding; + return { + encrypt: (plaintext, dst) => { + bytes3(plaintext); + const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); + const xk = expandKeyLE(key); + let i2 = 0; + for (; i2 + 4 <= b.length; ) { + const { s0, s1, s2, s3 } = encrypt(xk, b[i2 + 0], b[i2 + 1], b[i2 + 2], b[i2 + 3]); + o[i2++] = s0, o[i2++] = s1, o[i2++] = s2, o[i2++] = s3; + } + if (pcks5) { + const tmp32 = padPCKS(plaintext.subarray(i2 * 4)); + const { s0, s1, s2, s3 } = encrypt(xk, tmp32[0], tmp32[1], tmp32[2], tmp32[3]); + o[i2++] = s0, o[i2++] = s1, o[i2++] = s2, o[i2++] = s3; + } + xk.fill(0); + return _out; + }, + decrypt: (ciphertext, dst) => { + validateBlockDecrypt(ciphertext); + const xk = expandKeyDecLE(key); + const out = getDst(ciphertext.length, dst); + const b = u32(ciphertext); + const o = u32(out); + for (let i2 = 0; i2 + 4 <= b.length; ) { + const { s0, s1, s2, s3 } = decrypt(xk, b[i2 + 0], b[i2 + 1], b[i2 + 2], b[i2 + 3]); + o[i2++] = s0, o[i2++] = s1, o[i2++] = s2, o[i2++] = s3; + } + xk.fill(0); + return validatePCKS(out, pcks5); + } + }; + }); + var cbc = wrapCipher({ blockSize: 16, nonceLength: 16 }, function cbc2(key, iv, opts = {}) { + bytes3(key); + bytes3(iv, 16); + const pcks5 = !opts.disablePadding; + return { + encrypt: (plaintext, dst) => { + const xk = expandKeyLE(key); + const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); + const n32 = u32(iv); + let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; + let i2 = 0; + for (; i2 + 4 <= b.length; ) { + s0 ^= b[i2 + 0], s1 ^= b[i2 + 1], s2 ^= b[i2 + 2], s3 ^= b[i2 + 3]; + ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); + o[i2++] = s0, o[i2++] = s1, o[i2++] = s2, o[i2++] = s3; + } + if (pcks5) { + const tmp32 = padPCKS(plaintext.subarray(i2 * 4)); + s0 ^= tmp32[0], s1 ^= tmp32[1], s2 ^= tmp32[2], s3 ^= tmp32[3]; + ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); + o[i2++] = s0, o[i2++] = s1, o[i2++] = s2, o[i2++] = s3; + } + xk.fill(0); + return _out; + }, + decrypt: (ciphertext, dst) => { + validateBlockDecrypt(ciphertext); + const xk = expandKeyDecLE(key); + const n32 = u32(iv); + const out = getDst(ciphertext.length, dst); + const b = u32(ciphertext); + const o = u32(out); + let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; + for (let i2 = 0; i2 + 4 <= b.length; ) { + const ps0 = s0, ps1 = s1, ps2 = s2, ps3 = s3; + s0 = b[i2 + 0], s1 = b[i2 + 1], s2 = b[i2 + 2], s3 = b[i2 + 3]; + const { s0: o0, s1: o1, s2: o2, s3: o3 } = decrypt(xk, s0, s1, s2, s3); + o[i2++] = o0 ^ ps0, o[i2++] = o1 ^ ps1, o[i2++] = o2 ^ ps2, o[i2++] = o3 ^ ps3; + } + xk.fill(0); + return validatePCKS(out, pcks5); + } + }; + }); + var cfb = wrapCipher({ blockSize: 16, nonceLength: 16 }, function cfb2(key, iv) { + bytes3(key); + bytes3(iv, 16); + function processCfb(src, isEncrypt, dst) { + const xk = expandKeyLE(key); + const srcLen = src.length; + dst = getDst(srcLen, dst); + const src32 = u32(src); + const dst32 = u32(dst); + const next32 = isEncrypt ? dst32 : src32; + const n32 = u32(iv); + let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; + for (let i2 = 0; i2 + 4 <= src32.length; ) { + const { s0: e0, s1: e1, s2: e2, s3: e3 } = encrypt(xk, s0, s1, s2, s3); + dst32[i2 + 0] = src32[i2 + 0] ^ e0; + dst32[i2 + 1] = src32[i2 + 1] ^ e1; + dst32[i2 + 2] = src32[i2 + 2] ^ e2; + dst32[i2 + 3] = src32[i2 + 3] ^ e3; + s0 = next32[i2++], s1 = next32[i2++], s2 = next32[i2++], s3 = next32[i2++]; + } + const start = BLOCK_SIZE2 * Math.floor(src32.length / BLOCK_SIZE32); + if (start < srcLen) { + ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); + const buf = u8(new Uint32Array([s0, s1, s2, s3])); + for (let i2 = start, pos = 0; i2 < srcLen; i2++, pos++) + dst[i2] = src[i2] ^ buf[pos]; + buf.fill(0); + } + xk.fill(0); + return dst; + } + return { + encrypt: (plaintext, dst) => processCfb(plaintext, true, dst), + decrypt: (ciphertext, dst) => processCfb(ciphertext, false, dst) + }; + }); + function computeTag(fn, isLE4, key, data, AAD) { + const h = fn.create(key, data.length + (AAD?.length || 0)); + if (AAD) + h.update(AAD); + h.update(data); + const num = new Uint8Array(16); + const view = createView3(num); + if (AAD) + setBigUint643(view, 0, BigInt(AAD.length * 8), isLE4); + setBigUint643(view, 8, BigInt(data.length * 8), isLE4); + h.update(num); + return h.digest(); + } + var gcm = wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16 }, function gcm2(key, nonce, AAD) { + bytes3(nonce); + if (nonce.length === 0) + throw new Error("aes/gcm: empty nonce"); + const tagLength = 16; + function _computeTag(authKey, tagMask, data) { + const tag = computeTag(ghash, false, authKey, data, AAD); + for (let i2 = 0; i2 < tagMask.length; i2++) + tag[i2] ^= tagMask[i2]; + return tag; + } + function deriveKeys() { + const xk = expandKeyLE(key); + const authKey = EMPTY_BLOCK.slice(); + const counter = EMPTY_BLOCK.slice(); + ctr32(xk, false, counter, counter, authKey); + if (nonce.length === 12) { + counter.set(nonce); + } else { + const nonceLen = EMPTY_BLOCK.slice(); + const view = createView3(nonceLen); + setBigUint643(view, 8, BigInt(nonce.length * 8), false); + ghash.create(authKey).update(nonce).update(nonceLen).digestInto(counter); + } + const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK); + return { xk, authKey, counter, tagMask }; + } + return { + encrypt: (plaintext) => { + bytes3(plaintext); + const { xk, authKey, counter, tagMask } = deriveKeys(); + const out = new Uint8Array(plaintext.length + tagLength); + ctr32(xk, false, counter, plaintext, out); + const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength)); + out.set(tag, plaintext.length); + xk.fill(0); + return out; + }, + decrypt: (ciphertext) => { + bytes3(ciphertext); + if (ciphertext.length < tagLength) + throw new Error(`aes/gcm: ciphertext less than tagLen (${tagLength})`); + const { xk, authKey, counter, tagMask } = deriveKeys(); + const data = ciphertext.subarray(0, -tagLength); + const passedTag = ciphertext.subarray(-tagLength); + const tag = _computeTag(authKey, tagMask, data); + if (!equalBytes2(tag, passedTag)) + throw new Error("aes/gcm: invalid ghash tag"); + const out = ctr32(xk, false, counter, data); + authKey.fill(0); + tagMask.fill(0); + xk.fill(0); + return out; + } + }; + }); + var limit = (name, min, max) => (value) => { + if (!Number.isSafeInteger(value) || min > value || value > max) + throw new Error(`${name}: invalid value=${value}, must be [${min}..${max}]`); + }; + var siv = wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16 }, function siv2(key, nonce, AAD) { + const tagLength = 16; + const AAD_LIMIT = limit("AAD", 0, 2 ** 36); + const PLAIN_LIMIT = limit("plaintext", 0, 2 ** 36); + const NONCE_LIMIT = limit("nonce", 12, 12); + const CIPHER_LIMIT = limit("ciphertext", 16, 2 ** 36 + 16); + bytes3(nonce); + NONCE_LIMIT(nonce.length); + if (AAD) { + bytes3(AAD); + AAD_LIMIT(AAD.length); + } + function deriveKeys() { + const len = key.length; + if (len !== 16 && len !== 24 && len !== 32) + throw new Error(`key length must be 16, 24 or 32 bytes, got: ${len} bytes`); + const xk = expandKeyLE(key); + const encKey = new Uint8Array(len); + const authKey = new Uint8Array(16); + const n32 = u32(nonce); + let s0 = 0, s1 = n32[0], s2 = n32[1], s3 = n32[2]; + let counter = 0; + for (const derivedKey of [authKey, encKey].map(u32)) { + const d32 = u32(derivedKey); + for (let i2 = 0; i2 < d32.length; i2 += 2) { + const { s0: o0, s1: o1 } = encrypt(xk, s0, s1, s2, s3); + d32[i2 + 0] = o0; + d32[i2 + 1] = o1; + s0 = ++counter; + } + } + xk.fill(0); + return { authKey, encKey: expandKeyLE(encKey) }; + } + function _computeTag(encKey, authKey, data) { + const tag = computeTag(polyval, true, authKey, data, AAD); + for (let i2 = 0; i2 < 12; i2++) + tag[i2] ^= nonce[i2]; + tag[15] &= 127; + const t32 = u32(tag); + let s0 = t32[0], s1 = t32[1], s2 = t32[2], s3 = t32[3]; + ({ s0, s1, s2, s3 } = encrypt(encKey, s0, s1, s2, s3)); + t32[0] = s0, t32[1] = s1, t32[2] = s2, t32[3] = s3; + return tag; + } + function processSiv(encKey, tag, input) { + let block = tag.slice(); + block[15] |= 128; + return ctr32(encKey, true, block, input); + } + return { + encrypt: (plaintext) => { + bytes3(plaintext); + PLAIN_LIMIT(plaintext.length); + const { encKey, authKey } = deriveKeys(); + const tag = _computeTag(encKey, authKey, plaintext); + const out = new Uint8Array(plaintext.length + tagLength); + out.set(tag, plaintext.length); + out.set(processSiv(encKey, tag, plaintext)); + encKey.fill(0); + authKey.fill(0); + return out; + }, + decrypt: (ciphertext) => { + bytes3(ciphertext); + CIPHER_LIMIT(ciphertext.length); + const tag = ciphertext.subarray(-tagLength); + const { encKey, authKey } = deriveKeys(); + const plaintext = processSiv(encKey, tag, ciphertext.subarray(0, -tagLength)); + const expectedTag = _computeTag(encKey, authKey, plaintext); + encKey.fill(0); + authKey.fill(0); + if (!equalBytes2(tag, expectedTag)) + throw new Error("invalid polyval tag"); + return plaintext; + } + }; + }); + + // nip04.ts + function encrypt2(secretKey, pubkey, text) { + const privkey = secretKey instanceof Uint8Array ? bytesToHex2(secretKey) : secretKey; + const key = secp256k1.getSharedSecret(privkey, "02" + pubkey); + const normalizedKey = getNormalizedX(key); + let iv = Uint8Array.from(randomBytes2(16)); + let plaintext = utf8Encoder.encode(text); + let ciphertext = cbc(normalizedKey, iv).encrypt(plaintext); + let ctb64 = base64.encode(new Uint8Array(ciphertext)); + let ivb64 = base64.encode(new Uint8Array(iv.buffer)); + return `${ctb64}?iv=${ivb64}`; + } + function decrypt2(secretKey, pubkey, data) { + const privkey = secretKey instanceof Uint8Array ? bytesToHex2(secretKey) : secretKey; + let [ctb64, ivb64] = data.split("?iv="); + let key = secp256k1.getSharedSecret(privkey, "02" + pubkey); + let normalizedKey = getNormalizedX(key); + let iv = base64.decode(ivb64); + let ciphertext = base64.decode(ctb64); + let plaintext = cbc(normalizedKey, iv).decrypt(ciphertext); + return utf8Decoder.decode(plaintext); + } + function getNormalizedX(key) { + return key.slice(1, 33); + } + + // nip05.ts + var nip05_exports = {}; + __export(nip05_exports, { + NIP05_REGEX: () => NIP05_REGEX, + isNip05: () => isNip05, + isValid: () => isValid, + queryProfile: () => queryProfile, + searchDomain: () => searchDomain, + useFetchImplementation: () => useFetchImplementation + }); + var NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$/; + var isNip05 = (value) => NIP05_REGEX.test(value || ""); + var _fetch; + try { + _fetch = fetch; + } catch (_) { + null; + } + function useFetchImplementation(fetchImplementation) { + _fetch = fetchImplementation; + } + async function searchDomain(domain, query = "") { + try { + const url = `https://${domain}/.well-known/nostr.json?name=${query}`; + const res = await _fetch(url, { redirect: "manual" }); + if (res.status !== 200) { + throw Error("Wrong response code"); + } + const json = await res.json(); + return json.names; + } catch (_) { + return {}; + } + } + async function queryProfile(fullname) { + const match = fullname.match(NIP05_REGEX); + if (!match) + return null; + const [, name = "_", domain] = match; + try { + const url = `https://${domain}/.well-known/nostr.json?name=${name}`; + const res = await _fetch(url, { redirect: "manual" }); + if (res.status !== 200) { + throw Error("Wrong response code"); + } + const json = await res.json(); + const pubkey = json.names[name]; + return pubkey ? { pubkey, relays: json.relays?.[pubkey] } : null; + } catch (_e) { + return null; + } + } + async function isValid(pubkey, nip05) { + const res = await queryProfile(nip05); + return res ? res.pubkey === pubkey : false; + } + + // nip06.ts + var nip06_exports = {}; + __export(nip06_exports, { + accountFromExtendedKey: () => accountFromExtendedKey, + accountFromSeedWords: () => accountFromSeedWords, + extendedKeysFromSeedWords: () => extendedKeysFromSeedWords, + generateSeedWords: () => generateSeedWords, + privateKeyFromSeedWords: () => privateKeyFromSeedWords, + validateWords: () => validateWords + }); + + // node_modules/@scure/bip39/esm/wordlists/english.js + var wordlist = `abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo`.split("\n"); + + // node_modules/@noble/hashes/esm/hmac.js + var HMAC2 = class extends Hash2 { + constructor(hash3, _key) { + super(); + this.finished = false; + this.destroyed = false; + assert_default.hash(hash3); + const key = toBytes2(_key); + this.iHash = hash3.create(); + if (typeof this.iHash.update !== "function") + throw new Error("Expected instance of class which extends utils.Hash"); + this.blockLen = this.iHash.blockLen; + this.outputLen = this.iHash.outputLen; + const blockLen = this.blockLen; + const pad2 = new Uint8Array(blockLen); + pad2.set(key.length > blockLen ? hash3.create().update(key).digest() : key); + for (let i2 = 0; i2 < pad2.length; i2++) + pad2[i2] ^= 54; + this.iHash.update(pad2); + this.oHash = hash3.create(); + for (let i2 = 0; i2 < pad2.length; i2++) + pad2[i2] ^= 54 ^ 92; + this.oHash.update(pad2); + pad2.fill(0); + } + update(buf) { + assert_default.exists(this); + this.iHash.update(buf); + return this; + } + digestInto(out) { + assert_default.exists(this); + assert_default.bytes(out, this.outputLen); + this.finished = true; + this.iHash.digestInto(out); + this.oHash.update(out); + this.oHash.digestInto(out); + this.destroy(); + } + digest() { + const out = new Uint8Array(this.oHash.outputLen); + this.digestInto(out); + return out; + } + _cloneInto(to) { + to || (to = Object.create(Object.getPrototypeOf(this), {})); + const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this; + to = to; + to.finished = finished; + to.destroyed = destroyed; + to.blockLen = blockLen; + to.outputLen = outputLen; + to.oHash = oHash._cloneInto(to.oHash); + to.iHash = iHash._cloneInto(to.iHash); + return to; + } + destroy() { + this.destroyed = true; + this.oHash.destroy(); + this.iHash.destroy(); + } + }; + var hmac2 = (hash3, key, message) => new HMAC2(hash3, key).update(message).digest(); + hmac2.create = (hash3, key) => new HMAC2(hash3, key); + + // node_modules/@noble/hashes/esm/pbkdf2.js + function pbkdf2Init(hash3, _password, _salt, _opts) { + assert_default.hash(hash3); + const opts = checkOpts({ dkLen: 32, asyncTick: 10 }, _opts); + const { c, dkLen, asyncTick } = opts; + assert_default.number(c); + assert_default.number(dkLen); + assert_default.number(asyncTick); + if (c < 1) + throw new Error("PBKDF2: iterations (c) should be >= 1"); + const password = toBytes2(_password); + const salt2 = toBytes2(_salt); + const DK = new Uint8Array(dkLen); + const PRF = hmac2.create(hash3, password); + const PRFSalt = PRF._cloneInto().update(salt2); + return { c, dkLen, asyncTick, DK, PRF, PRFSalt }; + } + function pbkdf2Output(PRF, PRFSalt, DK, prfW, u) { + PRF.destroy(); + PRFSalt.destroy(); + if (prfW) + prfW.destroy(); + u.fill(0); + return DK; + } + function pbkdf2(hash3, password, salt2, opts) { + const { c, dkLen, DK, PRF, PRFSalt } = pbkdf2Init(hash3, password, salt2, opts); + let prfW; + const arr = new Uint8Array(4); + const view = createView2(arr); + const u = new Uint8Array(PRF.outputLen); + for (let ti = 1, pos = 0; pos < dkLen; ti++, pos += PRF.outputLen) { + const Ti = DK.subarray(pos, pos + PRF.outputLen); + view.setInt32(0, ti, false); + (prfW = PRFSalt._cloneInto(prfW)).update(arr).digestInto(u); + Ti.set(u.subarray(0, Ti.length)); + for (let ui = 1; ui < c; ui++) { + PRF._cloneInto(prfW).update(u).digestInto(u); + for (let i2 = 0; i2 < Ti.length; i2++) + Ti[i2] ^= u[i2]; + } + } + return pbkdf2Output(PRF, PRFSalt, DK, prfW, u); + } + + // node_modules/@noble/hashes/esm/_u64.js + var U32_MASK64 = BigInt(2 ** 32 - 1); + var _32n = BigInt(32); + function fromBig(n, le = false) { + if (le) + return { h: Number(n & U32_MASK64), l: Number(n >> _32n & U32_MASK64) }; + return { h: Number(n >> _32n & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 }; + } + function split(lst, le = false) { + let Ah = new Uint32Array(lst.length); + let Al = new Uint32Array(lst.length); + for (let i2 = 0; i2 < lst.length; i2++) { + const { h, l } = fromBig(lst[i2], le); + [Ah[i2], Al[i2]] = [h, l]; + } + return [Ah, Al]; + } + var toBig = (h, l) => BigInt(h >>> 0) << _32n | BigInt(l >>> 0); + var shrSH = (h, l, s) => h >>> s; + var shrSL = (h, l, s) => h << 32 - s | l >>> s; + var rotrSH = (h, l, s) => h >>> s | l << 32 - s; + var rotrSL = (h, l, s) => h << 32 - s | l >>> s; + var rotrBH = (h, l, s) => h << 64 - s | l >>> s - 32; + var rotrBL = (h, l, s) => h >>> s - 32 | l << 64 - s; + var rotr32H = (h, l) => l; + var rotr32L = (h, l) => h; + var rotlSH = (h, l, s) => h << s | l >>> 32 - s; + var rotlSL = (h, l, s) => l << s | h >>> 32 - s; + var rotlBH = (h, l, s) => l << s - 32 | h >>> 64 - s; + var rotlBL = (h, l, s) => h << s - 32 | l >>> 64 - s; + function add(Ah, Al, Bh, Bl) { + const l = (Al >>> 0) + (Bl >>> 0); + return { h: Ah + Bh + (l / 2 ** 32 | 0) | 0, l: l | 0 }; + } + var add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0); + var add3H = (low, Ah, Bh, Ch) => Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0; + var add4L = (Al, Bl, Cl, Dl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0); + var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0; + var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0); + var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0; + var u64 = { + fromBig, + split, + toBig, + shrSH, + shrSL, + rotrSH, + rotrSL, + rotrBH, + rotrBL, + rotr32H, + rotr32L, + rotlSH, + rotlSL, + rotlBH, + rotlBL, + add, + add3L, + add3H, + add4L, + add4H, + add5H, + add5L + }; + var u64_default = u64; + + // node_modules/@noble/hashes/esm/sha512.js + var [SHA512_Kh, SHA512_Kl] = u64_default.split([ + "0x428a2f98d728ae22", + "0x7137449123ef65cd", + "0xb5c0fbcfec4d3b2f", + "0xe9b5dba58189dbbc", + "0x3956c25bf348b538", + "0x59f111f1b605d019", + "0x923f82a4af194f9b", + "0xab1c5ed5da6d8118", + "0xd807aa98a3030242", + "0x12835b0145706fbe", + "0x243185be4ee4b28c", + "0x550c7dc3d5ffb4e2", + "0x72be5d74f27b896f", + "0x80deb1fe3b1696b1", + "0x9bdc06a725c71235", + "0xc19bf174cf692694", + "0xe49b69c19ef14ad2", + "0xefbe4786384f25e3", + "0x0fc19dc68b8cd5b5", + "0x240ca1cc77ac9c65", + "0x2de92c6f592b0275", + "0x4a7484aa6ea6e483", + "0x5cb0a9dcbd41fbd4", + "0x76f988da831153b5", + "0x983e5152ee66dfab", + "0xa831c66d2db43210", + "0xb00327c898fb213f", + "0xbf597fc7beef0ee4", + "0xc6e00bf33da88fc2", + "0xd5a79147930aa725", + "0x06ca6351e003826f", + "0x142929670a0e6e70", + "0x27b70a8546d22ffc", + "0x2e1b21385c26c926", + "0x4d2c6dfc5ac42aed", + "0x53380d139d95b3df", + "0x650a73548baf63de", + "0x766a0abb3c77b2a8", + "0x81c2c92e47edaee6", + "0x92722c851482353b", + "0xa2bfe8a14cf10364", + "0xa81a664bbc423001", + "0xc24b8b70d0f89791", + "0xc76c51a30654be30", + "0xd192e819d6ef5218", + "0xd69906245565a910", + "0xf40e35855771202a", + "0x106aa07032bbd1b8", + "0x19a4c116b8d2d0c8", + "0x1e376c085141ab53", + "0x2748774cdf8eeb99", + "0x34b0bcb5e19b48a8", + "0x391c0cb3c5c95a63", + "0x4ed8aa4ae3418acb", + "0x5b9cca4f7763e373", + "0x682e6ff3d6b2b8a3", + "0x748f82ee5defb2fc", + "0x78a5636f43172f60", + "0x84c87814a1f0ab72", + "0x8cc702081a6439ec", + "0x90befffa23631e28", + "0xa4506cebde82bde9", + "0xbef9a3f7b2c67915", + "0xc67178f2e372532b", + "0xca273eceea26619c", + "0xd186b8c721c0c207", + "0xeada7dd6cde0eb1e", + "0xf57d4f7fee6ed178", + "0x06f067aa72176fba", + "0x0a637dc5a2c898a6", + "0x113f9804bef90dae", + "0x1b710b35131c471b", + "0x28db77f523047d84", + "0x32caab7b40c72493", + "0x3c9ebe0a15c9bebc", + "0x431d67c49c100d4c", + "0x4cc5d4becb3e42b6", + "0x597f299cfc657e2a", + "0x5fcb6fab3ad6faec", + "0x6c44198c4a475817" + ].map((n) => BigInt(n))); + var SHA512_W_H = new Uint32Array(80); + var SHA512_W_L = new Uint32Array(80); + var SHA512 = class extends SHA22 { + constructor() { + super(128, 64, 16, false); + this.Ah = 1779033703 | 0; + this.Al = 4089235720 | 0; + this.Bh = 3144134277 | 0; + this.Bl = 2227873595 | 0; + this.Ch = 1013904242 | 0; + this.Cl = 4271175723 | 0; + this.Dh = 2773480762 | 0; + this.Dl = 1595750129 | 0; + this.Eh = 1359893119 | 0; + this.El = 2917565137 | 0; + this.Fh = 2600822924 | 0; + this.Fl = 725511199 | 0; + this.Gh = 528734635 | 0; + this.Gl = 4215389547 | 0; + this.Hh = 1541459225 | 0; + this.Hl = 327033209 | 0; + } + get() { + const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this; + return [Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl]; + } + set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) { + this.Ah = Ah | 0; + this.Al = Al | 0; + this.Bh = Bh | 0; + this.Bl = Bl | 0; + this.Ch = Ch | 0; + this.Cl = Cl | 0; + this.Dh = Dh | 0; + this.Dl = Dl | 0; + this.Eh = Eh | 0; + this.El = El | 0; + this.Fh = Fh | 0; + this.Fl = Fl | 0; + this.Gh = Gh | 0; + this.Gl = Gl | 0; + this.Hh = Hh | 0; + this.Hl = Hl | 0; + } + process(view, offset) { + for (let i2 = 0; i2 < 16; i2++, offset += 4) { + SHA512_W_H[i2] = view.getUint32(offset); + SHA512_W_L[i2] = view.getUint32(offset += 4); + } + for (let i2 = 16; i2 < 80; i2++) { + const W15h = SHA512_W_H[i2 - 15] | 0; + const W15l = SHA512_W_L[i2 - 15] | 0; + const s0h = u64_default.rotrSH(W15h, W15l, 1) ^ u64_default.rotrSH(W15h, W15l, 8) ^ u64_default.shrSH(W15h, W15l, 7); + const s0l = u64_default.rotrSL(W15h, W15l, 1) ^ u64_default.rotrSL(W15h, W15l, 8) ^ u64_default.shrSL(W15h, W15l, 7); + const W2h = SHA512_W_H[i2 - 2] | 0; + const W2l = SHA512_W_L[i2 - 2] | 0; + const s1h = u64_default.rotrSH(W2h, W2l, 19) ^ u64_default.rotrBH(W2h, W2l, 61) ^ u64_default.shrSH(W2h, W2l, 6); + const s1l = u64_default.rotrSL(W2h, W2l, 19) ^ u64_default.rotrBL(W2h, W2l, 61) ^ u64_default.shrSL(W2h, W2l, 6); + const SUMl = u64_default.add4L(s0l, s1l, SHA512_W_L[i2 - 7], SHA512_W_L[i2 - 16]); + const SUMh = u64_default.add4H(SUMl, s0h, s1h, SHA512_W_H[i2 - 7], SHA512_W_H[i2 - 16]); + SHA512_W_H[i2] = SUMh | 0; + SHA512_W_L[i2] = SUMl | 0; + } + let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this; + for (let i2 = 0; i2 < 80; i2++) { + const sigma1h = u64_default.rotrSH(Eh, El, 14) ^ u64_default.rotrSH(Eh, El, 18) ^ u64_default.rotrBH(Eh, El, 41); + const sigma1l = u64_default.rotrSL(Eh, El, 14) ^ u64_default.rotrSL(Eh, El, 18) ^ u64_default.rotrBL(Eh, El, 41); + const CHIh = Eh & Fh ^ ~Eh & Gh; + const CHIl = El & Fl ^ ~El & Gl; + const T1ll = u64_default.add5L(Hl, sigma1l, CHIl, SHA512_Kl[i2], SHA512_W_L[i2]); + const T1h = u64_default.add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i2], SHA512_W_H[i2]); + const T1l = T1ll | 0; + const sigma0h = u64_default.rotrSH(Ah, Al, 28) ^ u64_default.rotrBH(Ah, Al, 34) ^ u64_default.rotrBH(Ah, Al, 39); + const sigma0l = u64_default.rotrSL(Ah, Al, 28) ^ u64_default.rotrBL(Ah, Al, 34) ^ u64_default.rotrBL(Ah, Al, 39); + const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch; + const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl; + Hh = Gh | 0; + Hl = Gl | 0; + Gh = Fh | 0; + Gl = Fl | 0; + Fh = Eh | 0; + Fl = El | 0; + ({ h: Eh, l: El } = u64_default.add(Dh | 0, Dl | 0, T1h | 0, T1l | 0)); + Dh = Ch | 0; + Dl = Cl | 0; + Ch = Bh | 0; + Cl = Bl | 0; + Bh = Ah | 0; + Bl = Al | 0; + const All = u64_default.add3L(T1l, sigma0l, MAJl); + Ah = u64_default.add3H(All, T1h, sigma0h, MAJh); + Al = All | 0; + } + ({ h: Ah, l: Al } = u64_default.add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0)); + ({ h: Bh, l: Bl } = u64_default.add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0)); + ({ h: Ch, l: Cl } = u64_default.add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0)); + ({ h: Dh, l: Dl } = u64_default.add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0)); + ({ h: Eh, l: El } = u64_default.add(this.Eh | 0, this.El | 0, Eh | 0, El | 0)); + ({ h: Fh, l: Fl } = u64_default.add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0)); + ({ h: Gh, l: Gl } = u64_default.add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0)); + ({ h: Hh, l: Hl } = u64_default.add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0)); + this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl); + } + roundClean() { + SHA512_W_H.fill(0); + SHA512_W_L.fill(0); + } + destroy() { + this.buffer.fill(0); + this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + }; + var SHA512_224 = class extends SHA512 { + constructor() { + super(); + this.Ah = 2352822216 | 0; + this.Al = 424955298 | 0; + this.Bh = 1944164710 | 0; + this.Bl = 2312950998 | 0; + this.Ch = 502970286 | 0; + this.Cl = 855612546 | 0; + this.Dh = 1738396948 | 0; + this.Dl = 1479516111 | 0; + this.Eh = 258812777 | 0; + this.El = 2077511080 | 0; + this.Fh = 2011393907 | 0; + this.Fl = 79989058 | 0; + this.Gh = 1067287976 | 0; + this.Gl = 1780299464 | 0; + this.Hh = 286451373 | 0; + this.Hl = 2446758561 | 0; + this.outputLen = 28; + } + }; + var SHA512_256 = class extends SHA512 { + constructor() { + super(); + this.Ah = 573645204 | 0; + this.Al = 4230739756 | 0; + this.Bh = 2673172387 | 0; + this.Bl = 3360449730 | 0; + this.Ch = 596883563 | 0; + this.Cl = 1867755857 | 0; + this.Dh = 2520282905 | 0; + this.Dl = 1497426621 | 0; + this.Eh = 2519219938 | 0; + this.El = 2827943907 | 0; + this.Fh = 3193839141 | 0; + this.Fl = 1401305490 | 0; + this.Gh = 721525244 | 0; + this.Gl = 746961066 | 0; + this.Hh = 246885852 | 0; + this.Hl = 2177182882 | 0; + this.outputLen = 32; + } + }; + var SHA384 = class extends SHA512 { + constructor() { + super(); + this.Ah = 3418070365 | 0; + this.Al = 3238371032 | 0; + this.Bh = 1654270250 | 0; + this.Bl = 914150663 | 0; + this.Ch = 2438529370 | 0; + this.Cl = 812702999 | 0; + this.Dh = 355462360 | 0; + this.Dl = 4144912697 | 0; + this.Eh = 1731405415 | 0; + this.El = 4290775857 | 0; + this.Fh = 2394180231 | 0; + this.Fl = 1750603025 | 0; + this.Gh = 3675008525 | 0; + this.Gl = 1694076839 | 0; + this.Hh = 1203062813 | 0; + this.Hl = 3204075428 | 0; + this.outputLen = 48; + } + }; + var sha512 = wrapConstructor2(() => new SHA512()); + var sha512_224 = wrapConstructor2(() => new SHA512_224()); + var sha512_256 = wrapConstructor2(() => new SHA512_256()); + var sha384 = wrapConstructor2(() => new SHA384()); + + // node_modules/@scure/bip39/esm/index.js + var isJapanese = (wordlist2) => wordlist2[0] === "\u3042\u3044\u3053\u304F\u3057\u3093"; + function nfkd(str) { + if (typeof str !== "string") + throw new TypeError(`Invalid mnemonic type: ${typeof str}`); + return str.normalize("NFKD"); + } + function normalize2(str) { + const norm = nfkd(str); + const words = norm.split(" "); + if (![12, 15, 18, 21, 24].includes(words.length)) + throw new Error("Invalid mnemonic"); + return { nfkd: norm, words }; + } + function assertEntropy(entropy) { + assert_default.bytes(entropy, 16, 20, 24, 28, 32); + } + function generateMnemonic(wordlist2, strength = 128) { + assert_default.number(strength); + if (strength % 32 !== 0 || strength > 256) + throw new TypeError("Invalid entropy"); + return entropyToMnemonic(randomBytes2(strength / 8), wordlist2); + } + var calcChecksum = (entropy) => { + const bitsLeft = 8 - entropy.length / 4; + return new Uint8Array([sha2562(entropy)[0] >> bitsLeft << bitsLeft]); + }; + function getCoder(wordlist2) { + if (!Array.isArray(wordlist2) || wordlist2.length !== 2048 || typeof wordlist2[0] !== "string") + throw new Error("Worlist: expected array of 2048 strings"); + wordlist2.forEach((i2) => { + if (typeof i2 !== "string") + throw new Error(`Wordlist: non-string element: ${i2}`); + }); + return utils.chain(utils.checksum(1, calcChecksum), utils.radix2(11, true), utils.alphabet(wordlist2)); + } + function mnemonicToEntropy(mnemonic, wordlist2) { + const { words } = normalize2(mnemonic); + const entropy = getCoder(wordlist2).decode(words); + assertEntropy(entropy); + return entropy; + } + function entropyToMnemonic(entropy, wordlist2) { + assertEntropy(entropy); + const words = getCoder(wordlist2).encode(entropy); + return words.join(isJapanese(wordlist2) ? "\u3000" : " "); + } + function validateMnemonic(mnemonic, wordlist2) { + try { + mnemonicToEntropy(mnemonic, wordlist2); + } catch (e) { + return false; + } + return true; + } + var salt = (passphrase) => nfkd(`mnemonic${passphrase}`); + function mnemonicToSeedSync(mnemonic, passphrase = "") { + return pbkdf2(sha512, normalize2(mnemonic).nfkd, salt(passphrase), { c: 2048, dkLen: 64 }); + } + + // node_modules/@noble/hashes/esm/ripemd160.js + var Rho = new Uint8Array([7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8]); + var Id = Uint8Array.from({ length: 16 }, (_, i2) => i2); + var Pi = Id.map((i2) => (9 * i2 + 5) % 16); + var idxL = [Id]; + var idxR = [Pi]; + for (let i2 = 0; i2 < 4; i2++) + for (let j of [idxL, idxR]) + j.push(j[i2].map((k) => Rho[k])); + var shifts = [ + [11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8], + [12, 13, 11, 15, 6, 9, 9, 7, 12, 15, 11, 13, 7, 8, 7, 7], + [13, 15, 14, 11, 7, 7, 6, 8, 13, 14, 13, 12, 5, 5, 6, 9], + [14, 11, 12, 14, 8, 6, 5, 5, 15, 12, 15, 14, 9, 9, 8, 6], + [15, 12, 13, 13, 9, 5, 8, 6, 14, 11, 12, 11, 8, 6, 5, 5] + ].map((i2) => new Uint8Array(i2)); + var shiftsL = idxL.map((idx, i2) => idx.map((j) => shifts[i2][j])); + var shiftsR = idxR.map((idx, i2) => idx.map((j) => shifts[i2][j])); + var Kl = new Uint32Array([0, 1518500249, 1859775393, 2400959708, 2840853838]); + var Kr = new Uint32Array([1352829926, 1548603684, 1836072691, 2053994217, 0]); + var rotl = (word, shift) => word << shift | word >>> 32 - shift; + function f(group, x, y, z) { + if (group === 0) + return x ^ y ^ z; + else if (group === 1) + return x & y | ~x & z; + else if (group === 2) + return (x | ~y) ^ z; + else if (group === 3) + return x & z | y & ~z; + else + return x ^ (y | ~z); + } + var BUF = new Uint32Array(16); + var RIPEMD160 = class extends SHA22 { + constructor() { + super(64, 20, 8, true); + this.h0 = 1732584193 | 0; + this.h1 = 4023233417 | 0; + this.h2 = 2562383102 | 0; + this.h3 = 271733878 | 0; + this.h4 = 3285377520 | 0; + } + get() { + const { h0, h1, h2, h3, h4 } = this; + return [h0, h1, h2, h3, h4]; + } + set(h0, h1, h2, h3, h4) { + this.h0 = h0 | 0; + this.h1 = h1 | 0; + this.h2 = h2 | 0; + this.h3 = h3 | 0; + this.h4 = h4 | 0; + } + process(view, offset) { + for (let i2 = 0; i2 < 16; i2++, offset += 4) + BUF[i2] = view.getUint32(offset, true); + let al = this.h0 | 0, ar = al, bl = this.h1 | 0, br = bl, cl = this.h2 | 0, cr = cl, dl = this.h3 | 0, dr = dl, el = this.h4 | 0, er = el; + for (let group = 0; group < 5; group++) { + const rGroup = 4 - group; + const hbl = Kl[group], hbr = Kr[group]; + const rl = idxL[group], rr = idxR[group]; + const sl = shiftsL[group], sr = shiftsR[group]; + for (let i2 = 0; i2 < 16; i2++) { + const tl = rotl(al + f(group, bl, cl, dl) + BUF[rl[i2]] + hbl, sl[i2]) + el | 0; + al = el, el = dl, dl = rotl(cl, 10) | 0, cl = bl, bl = tl; + } + for (let i2 = 0; i2 < 16; i2++) { + const tr = rotl(ar + f(rGroup, br, cr, dr) + BUF[rr[i2]] + hbr, sr[i2]) + er | 0; + ar = er, er = dr, dr = rotl(cr, 10) | 0, cr = br, br = tr; + } + } + this.set(this.h1 + cl + dr | 0, this.h2 + dl + er | 0, this.h3 + el + ar | 0, this.h4 + al + br | 0, this.h0 + bl + cr | 0); + } + roundClean() { + BUF.fill(0); + } + destroy() { + this.destroyed = true; + this.buffer.fill(0); + this.set(0, 0, 0, 0, 0); + } + }; + var ripemd160 = wrapConstructor2(() => new RIPEMD160()); + + // node_modules/@scure/bip32/node_modules/@noble/curves/esm/abstract/utils.js + var utils_exports3 = {}; + __export(utils_exports3, { + bitGet: () => bitGet2, + bitLen: () => bitLen2, + bitMask: () => bitMask2, + bitSet: () => bitSet2, + bytesToHex: () => bytesToHex3, + bytesToNumberBE: () => bytesToNumberBE2, + bytesToNumberLE: () => bytesToNumberLE2, + concatBytes: () => concatBytes4, + createHmacDrbg: () => createHmacDrbg2, + ensureBytes: () => ensureBytes2, + equalBytes: () => equalBytes3, + hexToBytes: () => hexToBytes3, + hexToNumber: () => hexToNumber2, + numberToBytesBE: () => numberToBytesBE2, + numberToBytesLE: () => numberToBytesLE2, + numberToHexUnpadded: () => numberToHexUnpadded2, + numberToVarBytesBE: () => numberToVarBytesBE2, + utf8ToBytes: () => utf8ToBytes5, + validateObject: () => validateObject2 + }); + var _0n6 = BigInt(0); + var _1n6 = BigInt(1); + var _2n5 = BigInt(2); + var u8a4 = (a) => a instanceof Uint8Array; + var hexes3 = Array.from({ length: 256 }, (v, i2) => i2.toString(16).padStart(2, "0")); + function bytesToHex3(bytes4) { + if (!u8a4(bytes4)) + throw new Error("Uint8Array expected"); + let hex2 = ""; + for (let i2 = 0; i2 < bytes4.length; i2++) { + hex2 += hexes3[bytes4[i2]]; + } + return hex2; + } + function numberToHexUnpadded2(num) { + const hex2 = num.toString(16); + return hex2.length & 1 ? `0${hex2}` : hex2; + } + function hexToNumber2(hex2) { + if (typeof hex2 !== "string") + throw new Error("hex string expected, got " + typeof hex2); + return BigInt(hex2 === "" ? "0" : `0x${hex2}`); + } + function hexToBytes3(hex2) { + if (typeof hex2 !== "string") + throw new Error("hex string expected, got " + typeof hex2); + const len = hex2.length; + if (len % 2) + throw new Error("padded hex string expected, got unpadded hex of length " + len); + const array = new Uint8Array(len / 2); + for (let i2 = 0; i2 < array.length; i2++) { + const j = i2 * 2; + const hexByte = hex2.slice(j, j + 2); + const byte = Number.parseInt(hexByte, 16); + if (Number.isNaN(byte) || byte < 0) + throw new Error("Invalid byte sequence"); + array[i2] = byte; + } + return array; + } + function bytesToNumberBE2(bytes4) { + return hexToNumber2(bytesToHex3(bytes4)); + } + function bytesToNumberLE2(bytes4) { + if (!u8a4(bytes4)) + throw new Error("Uint8Array expected"); + return hexToNumber2(bytesToHex3(Uint8Array.from(bytes4).reverse())); + } + function numberToBytesBE2(n, len) { + return hexToBytes3(n.toString(16).padStart(len * 2, "0")); + } + function numberToBytesLE2(n, len) { + return numberToBytesBE2(n, len).reverse(); + } + function numberToVarBytesBE2(n) { + return hexToBytes3(numberToHexUnpadded2(n)); + } + function ensureBytes2(title, hex2, expectedLength) { + let res; + if (typeof hex2 === "string") { + try { + res = hexToBytes3(hex2); + } catch (e) { + throw new Error(`${title} must be valid hex string, got "${hex2}". Cause: ${e}`); + } + } else if (u8a4(hex2)) { + res = Uint8Array.from(hex2); + } else { + throw new Error(`${title} must be hex string or Uint8Array`); + } + const len = res.length; + if (typeof expectedLength === "number" && len !== expectedLength) + throw new Error(`${title} expected ${expectedLength} bytes, got ${len}`); + return res; + } + function concatBytes4(...arrays) { + const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0)); + let pad2 = 0; + arrays.forEach((a) => { + if (!u8a4(a)) + throw new Error("Uint8Array expected"); + r.set(a, pad2); + pad2 += a.length; + }); + return r; + } + function equalBytes3(b1, b2) { + if (b1.length !== b2.length) + return false; + for (let i2 = 0; i2 < b1.length; i2++) + if (b1[i2] !== b2[i2]) + return false; + return true; + } + function utf8ToBytes5(str) { + if (typeof str !== "string") + throw new Error(`utf8ToBytes expected string, got ${typeof str}`); + return new Uint8Array(new TextEncoder().encode(str)); + } + function bitLen2(n) { + let len; + for (len = 0; n > _0n6; n >>= _1n6, len += 1) + ; + return len; + } + function bitGet2(n, pos) { + return n >> BigInt(pos) & _1n6; + } + var bitSet2 = (n, pos, value) => { + return n | (value ? _1n6 : _0n6) << BigInt(pos); + }; + var bitMask2 = (n) => (_2n5 << BigInt(n - 1)) - _1n6; + var u8n2 = (data) => new Uint8Array(data); + var u8fr2 = (arr) => Uint8Array.from(arr); + function createHmacDrbg2(hashLen, qByteLen, hmacFn) { + if (typeof hashLen !== "number" || hashLen < 2) + throw new Error("hashLen must be a number"); + if (typeof qByteLen !== "number" || qByteLen < 2) + throw new Error("qByteLen must be a number"); + if (typeof hmacFn !== "function") + throw new Error("hmacFn must be a function"); + let v = u8n2(hashLen); + let k = u8n2(hashLen); + let i2 = 0; + const reset = () => { + v.fill(1); + k.fill(0); + i2 = 0; + }; + const h = (...b) => hmacFn(k, v, ...b); + const reseed = (seed = u8n2()) => { + k = h(u8fr2([0]), seed); + v = h(); + if (seed.length === 0) + return; + k = h(u8fr2([1]), seed); + v = h(); + }; + const gen = () => { + if (i2++ >= 1e3) + throw new Error("drbg: tried 1000 values"); + let len = 0; + const out = []; + while (len < qByteLen) { + v = h(); + const sl = v.slice(); + out.push(sl); + len += v.length; + } + return concatBytes4(...out); + }; + const genUntil = (seed, pred) => { + reset(); + reseed(seed); + let res = void 0; + while (!(res = pred(gen()))) + reseed(); + reset(); + return res; + }; + return genUntil; + } + var validatorFns2 = { + bigint: (val) => typeof val === "bigint", + function: (val) => typeof val === "function", + boolean: (val) => typeof val === "boolean", + string: (val) => typeof val === "string", + isSafeInteger: (val) => Number.isSafeInteger(val), + array: (val) => Array.isArray(val), + field: (val, object) => object.Fp.isValid(val), + hash: (val) => typeof val === "function" && Number.isSafeInteger(val.outputLen) + }; + function validateObject2(object, validators, optValidators = {}) { + const checkField = (fieldName, type, isOptional) => { + const checkVal = validatorFns2[type]; + if (typeof checkVal !== "function") + throw new Error(`Invalid validator "${type}", expected function`); + const val = object[fieldName]; + if (isOptional && val === void 0) + return; + if (!checkVal(val, object)) { + throw new Error(`Invalid param ${String(fieldName)}=${val} (${typeof val}), expected ${type}`); + } + }; + for (const [fieldName, type] of Object.entries(validators)) + checkField(fieldName, type, false); + for (const [fieldName, type] of Object.entries(optValidators)) + checkField(fieldName, type, true); + return object; + } + + // node_modules/@scure/bip32/node_modules/@noble/curves/esm/abstract/modular.js + var _0n7 = BigInt(0); + var _1n7 = BigInt(1); + var _2n6 = BigInt(2); + var _3n3 = BigInt(3); + var _4n3 = BigInt(4); + var _5n2 = BigInt(5); + var _8n2 = BigInt(8); + var _9n2 = BigInt(9); + var _16n2 = BigInt(16); + function mod2(a, b) { + const result = a % b; + return result >= _0n7 ? result : b + result; + } + function pow3(num, power, modulo) { + if (modulo <= _0n7 || power < _0n7) + throw new Error("Expected power/modulo > 0"); + if (modulo === _1n7) + return _0n7; + let res = _1n7; + while (power > _0n7) { + if (power & _1n7) + res = res * num % modulo; + num = num * num % modulo; + power >>= _1n7; + } + return res; + } + function pow22(x, power, modulo) { + let res = x; + while (power-- > _0n7) { + res *= res; + res %= modulo; + } + return res; + } + function invert2(number4, modulo) { + if (number4 === _0n7 || modulo <= _0n7) { + throw new Error(`invert: expected positive integers, got n=${number4} mod=${modulo}`); + } + let a = mod2(number4, modulo); + let b = modulo; + let x = _0n7, y = _1n7, u = _1n7, v = _0n7; + while (a !== _0n7) { + const q = b / a; + const r = b % a; + const m = x - u * q; + const n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + const gcd2 = b; + if (gcd2 !== _1n7) + throw new Error("invert: does not exist"); + return mod2(x, modulo); + } + function tonelliShanks2(P) { + const legendreC = (P - _1n7) / _2n6; + let Q, S, Z; + for (Q = P - _1n7, S = 0; Q % _2n6 === _0n7; Q /= _2n6, S++) + ; + for (Z = _2n6; Z < P && pow3(Z, legendreC, P) !== P - _1n7; Z++) + ; + if (S === 1) { + const p1div4 = (P + _1n7) / _4n3; + return function tonelliFast(Fp3, n) { + const root = Fp3.pow(n, p1div4); + if (!Fp3.eql(Fp3.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + const Q1div2 = (Q + _1n7) / _2n6; + return function tonelliSlow(Fp3, n) { + if (Fp3.pow(n, legendreC) === Fp3.neg(Fp3.ONE)) + throw new Error("Cannot find square root"); + let r = S; + let g = Fp3.pow(Fp3.mul(Fp3.ONE, Z), Q); + let x = Fp3.pow(n, Q1div2); + let b = Fp3.pow(n, Q); + while (!Fp3.eql(b, Fp3.ONE)) { + if (Fp3.eql(b, Fp3.ZERO)) + return Fp3.ZERO; + let m = 1; + for (let t2 = Fp3.sqr(b); m < r; m++) { + if (Fp3.eql(t2, Fp3.ONE)) + break; + t2 = Fp3.sqr(t2); + } + const ge2 = Fp3.pow(g, _1n7 << BigInt(r - m - 1)); + g = Fp3.sqr(ge2); + x = Fp3.mul(x, ge2); + b = Fp3.mul(b, g); + r = m; + } + return x; + }; + } + function FpSqrt2(P) { + if (P % _4n3 === _3n3) { + const p1div4 = (P + _1n7) / _4n3; + return function sqrt3mod4(Fp3, n) { + const root = Fp3.pow(n, p1div4); + if (!Fp3.eql(Fp3.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + if (P % _8n2 === _5n2) { + const c1 = (P - _5n2) / _8n2; + return function sqrt5mod8(Fp3, n) { + const n2 = Fp3.mul(n, _2n6); + const v = Fp3.pow(n2, c1); + const nv = Fp3.mul(n, v); + const i2 = Fp3.mul(Fp3.mul(nv, _2n6), v); + const root = Fp3.mul(nv, Fp3.sub(i2, Fp3.ONE)); + if (!Fp3.eql(Fp3.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + if (P % _16n2 === _9n2) { + } + return tonelliShanks2(P); + } + var FIELD_FIELDS2 = [ + "create", + "isValid", + "is0", + "neg", + "inv", + "sqrt", + "sqr", + "eql", + "add", + "sub", + "mul", + "pow", + "div", + "addN", + "subN", + "mulN", + "sqrN" + ]; + function validateField2(field) { + const initial = { + ORDER: "bigint", + MASK: "bigint", + BYTES: "isSafeInteger", + BITS: "isSafeInteger" + }; + const opts = FIELD_FIELDS2.reduce((map, val) => { + map[val] = "function"; + return map; + }, initial); + return validateObject2(field, opts); + } + function FpPow2(f2, num, power) { + if (power < _0n7) + throw new Error("Expected power > 0"); + if (power === _0n7) + return f2.ONE; + if (power === _1n7) + return num; + let p = f2.ONE; + let d = num; + while (power > _0n7) { + if (power & _1n7) + p = f2.mul(p, d); + d = f2.sqr(d); + power >>= _1n7; + } + return p; + } + function FpInvertBatch2(f2, nums) { + const tmp = new Array(nums.length); + const lastMultiplied = nums.reduce((acc, num, i2) => { + if (f2.is0(num)) + return acc; + tmp[i2] = acc; + return f2.mul(acc, num); + }, f2.ONE); + const inverted = f2.inv(lastMultiplied); + nums.reduceRight((acc, num, i2) => { + if (f2.is0(num)) + return acc; + tmp[i2] = f2.mul(acc, tmp[i2]); + return f2.mul(acc, num); + }, inverted); + return tmp; + } + function nLength2(n, nBitLength) { + const _nBitLength = nBitLength !== void 0 ? nBitLength : n.toString(2).length; + const nByteLength = Math.ceil(_nBitLength / 8); + return { nBitLength: _nBitLength, nByteLength }; + } + function Field2(ORDER, bitLen3, isLE4 = false, redef = {}) { + if (ORDER <= _0n7) + throw new Error(`Expected Fp ORDER > 0, got ${ORDER}`); + const { nBitLength: BITS, nByteLength: BYTES } = nLength2(ORDER, bitLen3); + if (BYTES > 2048) + throw new Error("Field lengths over 2048 bytes are not supported"); + const sqrtP = FpSqrt2(ORDER); + const f2 = Object.freeze({ + ORDER, + BITS, + BYTES, + MASK: bitMask2(BITS), + ZERO: _0n7, + ONE: _1n7, + create: (num) => mod2(num, ORDER), + isValid: (num) => { + if (typeof num !== "bigint") + throw new Error(`Invalid field element: expected bigint, got ${typeof num}`); + return _0n7 <= num && num < ORDER; + }, + is0: (num) => num === _0n7, + isOdd: (num) => (num & _1n7) === _1n7, + neg: (num) => mod2(-num, ORDER), + eql: (lhs, rhs) => lhs === rhs, + sqr: (num) => mod2(num * num, ORDER), + add: (lhs, rhs) => mod2(lhs + rhs, ORDER), + sub: (lhs, rhs) => mod2(lhs - rhs, ORDER), + mul: (lhs, rhs) => mod2(lhs * rhs, ORDER), + pow: (num, power) => FpPow2(f2, num, power), + div: (lhs, rhs) => mod2(lhs * invert2(rhs, ORDER), ORDER), + sqrN: (num) => num * num, + addN: (lhs, rhs) => lhs + rhs, + subN: (lhs, rhs) => lhs - rhs, + mulN: (lhs, rhs) => lhs * rhs, + inv: (num) => invert2(num, ORDER), + sqrt: redef.sqrt || ((n) => sqrtP(f2, n)), + invertBatch: (lst) => FpInvertBatch2(f2, lst), + cmov: (a, b, c) => c ? b : a, + toBytes: (num) => isLE4 ? numberToBytesLE2(num, BYTES) : numberToBytesBE2(num, BYTES), + fromBytes: (bytes4) => { + if (bytes4.length !== BYTES) + throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes4.length}`); + return isLE4 ? bytesToNumberLE2(bytes4) : bytesToNumberBE2(bytes4); + } + }); + return Object.freeze(f2); + } + function hashToPrivateScalar(hash3, groupOrder, isLE4 = false) { + hash3 = ensureBytes2("privateHash", hash3); + const hashLen = hash3.length; + const minLen = nLength2(groupOrder).nByteLength + 8; + if (minLen < 24 || hashLen < minLen || hashLen > 1024) + throw new Error(`hashToPrivateScalar: expected ${minLen}-1024 bytes of input, got ${hashLen}`); + const num = isLE4 ? bytesToNumberLE2(hash3) : bytesToNumberBE2(hash3); + return mod2(num, groupOrder - _1n7) + _1n7; + } + + // node_modules/@scure/bip32/node_modules/@noble/curves/esm/abstract/curve.js + var _0n8 = BigInt(0); + var _1n8 = BigInt(1); + function wNAF2(c, bits) { + const constTimeNegate = (condition, item) => { + const neg = item.negate(); + return condition ? neg : item; + }; + const opts = (W) => { + const windows = Math.ceil(bits / W) + 1; + const windowSize = 2 ** (W - 1); + return { windows, windowSize }; + }; + return { + constTimeNegate, + unsafeLadder(elm, n) { + let p = c.ZERO; + let d = elm; + while (n > _0n8) { + if (n & _1n8) + p = p.add(d); + d = d.double(); + n >>= _1n8; + } + return p; + }, + precomputeWindow(elm, W) { + const { windows, windowSize } = opts(W); + const points = []; + let p = elm; + let base = p; + for (let window = 0; window < windows; window++) { + base = p; + points.push(base); + for (let i2 = 1; i2 < windowSize; i2++) { + base = base.add(p); + points.push(base); + } + p = base.double(); + } + return points; + }, + wNAF(W, precomputes, n) { + const { windows, windowSize } = opts(W); + let p = c.ZERO; + let f2 = c.BASE; + const mask = BigInt(2 ** W - 1); + const maxNumber = 2 ** W; + const shiftBy = BigInt(W); + for (let window = 0; window < windows; window++) { + const offset = window * windowSize; + let wbits = Number(n & mask); + n >>= shiftBy; + if (wbits > windowSize) { + wbits -= maxNumber; + n += _1n8; + } + const offset1 = offset; + const offset2 = offset + Math.abs(wbits) - 1; + const cond1 = window % 2 !== 0; + const cond2 = wbits < 0; + if (wbits === 0) { + f2 = f2.add(constTimeNegate(cond1, precomputes[offset1])); + } else { + p = p.add(constTimeNegate(cond2, precomputes[offset2])); + } + } + return { p, f: f2 }; + }, + wNAFCached(P, precomputesMap, n, transform) { + const W = P._WINDOW_SIZE || 1; + let comp = precomputesMap.get(P); + if (!comp) { + comp = this.precomputeWindow(P, W); + if (W !== 1) { + precomputesMap.set(P, transform(comp)); + } + } + return this.wNAF(W, comp, n); + } + }; + } + function validateBasic2(curve) { + validateField2(curve.Fp); + validateObject2(curve, { + n: "bigint", + h: "bigint", + Gx: "field", + Gy: "field" + }, { + nBitLength: "isSafeInteger", + nByteLength: "isSafeInteger" + }); + return Object.freeze({ + ...nLength2(curve.n, curve.nBitLength), + ...curve, + ...{ p: curve.Fp.ORDER } + }); + } + + // node_modules/@scure/bip32/node_modules/@noble/curves/esm/abstract/weierstrass.js + function validatePointOpts2(curve) { + const opts = validateBasic2(curve); + validateObject2(opts, { + a: "field", + b: "field" + }, { + allowedPrivateKeyLengths: "array", + wrapPrivateKey: "boolean", + isTorsionFree: "function", + clearCofactor: "function", + allowInfinityPoint: "boolean", + fromBytes: "function", + toBytes: "function" + }); + const { endo, Fp: Fp3, a } = opts; + if (endo) { + if (!Fp3.eql(a, Fp3.ZERO)) { + throw new Error("Endomorphism can only be defined for Koblitz curves that have a=0"); + } + if (typeof endo !== "object" || typeof endo.beta !== "bigint" || typeof endo.splitScalar !== "function") { + throw new Error("Expected endomorphism with beta: bigint and splitScalar: function"); + } + } + return Object.freeze({ ...opts }); + } + var { bytesToNumberBE: b2n2, hexToBytes: h2b2 } = utils_exports3; + var DER2 = { + Err: class DERErr2 extends Error { + constructor(m = "") { + super(m); + } + }, + _parseInt(data) { + const { Err: E } = DER2; + if (data.length < 2 || data[0] !== 2) + throw new E("Invalid signature integer tag"); + const len = data[1]; + const res = data.subarray(2, len + 2); + if (!len || res.length !== len) + throw new E("Invalid signature integer: wrong length"); + if (res[0] & 128) + throw new E("Invalid signature integer: negative"); + if (res[0] === 0 && !(res[1] & 128)) + throw new E("Invalid signature integer: unnecessary leading zero"); + return { d: b2n2(res), l: data.subarray(len + 2) }; + }, + toSig(hex2) { + const { Err: E } = DER2; + const data = typeof hex2 === "string" ? h2b2(hex2) : hex2; + if (!(data instanceof Uint8Array)) + throw new Error("ui8a expected"); + let l = data.length; + if (l < 2 || data[0] != 48) + throw new E("Invalid signature tag"); + if (data[1] !== l - 2) + throw new E("Invalid signature: incorrect length"); + const { d: r, l: sBytes } = DER2._parseInt(data.subarray(2)); + const { d: s, l: rBytesLeft } = DER2._parseInt(sBytes); + if (rBytesLeft.length) + throw new E("Invalid signature: left bytes after parsing"); + return { r, s }; + }, + hexFromSig(sig) { + const slice = (s2) => Number.parseInt(s2[0], 16) & 8 ? "00" + s2 : s2; + const h = (num) => { + const hex2 = num.toString(16); + return hex2.length & 1 ? `0${hex2}` : hex2; + }; + const s = slice(h(sig.s)); + const r = slice(h(sig.r)); + const shl = s.length / 2; + const rhl = r.length / 2; + const sl = h(shl); + const rl = h(rhl); + return `30${h(rhl + shl + 4)}02${rl}${r}02${sl}${s}`; + } + }; + var _0n9 = BigInt(0); + var _1n9 = BigInt(1); + var _2n7 = BigInt(2); + var _3n4 = BigInt(3); + var _4n4 = BigInt(4); + function weierstrassPoints2(opts) { + const CURVE = validatePointOpts2(opts); + const { Fp: Fp3 } = CURVE; + const toBytes4 = CURVE.toBytes || ((c, point, isCompressed) => { + const a = point.toAffine(); + return concatBytes4(Uint8Array.from([4]), Fp3.toBytes(a.x), Fp3.toBytes(a.y)); + }); + const fromBytes = CURVE.fromBytes || ((bytes4) => { + const tail = bytes4.subarray(1); + const x = Fp3.fromBytes(tail.subarray(0, Fp3.BYTES)); + const y = Fp3.fromBytes(tail.subarray(Fp3.BYTES, 2 * Fp3.BYTES)); + return { x, y }; + }); + function weierstrassEquation(x) { + const { a, b } = CURVE; + const x2 = Fp3.sqr(x); + const x3 = Fp3.mul(x2, x); + return Fp3.add(Fp3.add(x3, Fp3.mul(x, a)), b); + } + if (!Fp3.eql(Fp3.sqr(CURVE.Gy), weierstrassEquation(CURVE.Gx))) + throw new Error("bad generator point: equation left != right"); + function isWithinCurveOrder(num) { + return typeof num === "bigint" && _0n9 < num && num < CURVE.n; + } + function assertGE(num) { + if (!isWithinCurveOrder(num)) + throw new Error("Expected valid bigint: 0 < bigint < curve.n"); + } + function normPrivateKeyToScalar(key) { + const { allowedPrivateKeyLengths: lengths, nByteLength, wrapPrivateKey, n } = CURVE; + if (lengths && typeof key !== "bigint") { + if (key instanceof Uint8Array) + key = bytesToHex3(key); + if (typeof key !== "string" || !lengths.includes(key.length)) + throw new Error("Invalid key"); + key = key.padStart(nByteLength * 2, "0"); + } + let num; + try { + num = typeof key === "bigint" ? key : bytesToNumberBE2(ensureBytes2("private key", key, nByteLength)); + } catch (error) { + throw new Error(`private key must be ${nByteLength} bytes, hex or bigint, not ${typeof key}`); + } + if (wrapPrivateKey) + num = mod2(num, n); + assertGE(num); + return num; + } + const pointPrecomputes = /* @__PURE__ */ new Map(); + function assertPrjPoint(other) { + if (!(other instanceof Point4)) + throw new Error("ProjectivePoint expected"); + } + class Point4 { + constructor(px, py, pz) { + this.px = px; + this.py = py; + this.pz = pz; + if (px == null || !Fp3.isValid(px)) + throw new Error("x required"); + if (py == null || !Fp3.isValid(py)) + throw new Error("y required"); + if (pz == null || !Fp3.isValid(pz)) + throw new Error("z required"); + } + static fromAffine(p) { + const { x, y } = p || {}; + if (!p || !Fp3.isValid(x) || !Fp3.isValid(y)) + throw new Error("invalid affine point"); + if (p instanceof Point4) + throw new Error("projective point not allowed"); + const is0 = (i2) => Fp3.eql(i2, Fp3.ZERO); + if (is0(x) && is0(y)) + return Point4.ZERO; + return new Point4(x, y, Fp3.ONE); + } + get x() { + return this.toAffine().x; + } + get y() { + return this.toAffine().y; + } + static normalizeZ(points) { + const toInv = Fp3.invertBatch(points.map((p) => p.pz)); + return points.map((p, i2) => p.toAffine(toInv[i2])).map(Point4.fromAffine); + } + static fromHex(hex2) { + const P = Point4.fromAffine(fromBytes(ensureBytes2("pointHex", hex2))); + P.assertValidity(); + return P; + } + static fromPrivateKey(privateKey) { + return Point4.BASE.multiply(normPrivateKeyToScalar(privateKey)); + } + _setWindowSize(windowSize) { + this._WINDOW_SIZE = windowSize; + pointPrecomputes.delete(this); + } + assertValidity() { + if (this.is0()) { + if (CURVE.allowInfinityPoint) + return; + throw new Error("bad point: ZERO"); + } + const { x, y } = this.toAffine(); + if (!Fp3.isValid(x) || !Fp3.isValid(y)) + throw new Error("bad point: x or y not FE"); + const left = Fp3.sqr(y); + const right = weierstrassEquation(x); + if (!Fp3.eql(left, right)) + throw new Error("bad point: equation left != right"); + if (!this.isTorsionFree()) + throw new Error("bad point: not in prime-order subgroup"); + } + hasEvenY() { + const { y } = this.toAffine(); + if (Fp3.isOdd) + return !Fp3.isOdd(y); + throw new Error("Field doesn't support isOdd"); + } + equals(other) { + assertPrjPoint(other); + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = other; + const U1 = Fp3.eql(Fp3.mul(X1, Z2), Fp3.mul(X2, Z1)); + const U2 = Fp3.eql(Fp3.mul(Y1, Z2), Fp3.mul(Y2, Z1)); + return U1 && U2; + } + negate() { + return new Point4(this.px, Fp3.neg(this.py), this.pz); + } + double() { + const { a, b } = CURVE; + const b3 = Fp3.mul(b, _3n4); + const { px: X1, py: Y1, pz: Z1 } = this; + let X3 = Fp3.ZERO, Y3 = Fp3.ZERO, Z3 = Fp3.ZERO; + let t0 = Fp3.mul(X1, X1); + let t1 = Fp3.mul(Y1, Y1); + let t2 = Fp3.mul(Z1, Z1); + let t3 = Fp3.mul(X1, Y1); + t3 = Fp3.add(t3, t3); + Z3 = Fp3.mul(X1, Z1); + Z3 = Fp3.add(Z3, Z3); + X3 = Fp3.mul(a, Z3); + Y3 = Fp3.mul(b3, t2); + Y3 = Fp3.add(X3, Y3); + X3 = Fp3.sub(t1, Y3); + Y3 = Fp3.add(t1, Y3); + Y3 = Fp3.mul(X3, Y3); + X3 = Fp3.mul(t3, X3); + Z3 = Fp3.mul(b3, Z3); + t2 = Fp3.mul(a, t2); + t3 = Fp3.sub(t0, t2); + t3 = Fp3.mul(a, t3); + t3 = Fp3.add(t3, Z3); + Z3 = Fp3.add(t0, t0); + t0 = Fp3.add(Z3, t0); + t0 = Fp3.add(t0, t2); + t0 = Fp3.mul(t0, t3); + Y3 = Fp3.add(Y3, t0); + t2 = Fp3.mul(Y1, Z1); + t2 = Fp3.add(t2, t2); + t0 = Fp3.mul(t2, t3); + X3 = Fp3.sub(X3, t0); + Z3 = Fp3.mul(t2, t1); + Z3 = Fp3.add(Z3, Z3); + Z3 = Fp3.add(Z3, Z3); + return new Point4(X3, Y3, Z3); + } + add(other) { + assertPrjPoint(other); + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = other; + let X3 = Fp3.ZERO, Y3 = Fp3.ZERO, Z3 = Fp3.ZERO; + const a = CURVE.a; + const b3 = Fp3.mul(CURVE.b, _3n4); + let t0 = Fp3.mul(X1, X2); + let t1 = Fp3.mul(Y1, Y2); + let t2 = Fp3.mul(Z1, Z2); + let t3 = Fp3.add(X1, Y1); + let t4 = Fp3.add(X2, Y2); + t3 = Fp3.mul(t3, t4); + t4 = Fp3.add(t0, t1); + t3 = Fp3.sub(t3, t4); + t4 = Fp3.add(X1, Z1); + let t5 = Fp3.add(X2, Z2); + t4 = Fp3.mul(t4, t5); + t5 = Fp3.add(t0, t2); + t4 = Fp3.sub(t4, t5); + t5 = Fp3.add(Y1, Z1); + X3 = Fp3.add(Y2, Z2); + t5 = Fp3.mul(t5, X3); + X3 = Fp3.add(t1, t2); + t5 = Fp3.sub(t5, X3); + Z3 = Fp3.mul(a, t4); + X3 = Fp3.mul(b3, t2); + Z3 = Fp3.add(X3, Z3); + X3 = Fp3.sub(t1, Z3); + Z3 = Fp3.add(t1, Z3); + Y3 = Fp3.mul(X3, Z3); + t1 = Fp3.add(t0, t0); + t1 = Fp3.add(t1, t0); + t2 = Fp3.mul(a, t2); + t4 = Fp3.mul(b3, t4); + t1 = Fp3.add(t1, t2); + t2 = Fp3.sub(t0, t2); + t2 = Fp3.mul(a, t2); + t4 = Fp3.add(t4, t2); + t0 = Fp3.mul(t1, t4); + Y3 = Fp3.add(Y3, t0); + t0 = Fp3.mul(t5, t4); + X3 = Fp3.mul(t3, X3); + X3 = Fp3.sub(X3, t0); + t0 = Fp3.mul(t3, t1); + Z3 = Fp3.mul(t5, Z3); + Z3 = Fp3.add(Z3, t0); + return new Point4(X3, Y3, Z3); + } + subtract(other) { + return this.add(other.negate()); + } + is0() { + return this.equals(Point4.ZERO); + } + wNAF(n) { + return wnaf.wNAFCached(this, pointPrecomputes, n, (comp) => { + const toInv = Fp3.invertBatch(comp.map((p) => p.pz)); + return comp.map((p, i2) => p.toAffine(toInv[i2])).map(Point4.fromAffine); + }); + } + multiplyUnsafe(n) { + const I = Point4.ZERO; + if (n === _0n9) + return I; + assertGE(n); + if (n === _1n9) + return this; + const { endo } = CURVE; + if (!endo) + return wnaf.unsafeLadder(this, n); + let { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); + let k1p = I; + let k2p = I; + let d = this; + while (k1 > _0n9 || k2 > _0n9) { + if (k1 & _1n9) + k1p = k1p.add(d); + if (k2 & _1n9) + k2p = k2p.add(d); + d = d.double(); + k1 >>= _1n9; + k2 >>= _1n9; + } + if (k1neg) + k1p = k1p.negate(); + if (k2neg) + k2p = k2p.negate(); + k2p = new Point4(Fp3.mul(k2p.px, endo.beta), k2p.py, k2p.pz); + return k1p.add(k2p); + } + multiply(scalar) { + assertGE(scalar); + let n = scalar; + let point, fake; + const { endo } = CURVE; + if (endo) { + const { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); + let { p: k1p, f: f1p } = this.wNAF(k1); + let { p: k2p, f: f2p } = this.wNAF(k2); + k1p = wnaf.constTimeNegate(k1neg, k1p); + k2p = wnaf.constTimeNegate(k2neg, k2p); + k2p = new Point4(Fp3.mul(k2p.px, endo.beta), k2p.py, k2p.pz); + point = k1p.add(k2p); + fake = f1p.add(f2p); + } else { + const { p, f: f2 } = this.wNAF(n); + point = p; + fake = f2; + } + return Point4.normalizeZ([point, fake])[0]; + } + multiplyAndAddUnsafe(Q, a, b) { + const G = Point4.BASE; + const mul3 = (P, a2) => a2 === _0n9 || a2 === _1n9 || !P.equals(G) ? P.multiplyUnsafe(a2) : P.multiply(a2); + const sum = mul3(this, a).add(mul3(Q, b)); + return sum.is0() ? void 0 : sum; + } + toAffine(iz) { + const { px: x, py: y, pz: z } = this; + const is0 = this.is0(); + if (iz == null) + iz = is0 ? Fp3.ONE : Fp3.inv(z); + const ax = Fp3.mul(x, iz); + const ay = Fp3.mul(y, iz); + const zz = Fp3.mul(z, iz); + if (is0) + return { x: Fp3.ZERO, y: Fp3.ZERO }; + if (!Fp3.eql(zz, Fp3.ONE)) + throw new Error("invZ was invalid"); + return { x: ax, y: ay }; + } + isTorsionFree() { + const { h: cofactor, isTorsionFree } = CURVE; + if (cofactor === _1n9) + return true; + if (isTorsionFree) + return isTorsionFree(Point4, this); + throw new Error("isTorsionFree() has not been declared for the elliptic curve"); + } + clearCofactor() { + const { h: cofactor, clearCofactor } = CURVE; + if (cofactor === _1n9) + return this; + if (clearCofactor) + return clearCofactor(Point4, this); + return this.multiplyUnsafe(CURVE.h); + } + toRawBytes(isCompressed = true) { + this.assertValidity(); + return toBytes4(Point4, this, isCompressed); + } + toHex(isCompressed = true) { + return bytesToHex3(this.toRawBytes(isCompressed)); + } + } + Point4.BASE = new Point4(CURVE.Gx, CURVE.Gy, Fp3.ONE); + Point4.ZERO = new Point4(Fp3.ZERO, Fp3.ONE, Fp3.ZERO); + const _bits = CURVE.nBitLength; + const wnaf = wNAF2(Point4, CURVE.endo ? Math.ceil(_bits / 2) : _bits); + return { + CURVE, + ProjectivePoint: Point4, + normPrivateKeyToScalar, + weierstrassEquation, + isWithinCurveOrder + }; + } + function validateOpts2(curve) { + const opts = validateBasic2(curve); + validateObject2(opts, { + hash: "hash", + hmac: "function", + randomBytes: "function" + }, { + bits2int: "function", + bits2int_modN: "function", + lowS: "boolean" + }); + return Object.freeze({ lowS: true, ...opts }); + } + function weierstrass2(curveDef) { + const CURVE = validateOpts2(curveDef); + const { Fp: Fp3, n: CURVE_ORDER } = CURVE; + const compressedLen = Fp3.BYTES + 1; + const uncompressedLen = 2 * Fp3.BYTES + 1; + function isValidFieldElement(num) { + return _0n9 < num && num < Fp3.ORDER; + } + function modN2(a) { + return mod2(a, CURVE_ORDER); + } + function invN(a) { + return invert2(a, CURVE_ORDER); + } + const { ProjectivePoint: Point4, normPrivateKeyToScalar, weierstrassEquation, isWithinCurveOrder } = weierstrassPoints2({ + ...CURVE, + toBytes(c, point, isCompressed) { + const a = point.toAffine(); + const x = Fp3.toBytes(a.x); + const cat = concatBytes4; + if (isCompressed) { + return cat(Uint8Array.from([point.hasEvenY() ? 2 : 3]), x); + } else { + return cat(Uint8Array.from([4]), x, Fp3.toBytes(a.y)); + } + }, + fromBytes(bytes4) { + const len = bytes4.length; + const head = bytes4[0]; + const tail = bytes4.subarray(1); + if (len === compressedLen && (head === 2 || head === 3)) { + const x = bytesToNumberBE2(tail); + if (!isValidFieldElement(x)) + throw new Error("Point is not on curve"); + const y2 = weierstrassEquation(x); + let y = Fp3.sqrt(y2); + const isYOdd = (y & _1n9) === _1n9; + const isHeadOdd = (head & 1) === 1; + if (isHeadOdd !== isYOdd) + y = Fp3.neg(y); + return { x, y }; + } else if (len === uncompressedLen && head === 4) { + const x = Fp3.fromBytes(tail.subarray(0, Fp3.BYTES)); + const y = Fp3.fromBytes(tail.subarray(Fp3.BYTES, 2 * Fp3.BYTES)); + return { x, y }; + } else { + throw new Error(`Point of length ${len} was invalid. Expected ${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes`); + } + } + }); + const numToNByteStr = (num) => bytesToHex3(numberToBytesBE2(num, CURVE.nByteLength)); + function isBiggerThanHalfOrder(number4) { + const HALF = CURVE_ORDER >> _1n9; + return number4 > HALF; + } + function normalizeS(s) { + return isBiggerThanHalfOrder(s) ? modN2(-s) : s; + } + const slcNum = (b, from, to) => bytesToNumberBE2(b.slice(from, to)); + class Signature { + constructor(r, s, recovery) { + this.r = r; + this.s = s; + this.recovery = recovery; + this.assertValidity(); + } + static fromCompact(hex2) { + const l = CURVE.nByteLength; + hex2 = ensureBytes2("compactSignature", hex2, l * 2); + return new Signature(slcNum(hex2, 0, l), slcNum(hex2, l, 2 * l)); + } + static fromDER(hex2) { + const { r, s } = DER2.toSig(ensureBytes2("DER", hex2)); + return new Signature(r, s); + } + assertValidity() { + if (!isWithinCurveOrder(this.r)) + throw new Error("r must be 0 < r < CURVE.n"); + if (!isWithinCurveOrder(this.s)) + throw new Error("s must be 0 < s < CURVE.n"); + } + addRecoveryBit(recovery) { + return new Signature(this.r, this.s, recovery); + } + recoverPublicKey(msgHash) { + const { r, s, recovery: rec } = this; + const h = bits2int_modN(ensureBytes2("msgHash", msgHash)); + if (rec == null || ![0, 1, 2, 3].includes(rec)) + throw new Error("recovery id invalid"); + const radj = rec === 2 || rec === 3 ? r + CURVE.n : r; + if (radj >= Fp3.ORDER) + throw new Error("recovery id 2 or 3 invalid"); + const prefix = (rec & 1) === 0 ? "02" : "03"; + const R = Point4.fromHex(prefix + numToNByteStr(radj)); + const ir = invN(radj); + const u1 = modN2(-h * ir); + const u2 = modN2(s * ir); + const Q = Point4.BASE.multiplyAndAddUnsafe(R, u1, u2); + if (!Q) + throw new Error("point at infinify"); + Q.assertValidity(); + return Q; + } + hasHighS() { + return isBiggerThanHalfOrder(this.s); + } + normalizeS() { + return this.hasHighS() ? new Signature(this.r, modN2(-this.s), this.recovery) : this; + } + toDERRawBytes() { + return hexToBytes3(this.toDERHex()); + } + toDERHex() { + return DER2.hexFromSig({ r: this.r, s: this.s }); + } + toCompactRawBytes() { + return hexToBytes3(this.toCompactHex()); + } + toCompactHex() { + return numToNByteStr(this.r) + numToNByteStr(this.s); + } + } + const utils2 = { + isValidPrivateKey(privateKey) { + try { + normPrivateKeyToScalar(privateKey); + return true; + } catch (error) { + return false; + } + }, + normPrivateKeyToScalar, + randomPrivateKey: () => { + const rand = CURVE.randomBytes(Fp3.BYTES + 8); + const num = hashToPrivateScalar(rand, CURVE_ORDER); + return numberToBytesBE2(num, CURVE.nByteLength); + }, + precompute(windowSize = 8, point = Point4.BASE) { + point._setWindowSize(windowSize); + point.multiply(BigInt(3)); + return point; + } + }; + function getPublicKey2(privateKey, isCompressed = true) { + return Point4.fromPrivateKey(privateKey).toRawBytes(isCompressed); + } + function isProbPub(item) { + const arr = item instanceof Uint8Array; + const str = typeof item === "string"; + const len = (arr || str) && item.length; + if (arr) + return len === compressedLen || len === uncompressedLen; + if (str) + return len === 2 * compressedLen || len === 2 * uncompressedLen; + if (item instanceof Point4) + return true; + return false; + } + function getSharedSecret(privateA, publicB, isCompressed = true) { + if (isProbPub(privateA)) + throw new Error("first arg must be private key"); + if (!isProbPub(publicB)) + throw new Error("second arg must be public key"); + const b = Point4.fromHex(publicB); + return b.multiply(normPrivateKeyToScalar(privateA)).toRawBytes(isCompressed); + } + const bits2int = CURVE.bits2int || function(bytes4) { + const num = bytesToNumberBE2(bytes4); + const delta = bytes4.length * 8 - CURVE.nBitLength; + return delta > 0 ? num >> BigInt(delta) : num; + }; + const bits2int_modN = CURVE.bits2int_modN || function(bytes4) { + return modN2(bits2int(bytes4)); + }; + const ORDER_MASK = bitMask2(CURVE.nBitLength); + function int2octets(num) { + if (typeof num !== "bigint") + throw new Error("bigint expected"); + if (!(_0n9 <= num && num < ORDER_MASK)) + throw new Error(`bigint expected < 2^${CURVE.nBitLength}`); + return numberToBytesBE2(num, CURVE.nByteLength); + } + function prepSig(msgHash, privateKey, opts = defaultSigOpts) { + if (["recovered", "canonical"].some((k) => k in opts)) + throw new Error("sign() legacy options not supported"); + const { hash: hash3, randomBytes: randomBytes3 } = CURVE; + let { lowS, prehash, extraEntropy: ent } = opts; + if (lowS == null) + lowS = true; + msgHash = ensureBytes2("msgHash", msgHash); + if (prehash) + msgHash = ensureBytes2("prehashed msgHash", hash3(msgHash)); + const h1int = bits2int_modN(msgHash); + const d = normPrivateKeyToScalar(privateKey); + const seedArgs = [int2octets(d), int2octets(h1int)]; + if (ent != null) { + const e = ent === true ? randomBytes3(Fp3.BYTES) : ent; + seedArgs.push(ensureBytes2("extraEntropy", e, Fp3.BYTES)); + } + const seed = concatBytes4(...seedArgs); + const m = h1int; + function k2sig(kBytes) { + const k = bits2int(kBytes); + if (!isWithinCurveOrder(k)) + return; + const ik = invN(k); + const q = Point4.BASE.multiply(k).toAffine(); + const r = modN2(q.x); + if (r === _0n9) + return; + const s = modN2(ik * modN2(m + r * d)); + if (s === _0n9) + return; + let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n9); + let normS = s; + if (lowS && isBiggerThanHalfOrder(s)) { + normS = normalizeS(s); + recovery ^= 1; + } + return new Signature(r, normS, recovery); + } + return { seed, k2sig }; + } + const defaultSigOpts = { lowS: CURVE.lowS, prehash: false }; + const defaultVerOpts = { lowS: CURVE.lowS, prehash: false }; + function sign(msgHash, privKey, opts = defaultSigOpts) { + const { seed, k2sig } = prepSig(msgHash, privKey, opts); + const C = CURVE; + const drbg = createHmacDrbg2(C.hash.outputLen, C.nByteLength, C.hmac); + return drbg(seed, k2sig); + } + Point4.BASE._setWindowSize(8); + function verify(signature, msgHash, publicKey, opts = defaultVerOpts) { + const sg = signature; + msgHash = ensureBytes2("msgHash", msgHash); + publicKey = ensureBytes2("publicKey", publicKey); + if ("strict" in opts) + throw new Error("options.strict was renamed to lowS"); + const { lowS, prehash } = opts; + let _sig = void 0; + let P; + try { + if (typeof sg === "string" || sg instanceof Uint8Array) { + try { + _sig = Signature.fromDER(sg); + } catch (derError) { + if (!(derError instanceof DER2.Err)) + throw derError; + _sig = Signature.fromCompact(sg); + } + } else if (typeof sg === "object" && typeof sg.r === "bigint" && typeof sg.s === "bigint") { + const { r: r2, s: s2 } = sg; + _sig = new Signature(r2, s2); + } else { + throw new Error("PARSE"); + } + P = Point4.fromHex(publicKey); + } catch (error) { + if (error.message === "PARSE") + throw new Error(`signature must be Signature instance, Uint8Array or hex string`); + return false; + } + if (lowS && _sig.hasHighS()) + return false; + if (prehash) + msgHash = CURVE.hash(msgHash); + const { r, s } = _sig; + const h = bits2int_modN(msgHash); + const is = invN(s); + const u1 = modN2(h * is); + const u2 = modN2(r * is); + const R = Point4.BASE.multiplyAndAddUnsafe(P, u1, u2)?.toAffine(); + if (!R) + return false; + const v = modN2(R.x); + return v === r; + } + return { + CURVE, + getPublicKey: getPublicKey2, + getSharedSecret, + sign, + verify, + ProjectivePoint: Point4, + Signature, + utils: utils2 + }; + } + + // node_modules/@scure/bip32/node_modules/@noble/curves/esm/_shortw_utils.js + function getHash2(hash3) { + return { + hash: hash3, + hmac: (key, ...msgs) => hmac2(hash3, key, concatBytes3(...msgs)), + randomBytes: randomBytes2 + }; + } + function createCurve2(curveDef, defHash) { + const create = (hash3) => weierstrass2({ ...curveDef, ...getHash2(hash3) }); + return Object.freeze({ ...create(defHash), create }); + } + + // node_modules/@scure/bip32/node_modules/@noble/curves/esm/secp256k1.js + var secp256k1P2 = BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); + var secp256k1N2 = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + var _1n10 = BigInt(1); + var _2n8 = BigInt(2); + var divNearest2 = (a, b) => (a + b / _2n8) / b; + function sqrtMod2(y) { + const P = secp256k1P2; + const _3n5 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22); + const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88); + const b2 = y * y * y % P; + const b3 = b2 * b2 * y % P; + const b6 = pow22(b3, _3n5, P) * b3 % P; + const b9 = pow22(b6, _3n5, P) * b3 % P; + const b11 = pow22(b9, _2n8, P) * b2 % P; + const b22 = pow22(b11, _11n, P) * b11 % P; + const b44 = pow22(b22, _22n, P) * b22 % P; + const b88 = pow22(b44, _44n, P) * b44 % P; + const b176 = pow22(b88, _88n, P) * b88 % P; + const b220 = pow22(b176, _44n, P) * b44 % P; + const b223 = pow22(b220, _3n5, P) * b3 % P; + const t1 = pow22(b223, _23n, P) * b22 % P; + const t2 = pow22(t1, _6n, P) * b2 % P; + const root = pow22(t2, _2n8, P); + if (!Fp2.eql(Fp2.sqr(root), y)) + throw new Error("Cannot find square root"); + return root; + } + var Fp2 = Field2(secp256k1P2, void 0, void 0, { sqrt: sqrtMod2 }); + var secp256k12 = createCurve2({ + a: BigInt(0), + b: BigInt(7), + Fp: Fp2, + n: secp256k1N2, + Gx: BigInt("55066263022277343669578718895168534326250603453777594175500187360389116729240"), + Gy: BigInt("32670510020758816978083085130507043184471273380659243275938904335757337482424"), + h: BigInt(1), + lowS: true, + endo: { + beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"), + splitScalar: (k) => { + const n = secp256k1N2; + const a1 = BigInt("0x3086d221a7d46bcde86c90e49284eb15"); + const b1 = -_1n10 * BigInt("0xe4437ed6010e88286f547fa90abfe4c3"); + const a2 = BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"); + const b2 = a1; + const POW_2_128 = BigInt("0x100000000000000000000000000000000"); + const c1 = divNearest2(b2 * k, n); + const c2 = divNearest2(-b1 * k, n); + let k1 = mod2(k - c1 * a1 - c2 * a2, n); + let k2 = mod2(-c1 * b1 - c2 * b2, n); + const k1neg = k1 > POW_2_128; + const k2neg = k2 > POW_2_128; + if (k1neg) + k1 = n - k1; + if (k2neg) + k2 = n - k2; + if (k1 > POW_2_128 || k2 > POW_2_128) { + throw new Error("splitScalar: Endomorphism failed, k=" + k); + } + return { k1neg, k1, k2neg, k2 }; + } + } + }, sha2562); + var _0n10 = BigInt(0); + var Point2 = secp256k12.ProjectivePoint; + + // node_modules/@scure/bip32/lib/esm/index.js + var Point3 = secp256k12.ProjectivePoint; + var base58check2 = base58check(sha2562); + function bytesToNumber(bytes4) { + return BigInt(`0x${bytesToHex2(bytes4)}`); + } + function numberToBytes(num) { + return hexToBytes2(num.toString(16).padStart(64, "0")); + } + var MASTER_SECRET = utf8ToBytes3("Bitcoin seed"); + var BITCOIN_VERSIONS = { private: 76066276, public: 76067358 }; + var HARDENED_OFFSET = 2147483648; + var hash160 = (data) => ripemd160(sha2562(data)); + var fromU32 = (data) => createView2(data).getUint32(0, false); + var toU32 = (n) => { + if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) { + throw new Error(`Invalid number=${n}. Should be from 0 to 2 ** 32 - 1`); + } + const buf = new Uint8Array(4); + createView2(buf).setUint32(0, n, false); + return buf; + }; + var HDKey = class { + get fingerprint() { + if (!this.pubHash) { + throw new Error("No publicKey set!"); + } + return fromU32(this.pubHash); + } + get identifier() { + return this.pubHash; + } + get pubKeyHash() { + return this.pubHash; + } + get privateKey() { + return this.privKeyBytes || null; + } + get publicKey() { + return this.pubKey || null; + } + get privateExtendedKey() { + const priv = this.privateKey; + if (!priv) { + throw new Error("No private key"); + } + return base58check2.encode(this.serialize(this.versions.private, concatBytes3(new Uint8Array([0]), priv))); + } + get publicExtendedKey() { + if (!this.pubKey) { + throw new Error("No public key"); + } + return base58check2.encode(this.serialize(this.versions.public, this.pubKey)); + } + static fromMasterSeed(seed, versions = BITCOIN_VERSIONS) { + bytes2(seed); + if (8 * seed.length < 128 || 8 * seed.length > 512) { + throw new Error(`HDKey: wrong seed length=${seed.length}. Should be between 128 and 512 bits; 256 bits is advised)`); + } + const I = hmac2(sha512, MASTER_SECRET, seed); + return new HDKey({ + versions, + chainCode: I.slice(32), + privateKey: I.slice(0, 32) + }); + } + static fromExtendedKey(base58key, versions = BITCOIN_VERSIONS) { + const keyBuffer = base58check2.decode(base58key); + const keyView = createView2(keyBuffer); + const version = keyView.getUint32(0, false); + const opt = { + versions, + depth: keyBuffer[4], + parentFingerprint: keyView.getUint32(5, false), + index: keyView.getUint32(9, false), + chainCode: keyBuffer.slice(13, 45) + }; + const key = keyBuffer.slice(45); + const isPriv = key[0] === 0; + if (version !== versions[isPriv ? "private" : "public"]) { + throw new Error("Version mismatch"); + } + if (isPriv) { + return new HDKey({ ...opt, privateKey: key.slice(1) }); + } else { + return new HDKey({ ...opt, publicKey: key }); + } + } + static fromJSON(json) { + return HDKey.fromExtendedKey(json.xpriv); + } + constructor(opt) { + this.depth = 0; + this.index = 0; + this.chainCode = null; + this.parentFingerprint = 0; + if (!opt || typeof opt !== "object") { + throw new Error("HDKey.constructor must not be called directly"); + } + this.versions = opt.versions || BITCOIN_VERSIONS; + this.depth = opt.depth || 0; + this.chainCode = opt.chainCode; + this.index = opt.index || 0; + this.parentFingerprint = opt.parentFingerprint || 0; + if (!this.depth) { + if (this.parentFingerprint || this.index) { + throw new Error("HDKey: zero depth with non-zero index/parent fingerprint"); + } + } + if (opt.publicKey && opt.privateKey) { + throw new Error("HDKey: publicKey and privateKey at same time."); + } + if (opt.privateKey) { + if (!secp256k12.utils.isValidPrivateKey(opt.privateKey)) { + throw new Error("Invalid private key"); + } + this.privKey = typeof opt.privateKey === "bigint" ? opt.privateKey : bytesToNumber(opt.privateKey); + this.privKeyBytes = numberToBytes(this.privKey); + this.pubKey = secp256k12.getPublicKey(opt.privateKey, true); + } else if (opt.publicKey) { + this.pubKey = Point3.fromHex(opt.publicKey).toRawBytes(true); + } else { + throw new Error("HDKey: no public or private key provided"); + } + this.pubHash = hash160(this.pubKey); + } + derive(path) { + if (!/^[mM]'?/.test(path)) { + throw new Error('Path must start with "m" or "M"'); + } + if (/^[mM]'?$/.test(path)) { + return this; + } + const parts = path.replace(/^[mM]'?\//, "").split("/"); + let child = this; + for (const c of parts) { + const m = /^(\d+)('?)$/.exec(c); + if (!m || m.length !== 3) { + throw new Error(`Invalid child index: ${c}`); + } + let idx = +m[1]; + if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) { + throw new Error("Invalid index"); + } + if (m[2] === "'") { + idx += HARDENED_OFFSET; + } + child = child.deriveChild(idx); + } + return child; + } + deriveChild(index) { + if (!this.pubKey || !this.chainCode) { + throw new Error("No publicKey or chainCode set"); + } + let data = toU32(index); + if (index >= HARDENED_OFFSET) { + const priv = this.privateKey; + if (!priv) { + throw new Error("Could not derive hardened child key"); + } + data = concatBytes3(new Uint8Array([0]), priv, data); + } else { + data = concatBytes3(this.pubKey, data); + } + const I = hmac2(sha512, this.chainCode, data); + const childTweak = bytesToNumber(I.slice(0, 32)); + const chainCode = I.slice(32); + if (!secp256k12.utils.isValidPrivateKey(childTweak)) { + throw new Error("Tweak bigger than curve order"); + } + const opt = { + versions: this.versions, + chainCode, + depth: this.depth + 1, + parentFingerprint: this.fingerprint, + index + }; + try { + if (this.privateKey) { + const added = mod2(this.privKey + childTweak, secp256k12.CURVE.n); + if (!secp256k12.utils.isValidPrivateKey(added)) { + throw new Error("The tweak was out of range or the resulted private key is invalid"); + } + opt.privateKey = added; + } else { + const added = Point3.fromHex(this.pubKey).add(Point3.fromPrivateKey(childTweak)); + if (added.equals(Point3.ZERO)) { + throw new Error("The tweak was equal to negative P, which made the result key invalid"); + } + opt.publicKey = added.toRawBytes(true); + } + return new HDKey(opt); + } catch (err) { + return this.deriveChild(index + 1); + } + } + sign(hash3) { + if (!this.privateKey) { + throw new Error("No privateKey set!"); + } + bytes2(hash3, 32); + return secp256k12.sign(hash3, this.privKey).toCompactRawBytes(); + } + verify(hash3, signature) { + bytes2(hash3, 32); + bytes2(signature, 64); + if (!this.publicKey) { + throw new Error("No publicKey set!"); + } + let sig; + try { + sig = secp256k12.Signature.fromCompact(signature); + } catch (error) { + return false; + } + return secp256k12.verify(sig, hash3, this.publicKey); + } + wipePrivateData() { + this.privKey = void 0; + if (this.privKeyBytes) { + this.privKeyBytes.fill(0); + this.privKeyBytes = void 0; + } + return this; + } + toJSON() { + return { + xpriv: this.privateExtendedKey, + xpub: this.publicExtendedKey + }; + } + serialize(version, key) { + if (!this.chainCode) { + throw new Error("No chainCode set"); + } + bytes2(key, 33); + return concatBytes3(toU32(version), new Uint8Array([this.depth]), toU32(this.parentFingerprint), toU32(this.index), this.chainCode, key); + } + }; + + // nip06.ts + var DERIVATION_PATH = `m/44'/1237'`; + function privateKeyFromSeedWords(mnemonic, passphrase, accountIndex = 0) { + let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)); + let privateKey = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`).privateKey; + if (!privateKey) + throw new Error("could not derive private key"); + return privateKey; + } + function accountFromSeedWords(mnemonic, passphrase, accountIndex = 0) { + const root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)); + const seed = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`); + const publicKey = bytesToHex2(seed.publicKey.slice(1)); + const privateKey = seed.privateKey; + if (!privateKey || !publicKey) { + throw new Error("could not derive key pair"); + } + return { privateKey, publicKey }; + } + function extendedKeysFromSeedWords(mnemonic, passphrase, extendedAccountIndex = 0) { + let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)); + let seed = root.derive(`${DERIVATION_PATH}/${extendedAccountIndex}'`); + let privateExtendedKey = seed.privateExtendedKey; + let publicExtendedKey = seed.publicExtendedKey; + if (!privateExtendedKey && !publicExtendedKey) + throw new Error("could not derive extended key pair"); + return { privateExtendedKey, publicExtendedKey }; + } + function accountFromExtendedKey(base58key, accountIndex = 0) { + let extendedKey = HDKey.fromExtendedKey(base58key); + let version = base58key.slice(0, 4); + let child = extendedKey.deriveChild(0).deriveChild(accountIndex); + let publicKey = bytesToHex2(child.publicKey.slice(1)); + if (!publicKey) + throw new Error("could not derive public key"); + if (version === "xprv") { + let privateKey = child.privateKey; + if (!privateKey) + throw new Error("could not derive private key"); + return { privateKey, publicKey }; + } + return { publicKey }; + } + function generateSeedWords() { + return generateMnemonic(wordlist); + } + function validateWords(words) { + return validateMnemonic(words, wordlist); + } + + // nip10.ts + var nip10_exports = {}; + __export(nip10_exports, { + parse: () => parse + }); + function parse(event) { + const result = { + reply: void 0, + root: void 0, + mentions: [], + profiles: [], + quotes: [] + }; + let maybeParent; + let maybeRoot; + for (let i2 = event.tags.length - 1; i2 >= 0; i2--) { + const tag = event.tags[i2]; + if (tag[0] === "e" && tag[1]) { + const [_, eTagEventId, eTagRelayUrl, eTagMarker, eTagAuthor] = tag; + const eventPointer = { + id: eTagEventId, + relays: eTagRelayUrl ? [eTagRelayUrl] : [], + author: eTagAuthor + }; + if (eTagMarker === "root") { + result.root = eventPointer; + continue; + } + if (eTagMarker === "reply") { + result.reply = eventPointer; + continue; + } + if (eTagMarker === "mention") { + result.mentions.push(eventPointer); + continue; + } + if (!maybeParent) { + maybeParent = eventPointer; + } else { + maybeRoot = eventPointer; + } + result.mentions.push(eventPointer); + continue; + } + if (tag[0] === "q" && tag[1]) { + const [_, eTagEventId, eTagRelayUrl] = tag; + result.quotes.push({ + id: eTagEventId, + relays: eTagRelayUrl ? [eTagRelayUrl] : [] + }); + } + if (tag[0] === "p" && tag[1]) { + result.profiles.push({ + pubkey: tag[1], + relays: tag[2] ? [tag[2]] : [] + }); + continue; + } + } + if (!result.root) { + result.root = maybeRoot || maybeParent || result.reply; + } + if (!result.reply) { + result.reply = maybeParent || result.root; + } + ; + [result.reply, result.root].forEach((ref) => { + if (!ref) + return; + let idx = result.mentions.indexOf(ref); + if (idx !== -1) { + result.mentions.splice(idx, 1); + } + if (ref.author) { + let author = result.profiles.find((p) => p.pubkey === ref.author); + if (author && author.relays) { + if (!ref.relays) { + ref.relays = []; + } + author.relays.forEach((url) => { + if (ref.relays?.indexOf(url) === -1) + ref.relays.push(url); + }); + author.relays = ref.relays; + } + } + }); + result.mentions.forEach((ref) => { + if (ref.author) { + let author = result.profiles.find((p) => p.pubkey === ref.author); + if (author && author.relays) { + if (!ref.relays) { + ref.relays = []; + } + author.relays.forEach((url) => { + if (ref.relays.indexOf(url) === -1) + ref.relays.push(url); + }); + author.relays = ref.relays; + } + } + }); + return result; + } + + // nip11.ts + var nip11_exports = {}; + __export(nip11_exports, { + fetchRelayInformation: () => fetchRelayInformation, + useFetchImplementation: () => useFetchImplementation2 + }); + var _fetch2; + try { + _fetch2 = fetch; + } catch { + } + function useFetchImplementation2(fetchImplementation) { + _fetch2 = fetchImplementation; + } + async function fetchRelayInformation(url) { + return await (await fetch(url.replace("ws://", "http://").replace("wss://", "https://"), { + headers: { Accept: "application/nostr+json" } + })).json(); + } + + // nip13.ts + var nip13_exports = {}; + __export(nip13_exports, { + fastEventHash: () => fastEventHash, + getPow: () => getPow, + minePow: () => minePow + }); + function getPow(hex2) { + let count = 0; + for (let i2 = 0; i2 < 64; i2 += 8) { + const nibble = parseInt(hex2.substring(i2, i2 + 8), 16); + if (nibble === 0) { + count += 32; + } else { + count += Math.clz32(nibble); + break; + } + } + return count; + } + function minePow(unsigned, difficulty) { + let count = 0; + const event = unsigned; + const tag = ["nonce", count.toString(), difficulty.toString()]; + event.tags.push(tag); + while (true) { + const now2 = Math.floor(new Date().getTime() / 1e3); + if (now2 !== event.created_at) { + count = 0; + event.created_at = now2; + } + tag[1] = (++count).toString(); + event.id = fastEventHash(event); + if (getPow(event.id) >= difficulty) { + break; + } + } + return event; + } + function fastEventHash(evt) { + return bytesToHex2( + sha2562(utf8Encoder.encode(JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]))) + ); + } + + // nip17.ts + var nip17_exports = {}; + __export(nip17_exports, { + unwrapEvent: () => unwrapEvent2, + unwrapManyEvents: () => unwrapManyEvents2, + wrapEvent: () => wrapEvent2, + wrapManyEvents: () => wrapManyEvents2 + }); + + // nip59.ts + var nip59_exports = {}; + __export(nip59_exports, { + createRumor: () => createRumor, + createSeal: () => createSeal, + createWrap: () => createWrap, + unwrapEvent: () => unwrapEvent, + unwrapManyEvents: () => unwrapManyEvents, + wrapEvent: () => wrapEvent, + wrapManyEvents: () => wrapManyEvents + }); + + // nip44.ts + var nip44_exports = {}; + __export(nip44_exports, { + decrypt: () => decrypt3, + encrypt: () => encrypt3, + getConversationKey: () => getConversationKey, + v2: () => v2 + }); + + // node_modules/@noble/ciphers/esm/_poly1305.js + var u8to16 = (a, i2) => a[i2++] & 255 | (a[i2++] & 255) << 8; + var Poly1305 = class { + constructor(key) { + this.blockLen = 16; + this.outputLen = 16; + this.buffer = new Uint8Array(16); + this.r = new Uint16Array(10); + this.h = new Uint16Array(10); + this.pad = new Uint16Array(8); + this.pos = 0; + this.finished = false; + key = toBytes3(key); + bytes3(key, 32); + const t0 = u8to16(key, 0); + const t1 = u8to16(key, 2); + const t2 = u8to16(key, 4); + const t3 = u8to16(key, 6); + const t4 = u8to16(key, 8); + const t5 = u8to16(key, 10); + const t6 = u8to16(key, 12); + const t7 = u8to16(key, 14); + this.r[0] = t0 & 8191; + this.r[1] = (t0 >>> 13 | t1 << 3) & 8191; + this.r[2] = (t1 >>> 10 | t2 << 6) & 7939; + this.r[3] = (t2 >>> 7 | t3 << 9) & 8191; + this.r[4] = (t3 >>> 4 | t4 << 12) & 255; + this.r[5] = t4 >>> 1 & 8190; + this.r[6] = (t4 >>> 14 | t5 << 2) & 8191; + this.r[7] = (t5 >>> 11 | t6 << 5) & 8065; + this.r[8] = (t6 >>> 8 | t7 << 8) & 8191; + this.r[9] = t7 >>> 5 & 127; + for (let i2 = 0; i2 < 8; i2++) + this.pad[i2] = u8to16(key, 16 + 2 * i2); + } + process(data, offset, isLast = false) { + const hibit = isLast ? 0 : 1 << 11; + const { h, r } = this; + const r0 = r[0]; + const r1 = r[1]; + const r2 = r[2]; + const r3 = r[3]; + const r4 = r[4]; + const r5 = r[5]; + const r6 = r[6]; + const r7 = r[7]; + const r8 = r[8]; + const r9 = r[9]; + const t0 = u8to16(data, offset + 0); + const t1 = u8to16(data, offset + 2); + const t2 = u8to16(data, offset + 4); + const t3 = u8to16(data, offset + 6); + const t4 = u8to16(data, offset + 8); + const t5 = u8to16(data, offset + 10); + const t6 = u8to16(data, offset + 12); + const t7 = u8to16(data, offset + 14); + let h0 = h[0] + (t0 & 8191); + let h1 = h[1] + ((t0 >>> 13 | t1 << 3) & 8191); + let h2 = h[2] + ((t1 >>> 10 | t2 << 6) & 8191); + let h3 = h[3] + ((t2 >>> 7 | t3 << 9) & 8191); + let h4 = h[4] + ((t3 >>> 4 | t4 << 12) & 8191); + let h5 = h[5] + (t4 >>> 1 & 8191); + let h6 = h[6] + ((t4 >>> 14 | t5 << 2) & 8191); + let h7 = h[7] + ((t5 >>> 11 | t6 << 5) & 8191); + let h8 = h[8] + ((t6 >>> 8 | t7 << 8) & 8191); + let h9 = h[9] + (t7 >>> 5 | hibit); + let c = 0; + let d0 = c + h0 * r0 + h1 * (5 * r9) + h2 * (5 * r8) + h3 * (5 * r7) + h4 * (5 * r6); + c = d0 >>> 13; + d0 &= 8191; + d0 += h5 * (5 * r5) + h6 * (5 * r4) + h7 * (5 * r3) + h8 * (5 * r2) + h9 * (5 * r1); + c += d0 >>> 13; + d0 &= 8191; + let d1 = c + h0 * r1 + h1 * r0 + h2 * (5 * r9) + h3 * (5 * r8) + h4 * (5 * r7); + c = d1 >>> 13; + d1 &= 8191; + d1 += h5 * (5 * r6) + h6 * (5 * r5) + h7 * (5 * r4) + h8 * (5 * r3) + h9 * (5 * r2); + c += d1 >>> 13; + d1 &= 8191; + let d2 = c + h0 * r2 + h1 * r1 + h2 * r0 + h3 * (5 * r9) + h4 * (5 * r8); + c = d2 >>> 13; + d2 &= 8191; + d2 += h5 * (5 * r7) + h6 * (5 * r6) + h7 * (5 * r5) + h8 * (5 * r4) + h9 * (5 * r3); + c += d2 >>> 13; + d2 &= 8191; + let d3 = c + h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * (5 * r9); + c = d3 >>> 13; + d3 &= 8191; + d3 += h5 * (5 * r8) + h6 * (5 * r7) + h7 * (5 * r6) + h8 * (5 * r5) + h9 * (5 * r4); + c += d3 >>> 13; + d3 &= 8191; + let d4 = c + h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0; + c = d4 >>> 13; + d4 &= 8191; + d4 += h5 * (5 * r9) + h6 * (5 * r8) + h7 * (5 * r7) + h8 * (5 * r6) + h9 * (5 * r5); + c += d4 >>> 13; + d4 &= 8191; + let d5 = c + h0 * r5 + h1 * r4 + h2 * r3 + h3 * r2 + h4 * r1; + c = d5 >>> 13; + d5 &= 8191; + d5 += h5 * r0 + h6 * (5 * r9) + h7 * (5 * r8) + h8 * (5 * r7) + h9 * (5 * r6); + c += d5 >>> 13; + d5 &= 8191; + let d6 = c + h0 * r6 + h1 * r5 + h2 * r4 + h3 * r3 + h4 * r2; + c = d6 >>> 13; + d6 &= 8191; + d6 += h5 * r1 + h6 * r0 + h7 * (5 * r9) + h8 * (5 * r8) + h9 * (5 * r7); + c += d6 >>> 13; + d6 &= 8191; + let d7 = c + h0 * r7 + h1 * r6 + h2 * r5 + h3 * r4 + h4 * r3; + c = d7 >>> 13; + d7 &= 8191; + d7 += h5 * r2 + h6 * r1 + h7 * r0 + h8 * (5 * r9) + h9 * (5 * r8); + c += d7 >>> 13; + d7 &= 8191; + let d8 = c + h0 * r8 + h1 * r7 + h2 * r6 + h3 * r5 + h4 * r4; + c = d8 >>> 13; + d8 &= 8191; + d8 += h5 * r3 + h6 * r2 + h7 * r1 + h8 * r0 + h9 * (5 * r9); + c += d8 >>> 13; + d8 &= 8191; + let d9 = c + h0 * r9 + h1 * r8 + h2 * r7 + h3 * r6 + h4 * r5; + c = d9 >>> 13; + d9 &= 8191; + d9 += h5 * r4 + h6 * r3 + h7 * r2 + h8 * r1 + h9 * r0; + c += d9 >>> 13; + d9 &= 8191; + c = (c << 2) + c | 0; + c = c + d0 | 0; + d0 = c & 8191; + c = c >>> 13; + d1 += c; + h[0] = d0; + h[1] = d1; + h[2] = d2; + h[3] = d3; + h[4] = d4; + h[5] = d5; + h[6] = d6; + h[7] = d7; + h[8] = d8; + h[9] = d9; + } + finalize() { + const { h, pad: pad2 } = this; + const g = new Uint16Array(10); + let c = h[1] >>> 13; + h[1] &= 8191; + for (let i2 = 2; i2 < 10; i2++) { + h[i2] += c; + c = h[i2] >>> 13; + h[i2] &= 8191; + } + h[0] += c * 5; + c = h[0] >>> 13; + h[0] &= 8191; + h[1] += c; + c = h[1] >>> 13; + h[1] &= 8191; + h[2] += c; + g[0] = h[0] + 5; + c = g[0] >>> 13; + g[0] &= 8191; + for (let i2 = 1; i2 < 10; i2++) { + g[i2] = h[i2] + c; + c = g[i2] >>> 13; + g[i2] &= 8191; + } + g[9] -= 1 << 13; + let mask = (c ^ 1) - 1; + for (let i2 = 0; i2 < 10; i2++) + g[i2] &= mask; + mask = ~mask; + for (let i2 = 0; i2 < 10; i2++) + h[i2] = h[i2] & mask | g[i2]; + h[0] = (h[0] | h[1] << 13) & 65535; + h[1] = (h[1] >>> 3 | h[2] << 10) & 65535; + h[2] = (h[2] >>> 6 | h[3] << 7) & 65535; + h[3] = (h[3] >>> 9 | h[4] << 4) & 65535; + h[4] = (h[4] >>> 12 | h[5] << 1 | h[6] << 14) & 65535; + h[5] = (h[6] >>> 2 | h[7] << 11) & 65535; + h[6] = (h[7] >>> 5 | h[8] << 8) & 65535; + h[7] = (h[8] >>> 8 | h[9] << 5) & 65535; + let f2 = h[0] + pad2[0]; + h[0] = f2 & 65535; + for (let i2 = 1; i2 < 8; i2++) { + f2 = (h[i2] + pad2[i2] | 0) + (f2 >>> 16) | 0; + h[i2] = f2 & 65535; + } + } + update(data) { + exists3(this); + const { buffer, blockLen } = this; + data = toBytes3(data); + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + if (take === blockLen) { + for (; blockLen <= len - pos; pos += blockLen) + this.process(data, pos); + continue; + } + buffer.set(data.subarray(pos, pos + take), this.pos); + this.pos += take; + pos += take; + if (this.pos === blockLen) { + this.process(buffer, 0, false); + this.pos = 0; + } + } + return this; + } + destroy() { + this.h.fill(0); + this.r.fill(0); + this.buffer.fill(0); + this.pad.fill(0); + } + digestInto(out) { + exists3(this); + output3(out, this); + this.finished = true; + const { buffer, h } = this; + let { pos } = this; + if (pos) { + buffer[pos++] = 1; + for (; pos < 16; pos++) + buffer[pos] = 0; + this.process(buffer, 0, true); + } + this.finalize(); + let opos = 0; + for (let i2 = 0; i2 < 8; i2++) { + out[opos++] = h[i2] >>> 0; + out[opos++] = h[i2] >>> 8; + } + return out; + } + digest() { + const { buffer, outputLen } = this; + this.digestInto(buffer); + const res = buffer.slice(0, outputLen); + this.destroy(); + return res; + } + }; + function wrapConstructorWithKey2(hashCons) { + const hashC = (msg, key) => hashCons(key).update(toBytes3(msg)).digest(); + const tmp = hashCons(new Uint8Array(32)); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.create = (key) => hashCons(key); + return hashC; + } + var poly1305 = wrapConstructorWithKey2((key) => new Poly1305(key)); + + // node_modules/@noble/ciphers/esm/_arx.js + var _utf8ToBytes = (str) => Uint8Array.from(str.split("").map((c) => c.charCodeAt(0))); + var sigma16 = _utf8ToBytes("expand 16-byte k"); + var sigma32 = _utf8ToBytes("expand 32-byte k"); + var sigma16_32 = u32(sigma16); + var sigma32_32 = u32(sigma32); + var sigma = sigma32_32.slice(); + function rotl2(a, b) { + return a << b | a >>> 32 - b; + } + function isAligned32(b) { + return b.byteOffset % 4 === 0; + } + var BLOCK_LEN = 64; + var BLOCK_LEN32 = 16; + var MAX_COUNTER = 2 ** 32 - 1; + var U32_EMPTY = new Uint32Array(); + function runCipher(core, sigma2, key, nonce, data, output4, counter, rounds) { + const len = data.length; + const block = new Uint8Array(BLOCK_LEN); + const b32 = u32(block); + const isAligned = isAligned32(data) && isAligned32(output4); + const d32 = isAligned ? u32(data) : U32_EMPTY; + const o32 = isAligned ? u32(output4) : U32_EMPTY; + for (let pos = 0; pos < len; counter++) { + core(sigma2, key, nonce, b32, counter, rounds); + if (counter >= MAX_COUNTER) + throw new Error("arx: counter overflow"); + const take = Math.min(BLOCK_LEN, len - pos); + if (isAligned && take === BLOCK_LEN) { + const pos32 = pos / 4; + if (pos % 4 !== 0) + throw new Error("arx: invalid block position"); + for (let j = 0, posj; j < BLOCK_LEN32; j++) { + posj = pos32 + j; + o32[posj] = d32[posj] ^ b32[j]; + } + pos += BLOCK_LEN; + continue; + } + for (let j = 0, posj; j < take; j++) { + posj = pos + j; + output4[posj] = data[posj] ^ block[j]; + } + pos += take; + } + } + function createCipher(core, opts) { + const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = checkOpts2({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts); + if (typeof core !== "function") + throw new Error("core must be a function"); + number3(counterLength); + number3(rounds); + bool2(counterRight); + bool2(allowShortKeys); + return (key, nonce, data, output4, counter = 0) => { + bytes3(key); + bytes3(nonce); + bytes3(data); + const len = data.length; + if (!output4) + output4 = new Uint8Array(len); + bytes3(output4); + number3(counter); + if (counter < 0 || counter >= MAX_COUNTER) + throw new Error("arx: counter overflow"); + if (output4.length < len) + throw new Error(`arx: output (${output4.length}) is shorter than data (${len})`); + const toClean = []; + let l = key.length, k, sigma2; + if (l === 32) { + k = key.slice(); + toClean.push(k); + sigma2 = sigma32_32; + } else if (l === 16 && allowShortKeys) { + k = new Uint8Array(32); + k.set(key); + k.set(key, 16); + sigma2 = sigma16_32; + toClean.push(k); + } else { + throw new Error(`arx: invalid 32-byte key, got length=${l}`); + } + if (!isAligned32(nonce)) { + nonce = nonce.slice(); + toClean.push(nonce); + } + const k32 = u32(k); + if (extendNonceFn) { + if (nonce.length !== 24) + throw new Error(`arx: extended nonce must be 24 bytes`); + extendNonceFn(sigma2, k32, u32(nonce.subarray(0, 16)), k32); + nonce = nonce.subarray(16); + } + const nonceNcLen = 16 - counterLength; + if (nonceNcLen !== nonce.length) + throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`); + if (nonceNcLen !== 12) { + const nc = new Uint8Array(12); + nc.set(nonce, counterRight ? 0 : 12 - nonce.length); + nonce = nc; + toClean.push(nonce); + } + const n32 = u32(nonce); + runCipher(core, sigma2, k32, n32, data, output4, counter, rounds); + while (toClean.length > 0) + toClean.pop().fill(0); + return output4; + }; + } + + // node_modules/@noble/ciphers/esm/chacha.js + function chachaCore(s, k, n, out, cnt, rounds = 20) { + let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; + let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15; + for (let r = 0; r < rounds; r += 2) { + x00 = x00 + x04 | 0; + x12 = rotl2(x12 ^ x00, 16); + x08 = x08 + x12 | 0; + x04 = rotl2(x04 ^ x08, 12); + x00 = x00 + x04 | 0; + x12 = rotl2(x12 ^ x00, 8); + x08 = x08 + x12 | 0; + x04 = rotl2(x04 ^ x08, 7); + x01 = x01 + x05 | 0; + x13 = rotl2(x13 ^ x01, 16); + x09 = x09 + x13 | 0; + x05 = rotl2(x05 ^ x09, 12); + x01 = x01 + x05 | 0; + x13 = rotl2(x13 ^ x01, 8); + x09 = x09 + x13 | 0; + x05 = rotl2(x05 ^ x09, 7); + x02 = x02 + x06 | 0; + x14 = rotl2(x14 ^ x02, 16); + x10 = x10 + x14 | 0; + x06 = rotl2(x06 ^ x10, 12); + x02 = x02 + x06 | 0; + x14 = rotl2(x14 ^ x02, 8); + x10 = x10 + x14 | 0; + x06 = rotl2(x06 ^ x10, 7); + x03 = x03 + x07 | 0; + x15 = rotl2(x15 ^ x03, 16); + x11 = x11 + x15 | 0; + x07 = rotl2(x07 ^ x11, 12); + x03 = x03 + x07 | 0; + x15 = rotl2(x15 ^ x03, 8); + x11 = x11 + x15 | 0; + x07 = rotl2(x07 ^ x11, 7); + x00 = x00 + x05 | 0; + x15 = rotl2(x15 ^ x00, 16); + x10 = x10 + x15 | 0; + x05 = rotl2(x05 ^ x10, 12); + x00 = x00 + x05 | 0; + x15 = rotl2(x15 ^ x00, 8); + x10 = x10 + x15 | 0; + x05 = rotl2(x05 ^ x10, 7); + x01 = x01 + x06 | 0; + x12 = rotl2(x12 ^ x01, 16); + x11 = x11 + x12 | 0; + x06 = rotl2(x06 ^ x11, 12); + x01 = x01 + x06 | 0; + x12 = rotl2(x12 ^ x01, 8); + x11 = x11 + x12 | 0; + x06 = rotl2(x06 ^ x11, 7); + x02 = x02 + x07 | 0; + x13 = rotl2(x13 ^ x02, 16); + x08 = x08 + x13 | 0; + x07 = rotl2(x07 ^ x08, 12); + x02 = x02 + x07 | 0; + x13 = rotl2(x13 ^ x02, 8); + x08 = x08 + x13 | 0; + x07 = rotl2(x07 ^ x08, 7); + x03 = x03 + x04 | 0; + x14 = rotl2(x14 ^ x03, 16); + x09 = x09 + x14 | 0; + x04 = rotl2(x04 ^ x09, 12); + x03 = x03 + x04 | 0; + x14 = rotl2(x14 ^ x03, 8); + x09 = x09 + x14 | 0; + x04 = rotl2(x04 ^ x09, 7); + } + let oi = 0; + out[oi++] = y00 + x00 | 0; + out[oi++] = y01 + x01 | 0; + out[oi++] = y02 + x02 | 0; + out[oi++] = y03 + x03 | 0; + out[oi++] = y04 + x04 | 0; + out[oi++] = y05 + x05 | 0; + out[oi++] = y06 + x06 | 0; + out[oi++] = y07 + x07 | 0; + out[oi++] = y08 + x08 | 0; + out[oi++] = y09 + x09 | 0; + out[oi++] = y10 + x10 | 0; + out[oi++] = y11 + x11 | 0; + out[oi++] = y12 + x12 | 0; + out[oi++] = y13 + x13 | 0; + out[oi++] = y14 + x14 | 0; + out[oi++] = y15 + x15 | 0; + } + function hchacha(s, k, i2, o32) { + let x00 = s[0], x01 = s[1], x02 = s[2], x03 = s[3], x04 = k[0], x05 = k[1], x06 = k[2], x07 = k[3], x08 = k[4], x09 = k[5], x10 = k[6], x11 = k[7], x12 = i2[0], x13 = i2[1], x14 = i2[2], x15 = i2[3]; + for (let r = 0; r < 20; r += 2) { + x00 = x00 + x04 | 0; + x12 = rotl2(x12 ^ x00, 16); + x08 = x08 + x12 | 0; + x04 = rotl2(x04 ^ x08, 12); + x00 = x00 + x04 | 0; + x12 = rotl2(x12 ^ x00, 8); + x08 = x08 + x12 | 0; + x04 = rotl2(x04 ^ x08, 7); + x01 = x01 + x05 | 0; + x13 = rotl2(x13 ^ x01, 16); + x09 = x09 + x13 | 0; + x05 = rotl2(x05 ^ x09, 12); + x01 = x01 + x05 | 0; + x13 = rotl2(x13 ^ x01, 8); + x09 = x09 + x13 | 0; + x05 = rotl2(x05 ^ x09, 7); + x02 = x02 + x06 | 0; + x14 = rotl2(x14 ^ x02, 16); + x10 = x10 + x14 | 0; + x06 = rotl2(x06 ^ x10, 12); + x02 = x02 + x06 | 0; + x14 = rotl2(x14 ^ x02, 8); + x10 = x10 + x14 | 0; + x06 = rotl2(x06 ^ x10, 7); + x03 = x03 + x07 | 0; + x15 = rotl2(x15 ^ x03, 16); + x11 = x11 + x15 | 0; + x07 = rotl2(x07 ^ x11, 12); + x03 = x03 + x07 | 0; + x15 = rotl2(x15 ^ x03, 8); + x11 = x11 + x15 | 0; + x07 = rotl2(x07 ^ x11, 7); + x00 = x00 + x05 | 0; + x15 = rotl2(x15 ^ x00, 16); + x10 = x10 + x15 | 0; + x05 = rotl2(x05 ^ x10, 12); + x00 = x00 + x05 | 0; + x15 = rotl2(x15 ^ x00, 8); + x10 = x10 + x15 | 0; + x05 = rotl2(x05 ^ x10, 7); + x01 = x01 + x06 | 0; + x12 = rotl2(x12 ^ x01, 16); + x11 = x11 + x12 | 0; + x06 = rotl2(x06 ^ x11, 12); + x01 = x01 + x06 | 0; + x12 = rotl2(x12 ^ x01, 8); + x11 = x11 + x12 | 0; + x06 = rotl2(x06 ^ x11, 7); + x02 = x02 + x07 | 0; + x13 = rotl2(x13 ^ x02, 16); + x08 = x08 + x13 | 0; + x07 = rotl2(x07 ^ x08, 12); + x02 = x02 + x07 | 0; + x13 = rotl2(x13 ^ x02, 8); + x08 = x08 + x13 | 0; + x07 = rotl2(x07 ^ x08, 7); + x03 = x03 + x04 | 0; + x14 = rotl2(x14 ^ x03, 16); + x09 = x09 + x14 | 0; + x04 = rotl2(x04 ^ x09, 12); + x03 = x03 + x04 | 0; + x14 = rotl2(x14 ^ x03, 8); + x09 = x09 + x14 | 0; + x04 = rotl2(x04 ^ x09, 7); + } + let oi = 0; + o32[oi++] = x00; + o32[oi++] = x01; + o32[oi++] = x02; + o32[oi++] = x03; + o32[oi++] = x12; + o32[oi++] = x13; + o32[oi++] = x14; + o32[oi++] = x15; + } + var chacha20 = /* @__PURE__ */ createCipher(chachaCore, { + counterRight: false, + counterLength: 4, + allowShortKeys: false + }); + var xchacha20 = /* @__PURE__ */ createCipher(chachaCore, { + counterRight: false, + counterLength: 8, + extendNonceFn: hchacha, + allowShortKeys: false + }); + var ZEROS162 = /* @__PURE__ */ new Uint8Array(16); + var updatePadded = (h, msg) => { + h.update(msg); + const left = msg.length % 16; + if (left) + h.update(ZEROS162.subarray(left)); + }; + var ZEROS322 = /* @__PURE__ */ new Uint8Array(32); + function computeTag2(fn, key, nonce, data, AAD) { + const authKey = fn(key, nonce, ZEROS322); + const h = poly1305.create(authKey); + if (AAD) + updatePadded(h, AAD); + updatePadded(h, data); + const num = new Uint8Array(16); + const view = createView3(num); + setBigUint643(view, 0, BigInt(AAD ? AAD.length : 0), true); + setBigUint643(view, 8, BigInt(data.length), true); + h.update(num); + const res = h.digest(); + authKey.fill(0); + return res; + } + var _poly1305_aead = (xorStream) => (key, nonce, AAD) => { + const tagLength = 16; + bytes3(key, 32); + bytes3(nonce); + return { + encrypt: (plaintext, output4) => { + const plength = plaintext.length; + const clength = plength + tagLength; + if (output4) { + bytes3(output4, clength); + } else { + output4 = new Uint8Array(clength); + } + xorStream(key, nonce, plaintext, output4, 1); + const tag = computeTag2(xorStream, key, nonce, output4.subarray(0, -tagLength), AAD); + output4.set(tag, plength); + return output4; + }, + decrypt: (ciphertext, output4) => { + const clength = ciphertext.length; + const plength = clength - tagLength; + if (clength < tagLength) + throw new Error(`encrypted data must be at least ${tagLength} bytes`); + if (output4) { + bytes3(output4, plength); + } else { + output4 = new Uint8Array(plength); + } + const data = ciphertext.subarray(0, -tagLength); + const passedTag = ciphertext.subarray(-tagLength); + const tag = computeTag2(xorStream, key, nonce, data, AAD); + if (!equalBytes2(passedTag, tag)) + throw new Error("invalid tag"); + xorStream(key, nonce, data, output4, 1); + return output4; + } + }; + }; + var chacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 12, tagLength: 16 }, _poly1305_aead(chacha20)); + var xchacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 24, tagLength: 16 }, _poly1305_aead(xchacha20)); + + // node_modules/@noble/hashes/esm/hkdf.js + function extract(hash3, ikm, salt2) { + assert_default.hash(hash3); + if (salt2 === void 0) + salt2 = new Uint8Array(hash3.outputLen); + return hmac2(hash3, toBytes2(salt2), toBytes2(ikm)); + } + var HKDF_COUNTER = new Uint8Array([0]); + var EMPTY_BUFFER = new Uint8Array(); + function expand(hash3, prk, info, length = 32) { + assert_default.hash(hash3); + assert_default.number(length); + if (length > 255 * hash3.outputLen) + throw new Error("Length should be <= 255*HashLen"); + const blocks = Math.ceil(length / hash3.outputLen); + if (info === void 0) + info = EMPTY_BUFFER; + const okm = new Uint8Array(blocks * hash3.outputLen); + const HMAC3 = hmac2.create(hash3, prk); + const HMACTmp = HMAC3._cloneInto(); + const T = new Uint8Array(HMAC3.outputLen); + for (let counter = 0; counter < blocks; counter++) { + HKDF_COUNTER[0] = counter + 1; + HMACTmp.update(counter === 0 ? EMPTY_BUFFER : T).update(info).update(HKDF_COUNTER).digestInto(T); + okm.set(T, hash3.outputLen * counter); + HMAC3._cloneInto(HMACTmp); + } + HMAC3.destroy(); + HMACTmp.destroy(); + T.fill(0); + HKDF_COUNTER.fill(0); + return okm.slice(0, length); + } + + // nip44.ts + var minPlaintextSize = 1; + var maxPlaintextSize = 65535; + function getConversationKey(privkeyA, pubkeyB) { + const sharedX = secp256k1.getSharedSecret(privkeyA, "02" + pubkeyB).subarray(1, 33); + return extract(sha2562, sharedX, "nip44-v2"); + } + function getMessageKeys(conversationKey, nonce) { + const keys = expand(sha2562, conversationKey, nonce, 76); + return { + chacha_key: keys.subarray(0, 32), + chacha_nonce: keys.subarray(32, 44), + hmac_key: keys.subarray(44, 76) + }; + } + function calcPaddedLen(len) { + 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); + } + function writeU16BE(num) { + if (!Number.isSafeInteger(num) || num < minPlaintextSize || num > 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; + } + function pad(plaintext) { + const unpadded = utf8Encoder.encode(plaintext); + const unpaddedLen = unpadded.length; + const prefix = writeU16BE(unpaddedLen); + const suffix = new Uint8Array(calcPaddedLen(unpaddedLen) - unpaddedLen); + return concatBytes3(prefix, unpadded, suffix); + } + function unpad(padded) { + const unpaddedLen = new DataView(padded.buffer).getUint16(0); + const unpadded = padded.subarray(2, 2 + unpaddedLen); + if (unpaddedLen < minPlaintextSize || unpaddedLen > maxPlaintextSize || unpadded.length !== unpaddedLen || padded.length !== 2 + calcPaddedLen(unpaddedLen)) + throw new Error("invalid padding"); + return utf8Decoder.decode(unpadded); + } + function hmacAad(key, message, aad) { + if (aad.length !== 32) + throw new Error("AAD associated data must be 32 bytes"); + const combined = concatBytes3(aad, message); + return hmac2(sha2562, key, combined); + } + function decodePayload(payload) { + 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; + try { + data = base64.decode(payload); + } catch (error) { + throw new Error("invalid base64: " + 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) + }; + } + function encrypt3(plaintext, conversationKey, nonce = randomBytes2(32)) { + const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce); + const padded = pad(plaintext); + const ciphertext = chacha20(chacha_key, chacha_nonce, padded); + const mac = hmacAad(hmac_key, ciphertext, nonce); + return base64.encode(concatBytes3(new Uint8Array([2]), nonce, ciphertext, mac)); + } + function decrypt3(payload, conversationKey) { + const { nonce, ciphertext, mac } = decodePayload(payload); + const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce); + const calculatedMac = hmacAad(hmac_key, ciphertext, nonce); + if (!equalBytes2(calculatedMac, mac)) + throw new Error("invalid MAC"); + const padded = chacha20(chacha_key, chacha_nonce, ciphertext); + return unpad(padded); + } + var v2 = { + utils: { + getConversationKey, + calcPaddedLen + }, + encrypt: encrypt3, + decrypt: decrypt3 + }; + + // nip59.ts + var TWO_DAYS = 2 * 24 * 60 * 60; + var now = () => Math.round(Date.now() / 1e3); + var randomNow = () => Math.round(now() - Math.random() * TWO_DAYS); + var nip44ConversationKey = (privateKey, publicKey) => getConversationKey(privateKey, publicKey); + var nip44Encrypt = (data, privateKey, publicKey) => encrypt3(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey)); + var nip44Decrypt = (data, privateKey) => JSON.parse(decrypt3(data.content, nip44ConversationKey(privateKey, data.pubkey))); + function createRumor(event, privateKey) { + const rumor = { + created_at: now(), + content: "", + tags: [], + ...event, + pubkey: getPublicKey(privateKey) + }; + rumor.id = getEventHash(rumor); + return rumor; + } + function createSeal(rumor, privateKey, recipientPublicKey) { + return finalizeEvent( + { + kind: Seal, + content: nip44Encrypt(rumor, privateKey, recipientPublicKey), + created_at: randomNow(), + tags: [] + }, + privateKey + ); + } + function createWrap(seal, recipientPublicKey) { + const randomKey = generateSecretKey(); + return finalizeEvent( + { + kind: GiftWrap, + content: nip44Encrypt(seal, randomKey, recipientPublicKey), + created_at: randomNow(), + tags: [["p", recipientPublicKey]] + }, + randomKey + ); + } + function wrapEvent(event, senderPrivateKey, recipientPublicKey) { + const rumor = createRumor(event, senderPrivateKey); + const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey); + return createWrap(seal, recipientPublicKey); + } + function wrapManyEvents(event, senderPrivateKey, recipientsPublicKeys) { + if (!recipientsPublicKeys || recipientsPublicKeys.length === 0) { + throw new Error("At least one recipient is required."); + } + const senderPublicKey = getPublicKey(senderPrivateKey); + const wrappeds = [wrapEvent(event, senderPrivateKey, senderPublicKey)]; + recipientsPublicKeys.forEach((recipientPublicKey) => { + wrappeds.push(wrapEvent(event, senderPrivateKey, recipientPublicKey)); + }); + return wrappeds; + } + function unwrapEvent(wrap, recipientPrivateKey) { + const unwrappedSeal = nip44Decrypt(wrap, recipientPrivateKey); + return nip44Decrypt(unwrappedSeal, recipientPrivateKey); + } + function unwrapManyEvents(wrappedEvents, recipientPrivateKey) { + let unwrappedEvents = []; + wrappedEvents.forEach((e) => { + unwrappedEvents.push(unwrapEvent(e, recipientPrivateKey)); + }); + unwrappedEvents.sort((a, b) => a.created_at - b.created_at); + return unwrappedEvents; + } + + // nip17.ts + function createEvent(recipients, message, conversationTitle, replyTo) { + const baseEvent = { + created_at: Math.ceil(Date.now() / 1e3), + kind: PrivateDirectMessage, + tags: [], + content: message + }; + const recipientsArray = Array.isArray(recipients) ? recipients : [recipients]; + recipientsArray.forEach(({ publicKey, relayUrl }) => { + baseEvent.tags.push(relayUrl ? ["p", publicKey, relayUrl] : ["p", publicKey]); + }); + if (replyTo) { + baseEvent.tags.push(["e", replyTo.eventId, replyTo.relayUrl || "", "reply"]); + } + if (conversationTitle) { + baseEvent.tags.push(["subject", conversationTitle]); + } + return baseEvent; + } + function wrapEvent2(senderPrivateKey, recipient, message, conversationTitle, replyTo) { + const event = createEvent(recipient, message, conversationTitle, replyTo); + return wrapEvent(event, senderPrivateKey, recipient.publicKey); + } + function wrapManyEvents2(senderPrivateKey, recipients, message, conversationTitle, replyTo) { + if (!recipients || recipients.length === 0) { + throw new Error("At least one recipient is required."); + } + const senderPublicKey = getPublicKey(senderPrivateKey); + return [{ publicKey: senderPublicKey }, ...recipients].map( + (recipient) => wrapEvent2(senderPrivateKey, recipient, message, conversationTitle, replyTo) + ); + } + var unwrapEvent2 = unwrapEvent; + var unwrapManyEvents2 = unwrapManyEvents; + + // nip18.ts + var nip18_exports = {}; + __export(nip18_exports, { + finishRepostEvent: () => finishRepostEvent, + getRepostedEvent: () => getRepostedEvent, + getRepostedEventPointer: () => getRepostedEventPointer + }); + function finishRepostEvent(t, reposted, relayUrl, privateKey) { + let kind; + const tags = [...t.tags ?? [], ["e", reposted.id, relayUrl], ["p", reposted.pubkey]]; + if (reposted.kind === ShortTextNote) { + kind = Repost; + } else { + kind = GenericRepost; + tags.push(["k", String(reposted.kind)]); + } + return finalizeEvent( + { + kind, + tags, + content: t.content === "" || reposted.tags?.find((tag) => tag[0] === "-") ? "" : JSON.stringify(reposted), + created_at: t.created_at + }, + privateKey + ); + } + function getRepostedEventPointer(event) { + if (![Repost, GenericRepost].includes(event.kind)) { + return void 0; + } + let lastETag; + let lastPTag; + for (let i2 = event.tags.length - 1; i2 >= 0 && (lastETag === void 0 || lastPTag === void 0); i2--) { + const tag = event.tags[i2]; + if (tag.length >= 2) { + if (tag[0] === "e" && lastETag === void 0) { + lastETag = tag; + } else if (tag[0] === "p" && lastPTag === void 0) { + lastPTag = tag; + } + } + } + if (lastETag === void 0) { + return void 0; + } + return { + id: lastETag[1], + relays: [lastETag[2], lastPTag?.[2]].filter((x) => typeof x === "string"), + author: lastPTag?.[1] + }; + } + function getRepostedEvent(event, { skipVerification } = {}) { + const pointer = getRepostedEventPointer(event); + if (pointer === void 0 || event.content === "") { + return void 0; + } + let repostedEvent; + try { + repostedEvent = JSON.parse(event.content); + } catch (error) { + return void 0; + } + if (repostedEvent.id !== pointer.id) { + return void 0; + } + if (!skipVerification && !verifyEvent(repostedEvent)) { + return void 0; + } + return repostedEvent; + } + + // nip21.ts + var nip21_exports = {}; + __export(nip21_exports, { + NOSTR_URI_REGEX: () => NOSTR_URI_REGEX, + parse: () => parse2, + test: () => test + }); + var NOSTR_URI_REGEX = new RegExp(`nostr:(${BECH32_REGEX.source})`); + function test(value) { + return typeof value === "string" && new RegExp(`^${NOSTR_URI_REGEX.source}$`).test(value); + } + function parse2(uri) { + const match = uri.match(new RegExp(`^${NOSTR_URI_REGEX.source}$`)); + if (!match) + throw new Error(`Invalid Nostr URI: ${uri}`); + return { + uri: match[0], + value: match[1], + decoded: decode(match[1]) + }; + } + + // nip25.ts + var nip25_exports = {}; + __export(nip25_exports, { + finishReactionEvent: () => finishReactionEvent, + getReactedEventPointer: () => getReactedEventPointer + }); + function finishReactionEvent(t, reacted, privateKey) { + const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p")); + return finalizeEvent( + { + ...t, + kind: Reaction, + tags: [...t.tags ?? [], ...inheritedTags, ["e", reacted.id], ["p", reacted.pubkey]], + content: t.content ?? "+" + }, + privateKey + ); + } + function getReactedEventPointer(event) { + if (event.kind !== Reaction) { + return void 0; + } + let lastETag; + let lastPTag; + for (let i2 = event.tags.length - 1; i2 >= 0 && (lastETag === void 0 || lastPTag === void 0); i2--) { + const tag = event.tags[i2]; + if (tag.length >= 2) { + if (tag[0] === "e" && lastETag === void 0) { + lastETag = tag; + } else if (tag[0] === "p" && lastPTag === void 0) { + lastPTag = tag; + } + } + } + if (lastETag === void 0 || lastPTag === void 0) { + return void 0; + } + return { + id: lastETag[1], + relays: [lastETag[2], lastPTag[2]].filter((x) => x !== void 0), + author: lastPTag[1] + }; + } + + // nip27.ts + var nip27_exports = {}; + __export(nip27_exports, { + parse: () => parse3 + }); + var noCharacter = /\W/m; + var noURLCharacter = /\W |\W$|$|,| /m; + function* parse3(content) { + const max = content.length; + let prevIndex = 0; + let index = 0; + while (index < max) { + let u = content.indexOf(":", index); + if (u === -1) { + break; + } + if (content.substring(u - 5, u) === "nostr") { + const m = content.substring(u + 60).match(noCharacter); + const end = m ? u + 60 + m.index : max; + try { + let pointer; + let { data, type } = decode(content.substring(u + 1, end)); + switch (type) { + case "npub": + pointer = { pubkey: data }; + break; + case "nsec": + case "note": + index = end + 1; + continue; + default: + pointer = data; + } + if (prevIndex !== u - 5) { + yield { type: "text", text: content.substring(prevIndex, u - 5) }; + } + yield { type: "reference", pointer }; + index = end; + prevIndex = index; + continue; + } catch (_err) { + index = u + 1; + continue; + } + } else if (content.substring(u - 5, u) === "https" || content.substring(u - 4, u) === "http") { + const m = content.substring(u + 4).match(noURLCharacter); + const end = m ? u + 4 + m.index : max; + const prefixLen = content[u - 1] === "s" ? 5 : 4; + try { + let url = new URL(content.substring(u - prefixLen, end)); + if (url.hostname.indexOf(".") === -1) { + throw new Error("invalid url"); + } + if (prevIndex !== u - prefixLen) { + yield { type: "text", text: content.substring(prevIndex, u - prefixLen) }; + } + if (/\.(png|jpe?g|gif|webp)$/i.test(url.pathname)) { + yield { type: "image", url: url.toString() }; + index = end; + prevIndex = index; + continue; + } + if (/\.(mp4|avi|webm|mkv)$/i.test(url.pathname)) { + yield { type: "video", url: url.toString() }; + index = end; + prevIndex = index; + continue; + } + if (/\.(mp3|aac|ogg|opus)$/i.test(url.pathname)) { + yield { type: "audio", url: url.toString() }; + index = end; + prevIndex = index; + continue; + } + yield { type: "url", url: url.toString() }; + index = end; + prevIndex = index; + continue; + } catch (_err) { + index = end + 1; + continue; + } + } else if (content.substring(u - 3, u) === "wss" || content.substring(u - 2, u) === "ws") { + const m = content.substring(u + 4).match(noURLCharacter); + const end = m ? u + 4 + m.index : max; + const prefixLen = content[u - 1] === "s" ? 3 : 2; + try { + let url = new URL(content.substring(u - prefixLen, end)); + if (url.hostname.indexOf(".") === -1) { + throw new Error("invalid ws url"); + } + if (prevIndex !== u - prefixLen) { + yield { type: "text", text: content.substring(prevIndex, u - prefixLen) }; + } + yield { type: "relay", url: url.toString() }; + index = end; + prevIndex = index; + continue; + } catch (_err) { + index = end + 1; + continue; + } + } else { + index = u + 1; + continue; + } + } + if (prevIndex !== max) { + yield { type: "text", text: content.substring(prevIndex) }; + } + } + + // nip28.ts + var nip28_exports = {}; + __export(nip28_exports, { + channelCreateEvent: () => channelCreateEvent, + channelHideMessageEvent: () => channelHideMessageEvent, + channelMessageEvent: () => channelMessageEvent, + channelMetadataEvent: () => channelMetadataEvent, + channelMuteUserEvent: () => channelMuteUserEvent + }); + var channelCreateEvent = (t, privateKey) => { + let content; + if (typeof t.content === "object") { + content = JSON.stringify(t.content); + } else if (typeof t.content === "string") { + content = t.content; + } else { + return void 0; + } + return finalizeEvent( + { + kind: ChannelCreation, + tags: [...t.tags ?? []], + content, + created_at: t.created_at + }, + privateKey + ); + }; + var channelMetadataEvent = (t, privateKey) => { + let content; + if (typeof t.content === "object") { + content = JSON.stringify(t.content); + } else if (typeof t.content === "string") { + content = t.content; + } else { + return void 0; + } + return finalizeEvent( + { + kind: ChannelMetadata, + tags: [["e", t.channel_create_event_id], ...t.tags ?? []], + content, + created_at: t.created_at + }, + privateKey + ); + }; + var channelMessageEvent = (t, privateKey) => { + const tags = [["e", t.channel_create_event_id, t.relay_url, "root"]]; + if (t.reply_to_channel_message_event_id) { + tags.push(["e", t.reply_to_channel_message_event_id, t.relay_url, "reply"]); + } + return finalizeEvent( + { + kind: ChannelMessage, + tags: [...tags, ...t.tags ?? []], + content: t.content, + created_at: t.created_at + }, + privateKey + ); + }; + var channelHideMessageEvent = (t, privateKey) => { + let content; + if (typeof t.content === "object") { + content = JSON.stringify(t.content); + } else if (typeof t.content === "string") { + content = t.content; + } else { + return void 0; + } + return finalizeEvent( + { + kind: ChannelHideMessage, + tags: [["e", t.channel_message_event_id], ...t.tags ?? []], + content, + created_at: t.created_at + }, + privateKey + ); + }; + var channelMuteUserEvent = (t, privateKey) => { + let content; + if (typeof t.content === "object") { + content = JSON.stringify(t.content); + } else if (typeof t.content === "string") { + content = t.content; + } else { + return void 0; + } + return finalizeEvent( + { + kind: ChannelMuteUser, + tags: [["p", t.pubkey_to_mute], ...t.tags ?? []], + content, + created_at: t.created_at + }, + privateKey + ); + }; + + // nip30.ts + var nip30_exports = {}; + __export(nip30_exports, { + EMOJI_SHORTCODE_REGEX: () => EMOJI_SHORTCODE_REGEX, + matchAll: () => matchAll, + regex: () => regex, + replaceAll: () => replaceAll + }); + var EMOJI_SHORTCODE_REGEX = /:(\w+):/; + var regex = () => new RegExp(`\\B${EMOJI_SHORTCODE_REGEX.source}\\B`, "g"); + function* matchAll(content) { + const matches = content.matchAll(regex()); + for (const match of matches) { + try { + const [shortcode, name] = match; + yield { + shortcode, + name, + start: match.index, + end: match.index + shortcode.length + }; + } catch (_e) { + } + } + } + function replaceAll(content, replacer) { + return content.replaceAll(regex(), (shortcode, name) => { + return replacer({ + shortcode, + name + }); + }); + } + + // nip39.ts + var nip39_exports = {}; + __export(nip39_exports, { + useFetchImplementation: () => useFetchImplementation3, + validateGithub: () => validateGithub + }); + var _fetch3; + try { + _fetch3 = fetch; + } catch { + } + function useFetchImplementation3(fetchImplementation) { + _fetch3 = fetchImplementation; + } + async function validateGithub(pubkey, username, proof) { + try { + let res = await (await _fetch3(`https://gist.github.com/${username}/${proof}/raw`)).text(); + return res === `Verifying that I control the following Nostr public key: ${pubkey}`; + } catch (_) { + return false; + } + } + + // nip46.ts + var nip46_exports = {}; + __export(nip46_exports, { + BUNKER_REGEX: () => BUNKER_REGEX, + BunkerSigner: () => BunkerSigner, + createAccount: () => createAccount, + createNostrConnectURI: () => createNostrConnectURI, + fetchBunkerProviders: () => fetchBunkerProviders, + parseBunkerInput: () => parseBunkerInput, + parseNostrConnectURI: () => parseNostrConnectURI, + queryBunkerProfile: () => queryBunkerProfile, + toBunkerURL: () => toBunkerURL, + useFetchImplementation: () => useFetchImplementation4 + }); + var _fetch4; + try { + _fetch4 = fetch; + } catch { + } + function useFetchImplementation4(fetchImplementation) { + _fetch4 = fetchImplementation; + } + var BUNKER_REGEX = /^bunker:\/\/([0-9a-f]{64})\??([?\/\w:.=&%-]*)$/; + var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + function toBunkerURL(bunkerPointer) { + let bunkerURL = new URL(`bunker://${bunkerPointer.pubkey}`); + bunkerPointer.relays.forEach((relay) => { + bunkerURL.searchParams.append("relay", relay); + }); + if (bunkerPointer.secret) { + bunkerURL.searchParams.set("secret", bunkerPointer.secret); + } + return bunkerURL.toString(); + } + async function parseBunkerInput(input) { + let match = input.match(BUNKER_REGEX); + if (match) { + try { + const pubkey = match[1]; + const qs = new URLSearchParams(match[2]); + return { + pubkey, + relays: qs.getAll("relay"), + secret: qs.get("secret") + }; + } catch (_err) { + } + } + return queryBunkerProfile(input); + } + async function queryBunkerProfile(nip05) { + const match = nip05.match(NIP05_REGEX); + if (!match) + return null; + const [_, name = "_", domain] = match; + try { + const url = `https://${domain}/.well-known/nostr.json?name=${name}`; + const res = await (await _fetch4(url, { redirect: "error" })).json(); + let pubkey = res.names[name]; + let relays = res.nip46[pubkey] || []; + return { pubkey, relays, secret: null }; + } catch (_err) { + return null; + } + } + function createNostrConnectURI(params) { + if (!params.clientPubkey) { + throw new Error("clientPubkey is required."); + } + if (!params.relays || params.relays.length === 0) { + throw new Error("At least one relay is required."); + } + if (!params.secret) { + throw new Error("secret is required."); + } + const queryParams = new URLSearchParams(); + params.relays.forEach((relay) => { + queryParams.append("relay", relay); + }); + queryParams.append("secret", params.secret); + if (params.perms && params.perms.length > 0) { + queryParams.append("perms", params.perms.join(",")); + } + if (params.name) { + queryParams.append("name", params.name); + } + if (params.url) { + queryParams.append("url", params.url); + } + if (params.image) { + queryParams.append("image", params.image); + } + return `nostrconnect://${params.clientPubkey}?${queryParams.toString()}`; + } + function parseNostrConnectURI(uri) { + if (!uri.startsWith("nostrconnect://")) { + throw new Error('Invalid nostrconnect URI: Must start with "nostrconnect://".'); + } + const [protocolAndPubkey, queryString] = uri.split("?"); + if (!protocolAndPubkey || !queryString) { + throw new Error("Invalid nostrconnect URI: Missing query string."); + } + const clientPubkey = protocolAndPubkey.substring("nostrconnect://".length); + if (!clientPubkey) { + throw new Error("Invalid nostrconnect URI: Missing client-pubkey."); + } + const queryParams = new URLSearchParams(queryString); + const relays = queryParams.getAll("relay"); + if (relays.length === 0) { + throw new Error('Invalid nostrconnect URI: Missing "relay" parameter.'); + } + const secret = queryParams.get("secret"); + if (!secret) { + throw new Error('Invalid nostrconnect URI: Missing "secret" parameter.'); + } + const permsString = queryParams.get("perms"); + const perms = permsString ? permsString.split(",") : void 0; + const name = queryParams.get("name") || void 0; + const url = queryParams.get("url") || void 0; + const image = queryParams.get("image") || void 0; + return { + protocol: "nostrconnect", + clientPubkey, + params: { + relays, + secret, + perms, + name, + url, + image + }, + originalString: uri + }; + } + var BunkerSigner = class { + params; + pool; + subCloser; + isOpen; + serial; + idPrefix; + listeners; + waitingForAuth; + secretKey; + conversationKey; + bp; + cachedPubKey; + constructor(clientSecretKey, params) { + this.params = params; + this.pool = params.pool || new SimplePool(); + this.secretKey = clientSecretKey; + this.isOpen = false; + this.idPrefix = Math.random().toString(36).substring(7); + this.serial = 0; + this.listeners = {}; + this.waitingForAuth = {}; + } + static fromBunker(clientSecretKey, bp, params = {}) { + if (bp.relays.length === 0) { + throw new Error("No relays specified for this bunker"); + } + const signer = new BunkerSigner(clientSecretKey, params); + signer.conversationKey = getConversationKey(clientSecretKey, bp.pubkey); + signer.bp = bp; + signer.setupSubscription(params); + return signer; + } + static async fromURI(clientSecretKey, connectionURI, params = {}, maxWait = 3e5) { + const signer = new BunkerSigner(clientSecretKey, params); + const parsedURI = parseNostrConnectURI(connectionURI); + const clientPubkey = getPublicKey(clientSecretKey); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + sub.close(); + reject(new Error(`Connection timed out after ${maxWait / 1e3} seconds`)); + }, maxWait); + const sub = signer.pool.subscribe( + parsedURI.params.relays, + { kinds: [NostrConnect], "#p": [clientPubkey] }, + { + onevent: async (event) => { + try { + const tempConvKey = getConversationKey(clientSecretKey, event.pubkey); + const decryptedContent = decrypt3(event.content, tempConvKey); + const response = JSON.parse(decryptedContent); + if (response.result === parsedURI.params.secret) { + clearTimeout(timer); + sub.close(); + signer.bp = { + pubkey: event.pubkey, + relays: parsedURI.params.relays, + secret: parsedURI.params.secret + }; + signer.conversationKey = getConversationKey(clientSecretKey, event.pubkey); + signer.setupSubscription(params); + resolve(signer); + } + } catch (e) { + console.warn("Failed to process potential connection event", e); + } + }, + onclose: () => { + clearTimeout(timer); + reject(new Error("Subscription closed before connection was established.")); + }, + maxWait + } + ); + }); + } + setupSubscription(params) { + const listeners = this.listeners; + const waitingForAuth = this.waitingForAuth; + const convKey = this.conversationKey; + this.subCloser = this.pool.subscribe( + this.bp.relays, + { kinds: [NostrConnect], authors: [this.bp.pubkey], "#p": [getPublicKey(this.secretKey)] }, + { + onevent: async (event) => { + const o = JSON.parse(decrypt3(event.content, convKey)); + const { id, result, error } = o; + if (result === "auth_url" && waitingForAuth[id]) { + delete waitingForAuth[id]; + if (params.onauth) { + params.onauth(error); + } else { + console.warn( + `nostr-tools/nip46: remote signer ${this.bp.pubkey} tried to send an "auth_url"='${error}' but there was no onauth() callback configured.` + ); + } + return; + } + let handler = listeners[id]; + if (handler) { + if (error) + handler.reject(error); + else if (result) + handler.resolve(result); + delete listeners[id]; + } + }, + onclose: () => { + this.subCloser = void 0; + } + } + ); + this.isOpen = true; + } + async close() { + this.isOpen = false; + this.subCloser.close(); + } + async sendRequest(method, params) { + return new Promise(async (resolve, reject) => { + try { + if (!this.isOpen) + throw new Error("this signer is not open anymore, create a new one"); + if (!this.subCloser) + this.setupSubscription(this.params); + this.serial++; + const id = `${this.idPrefix}-${this.serial}`; + const encryptedContent = encrypt3(JSON.stringify({ id, method, params }), this.conversationKey); + const verifiedEvent = finalizeEvent( + { + kind: NostrConnect, + tags: [["p", this.bp.pubkey]], + content: encryptedContent, + created_at: Math.floor(Date.now() / 1e3) + }, + this.secretKey + ); + this.listeners[id] = { resolve, reject }; + this.waitingForAuth[id] = true; + await Promise.any(this.pool.publish(this.bp.relays, verifiedEvent)); + } catch (err) { + reject(err); + } + }); + } + async ping() { + let resp = await this.sendRequest("ping", []); + if (resp !== "pong") + throw new Error(`result is not pong: ${resp}`); + } + async connect() { + await this.sendRequest("connect", [this.bp.pubkey, this.bp.secret || ""]); + } + async getPublicKey() { + if (!this.cachedPubKey) { + this.cachedPubKey = await this.sendRequest("get_public_key", []); + } + return this.cachedPubKey; + } + async signEvent(event) { + let resp = await this.sendRequest("sign_event", [JSON.stringify(event)]); + let signed = JSON.parse(resp); + if (verifyEvent(signed)) { + return signed; + } else { + throw new Error(`event returned from bunker is improperly signed: ${JSON.stringify(signed)}`); + } + } + async nip04Encrypt(thirdPartyPubkey, plaintext) { + return await this.sendRequest("nip04_encrypt", [thirdPartyPubkey, plaintext]); + } + async nip04Decrypt(thirdPartyPubkey, ciphertext) { + return await this.sendRequest("nip04_decrypt", [thirdPartyPubkey, ciphertext]); + } + async nip44Encrypt(thirdPartyPubkey, plaintext) { + return await this.sendRequest("nip44_encrypt", [thirdPartyPubkey, plaintext]); + } + async nip44Decrypt(thirdPartyPubkey, ciphertext) { + return await this.sendRequest("nip44_decrypt", [thirdPartyPubkey, ciphertext]); + } + }; + async function createAccount(bunker, params, username, domain, email, localSecretKey = generateSecretKey()) { + if (email && !EMAIL_REGEX.test(email)) + throw new Error("Invalid email"); + let rpc = BunkerSigner.fromBunker(localSecretKey, bunker.bunkerPointer, params); + let pubkey = await rpc.sendRequest("create_account", [username, domain, email || ""]); + rpc.bp.pubkey = pubkey; + await rpc.connect(); + return rpc; + } + async function fetchBunkerProviders(pool, relays) { + const events = await pool.querySync(relays, { + kinds: [Handlerinformation], + "#k": [NostrConnect.toString()] + }); + events.sort((a, b) => b.created_at - a.created_at); + const validatedBunkers = await Promise.all( + events.map(async (event, i2) => { + try { + const content = JSON.parse(event.content); + try { + if (events.findIndex((ev) => JSON.parse(ev.content).nip05 === content.nip05) !== i2) + return void 0; + } catch (err) { + } + const bp = await queryBunkerProfile(content.nip05); + if (bp && bp.pubkey === event.pubkey && bp.relays.length) { + return { + bunkerPointer: bp, + nip05: content.nip05, + domain: content.nip05.split("@")[1], + name: content.name || content.display_name, + picture: content.picture, + about: content.about, + website: content.website, + local: false + }; + } + } catch (err) { + return void 0; + } + }) + ); + return validatedBunkers.filter((b) => b !== void 0); + } + + // nip47.ts + var nip47_exports = {}; + __export(nip47_exports, { + makeNwcRequestEvent: () => makeNwcRequestEvent, + parseConnectionString: () => parseConnectionString + }); + function parseConnectionString(connectionString) { + const { host, pathname, searchParams } = new URL(connectionString); + const pubkey = pathname || host; + const relay = searchParams.get("relay"); + const secret = searchParams.get("secret"); + if (!pubkey || !relay || !secret) { + throw new Error("invalid connection string"); + } + return { pubkey, relay, secret }; + } + async function makeNwcRequestEvent(pubkey, secretKey, invoice) { + const content = { + method: "pay_invoice", + params: { + invoice + } + }; + const encryptedContent = encrypt2(secretKey, pubkey, JSON.stringify(content)); + const eventTemplate = { + kind: NWCWalletRequest, + created_at: Math.round(Date.now() / 1e3), + content: encryptedContent, + tags: [["p", pubkey]] + }; + return finalizeEvent(eventTemplate, secretKey); + } + + // nip54.ts + var nip54_exports = {}; + __export(nip54_exports, { + normalizeIdentifier: () => normalizeIdentifier + }); + function normalizeIdentifier(name) { + name = name.trim().toLowerCase(); + name = name.normalize("NFKC"); + return Array.from(name).map((char) => { + if (/\p{Letter}/u.test(char) || /\p{Number}/u.test(char)) { + return char; + } + return "-"; + }).join(""); + } + + // nip57.ts + var nip57_exports = {}; + __export(nip57_exports, { + getSatoshisAmountFromBolt11: () => getSatoshisAmountFromBolt11, + getZapEndpoint: () => getZapEndpoint, + makeZapReceipt: () => makeZapReceipt, + makeZapRequest: () => makeZapRequest, + useFetchImplementation: () => useFetchImplementation5, + validateZapRequest: () => validateZapRequest + }); + var _fetch5; + try { + _fetch5 = fetch; + } catch { + } + function useFetchImplementation5(fetchImplementation) { + _fetch5 = fetchImplementation; + } + async function getZapEndpoint(metadata) { + try { + let lnurl = ""; + let { lud06, lud16 } = JSON.parse(metadata.content); + if (lud06) { + let { words } = bech32.decode(lud06, 1e3); + let data = bech32.fromWords(words); + lnurl = utf8Decoder.decode(data); + } else if (lud16) { + let [name, domain] = lud16.split("@"); + lnurl = new URL(`/.well-known/lnurlp/${name}`, `https://${domain}`).toString(); + } else { + return null; + } + let res = await _fetch5(lnurl); + let body = await res.json(); + if (body.allowsNostr && body.nostrPubkey) { + return body.callback; + } + } catch (err) { + } + return null; + } + function makeZapRequest(params) { + let zr = { + kind: 9734, + created_at: Math.round(Date.now() / 1e3), + content: params.comment || "", + tags: [ + ["p", "pubkey" in params ? params.pubkey : params.event.pubkey], + ["amount", params.amount.toString()], + ["relays", ...params.relays] + ] + }; + if ("event" in params) { + zr.tags.push(["e", params.event.id]); + if (isReplaceableKind(params.event.kind)) { + const a = ["a", `${params.event.kind}:${params.event.pubkey}:`]; + zr.tags.push(a); + } else if (isAddressableKind(params.event.kind)) { + let d = params.event.tags.find(([t, v]) => t === "d" && v); + if (!d) + throw new Error("d tag not found or is empty"); + const a = ["a", `${params.event.kind}:${params.event.pubkey}:${d[1]}`]; + zr.tags.push(a); + } + zr.tags.push(["k", params.event.kind.toString()]); + } + return zr; + } + function validateZapRequest(zapRequestString) { + let zapRequest; + try { + zapRequest = JSON.parse(zapRequestString); + } catch (err) { + return "Invalid zap request JSON."; + } + if (!validateEvent(zapRequest)) + return "Zap request is not a valid Nostr event."; + if (!verifyEvent(zapRequest)) + return "Invalid signature on zap request."; + let p = zapRequest.tags.find(([t, v]) => t === "p" && v); + if (!p) + return "Zap request doesn't have a 'p' tag."; + if (!p[1].match(/^[a-f0-9]{64}$/)) + return "Zap request 'p' tag is not valid hex."; + let e = zapRequest.tags.find(([t, v]) => t === "e" && v); + if (e && !e[1].match(/^[a-f0-9]{64}$/)) + return "Zap request 'e' tag is not valid hex."; + let relays = zapRequest.tags.find(([t, v]) => t === "relays" && v); + if (!relays) + return "Zap request doesn't have a 'relays' tag."; + return null; + } + function makeZapReceipt({ + zapRequest, + preimage, + bolt11, + paidAt + }) { + let zr = JSON.parse(zapRequest); + let tagsFromZapRequest = zr.tags.filter(([t]) => t === "e" || t === "p" || t === "a"); + let zap = { + kind: 9735, + created_at: Math.round(paidAt.getTime() / 1e3), + content: "", + tags: [...tagsFromZapRequest, ["P", zr.pubkey], ["bolt11", bolt11], ["description", zapRequest]] + }; + if (preimage) { + zap.tags.push(["preimage", preimage]); + } + return zap; + } + function getSatoshisAmountFromBolt11(bolt11) { + if (bolt11.length < 50) { + return 0; + } + bolt11 = bolt11.substring(0, 50); + const idx = bolt11.lastIndexOf("1"); + if (idx === -1) { + return 0; + } + const hrp = bolt11.substring(0, idx); + if (!hrp.startsWith("lnbc")) { + return 0; + } + const amount = hrp.substring(4); + if (amount.length < 1) { + return 0; + } + const char = amount[amount.length - 1]; + const digit = char.charCodeAt(0) - "0".charCodeAt(0); + const isDigit = digit >= 0 && digit <= 9; + let cutPoint = amount.length - 1; + if (isDigit) { + cutPoint++; + } + if (cutPoint < 1) { + return 0; + } + const num = parseInt(amount.substring(0, cutPoint)); + switch (char) { + case "m": + return num * 1e5; + case "u": + return num * 100; + case "n": + return num / 10; + case "p": + return num / 1e4; + default: + return num * 1e8; + } + } + + // nip98.ts + var nip98_exports = {}; + __export(nip98_exports, { + getToken: () => getToken, + hashPayload: () => hashPayload, + unpackEventFromToken: () => unpackEventFromToken, + validateEvent: () => validateEvent2, + validateEventKind: () => validateEventKind, + validateEventMethodTag: () => validateEventMethodTag, + validateEventPayloadTag: () => validateEventPayloadTag, + validateEventTimestamp: () => validateEventTimestamp, + validateEventUrlTag: () => validateEventUrlTag, + validateToken: () => validateToken + }); + var _authorizationScheme = "Nostr "; + async function getToken(loginUrl, httpMethod, sign, includeAuthorizationScheme = false, payload) { + const event = { + kind: HTTPAuth, + tags: [ + ["u", loginUrl], + ["method", httpMethod] + ], + created_at: Math.round(new Date().getTime() / 1e3), + content: "" + }; + if (payload) { + event.tags.push(["payload", hashPayload(payload)]); + } + const signedEvent = await sign(event); + const authorizationScheme = includeAuthorizationScheme ? _authorizationScheme : ""; + return authorizationScheme + base64.encode(utf8Encoder.encode(JSON.stringify(signedEvent))); + } + async function validateToken(token, url, method) { + const event = await unpackEventFromToken(token).catch((error) => { + throw error; + }); + const valid = await validateEvent2(event, url, method).catch((error) => { + throw error; + }); + return valid; + } + async function unpackEventFromToken(token) { + if (!token) { + throw new Error("Missing token"); + } + token = token.replace(_authorizationScheme, ""); + const eventB64 = utf8Decoder.decode(base64.decode(token)); + if (!eventB64 || eventB64.length === 0 || !eventB64.startsWith("{")) { + throw new Error("Invalid token"); + } + const event = JSON.parse(eventB64); + return event; + } + function validateEventTimestamp(event) { + if (!event.created_at) { + return false; + } + return Math.round(new Date().getTime() / 1e3) - event.created_at < 60; + } + function validateEventKind(event) { + return event.kind === HTTPAuth; + } + function validateEventUrlTag(event, url) { + const urlTag = event.tags.find((t) => t[0] === "u"); + if (!urlTag) { + return false; + } + return urlTag.length > 0 && urlTag[1] === url; + } + function validateEventMethodTag(event, method) { + const methodTag = event.tags.find((t) => t[0] === "method"); + if (!methodTag) { + return false; + } + return methodTag.length > 0 && methodTag[1].toLowerCase() === method.toLowerCase(); + } + function hashPayload(payload) { + const hash3 = sha2562(utf8Encoder.encode(JSON.stringify(payload))); + return bytesToHex2(hash3); + } + function validateEventPayloadTag(event, payload) { + const payloadTag = event.tags.find((t) => t[0] === "payload"); + if (!payloadTag) { + return false; + } + const payloadHash = hashPayload(payload); + return payloadTag.length > 0 && payloadTag[1] === payloadHash; + } + async function validateEvent2(event, url, method, body) { + if (!verifyEvent(event)) { + throw new Error("Invalid nostr event, signature invalid"); + } + if (!validateEventKind(event)) { + throw new Error("Invalid nostr event, kind invalid"); + } + if (!validateEventTimestamp(event)) { + throw new Error("Invalid nostr event, created_at timestamp invalid"); + } + if (!validateEventUrlTag(event, url)) { + throw new Error("Invalid nostr event, url tag invalid"); + } + if (!validateEventMethodTag(event, method)) { + throw new Error("Invalid nostr event, method tag invalid"); + } + if (Boolean(body) && typeof body === "object" && Object.keys(body).length > 0) { + if (!validateEventPayloadTag(event, body)) { + throw new Error("Invalid nostr event, payload tag does not match request body hash"); + } + } + return true; + } + return __toCommonJS(nostr_tools_exports); +})(); diff --git a/api/text_graph.js b/api/text_graph.js new file mode 100644 index 0000000..059d7f4 --- /dev/null +++ b/api/text_graph.js @@ -0,0 +1,463 @@ +/** + * ASCIIBarChart - A dynamic ASCII-based vertical bar chart renderer + * + * Creates real-time animated bar charts using monospaced characters (X) + * with automatic scaling, labels, and responsive font sizing. + */ +class ASCIIBarChart { + /** + * Create a new ASCII bar chart + * @param {string} containerId - The ID of the HTML element to render the chart in + * @param {Object} options - Configuration options + * @param {number} [options.maxHeight=20] - Maximum height of the chart in rows + * @param {number} [options.maxDataPoints=30] - Maximum number of data columns before scrolling + * @param {string} [options.title=''] - Chart title (displayed centered at top) + * @param {string} [options.xAxisLabel=''] - X-axis label (displayed centered at bottom) + * @param {string} [options.yAxisLabel=''] - Y-axis label (displayed vertically on left) + * @param {boolean} [options.autoFitWidth=true] - Automatically adjust font size to fit container width + * @param {boolean} [options.useBinMode=false] - Enable time bin mode for data aggregation + * @param {number} [options.binDuration=10000] - Duration of each time bin in milliseconds (10 seconds default) + * @param {string} [options.xAxisLabelFormat='elapsed'] - X-axis label format: 'elapsed', 'bins', 'timestamps', 'ranges' + * @param {boolean} [options.debug=false] - Enable debug logging + */ + constructor(containerId, options = {}) { + this.container = document.getElementById(containerId); + this.data = []; + this.maxHeight = options.maxHeight || 20; + this.maxDataPoints = options.maxDataPoints || 30; + this.totalDataPoints = 0; // Track total number of data points added + this.title = options.title || ''; + this.xAxisLabel = options.xAxisLabel || ''; + this.yAxisLabel = options.yAxisLabel || ''; + this.autoFitWidth = options.autoFitWidth !== false; // Default to true + this.debug = options.debug || false; // Debug logging option + + // Time bin configuration + this.useBinMode = options.useBinMode !== false; // Default to true + this.binDuration = options.binDuration || 4000; // 4 seconds default + this.xAxisLabelFormat = options.xAxisLabelFormat || 'elapsed'; + + // Time bin data structures + this.bins = []; + this.currentBinIndex = -1; + this.binStartTime = null; + this.binCheckInterval = null; + this.chartStartTime = Date.now(); + + // Set up resize observer if auto-fit is enabled + if (this.autoFitWidth) { + this.resizeObserver = new ResizeObserver(() => { + this.adjustFontSize(); + }); + this.resizeObserver.observe(this.container); + } + + // Initialize first bin if bin mode is enabled + if (this.useBinMode) { + this.initializeBins(); + } + } + + /** + * Add a new data point to the chart + * @param {number} value - The numeric value to add + */ + addValue(value) { + // Time bin mode: add value to current active bin count + this.checkBinRotation(); // Ensure we have an active bin + this.bins[this.currentBinIndex].count += value; // Changed from ++ to += value + this.totalDataPoints++; + + this.render(); + this.updateInfo(); + } + + /** + * Clear all data from the chart + */ + clear() { + this.data = []; + this.totalDataPoints = 0; + + if (this.useBinMode) { + this.bins = []; + this.currentBinIndex = -1; + this.binStartTime = null; + this.initializeBins(); + } + + this.render(); + this.updateInfo(); + } + + /** + * Calculate the width of the chart in characters + * @returns {number} The chart width in characters + * @private + */ + getChartWidth() { + let dataLength = this.maxDataPoints; // Always use maxDataPoints for consistent width + + if (dataLength === 0) return 50; // Default width for empty chart + + const yAxisPadding = this.yAxisLabel ? 2 : 0; + const yAxisNumbers = 3; // Width of Y-axis numbers + const separator = 1; // The '|' character + // const dataWidth = dataLength * 2; // Each column is 2 characters wide // TEMP: commented for no-space test + const dataWidth = dataLength; // Each column is 1 character wide // TEMP: adjusted for no-space columns + const padding = 1; // Extra padding + + const totalWidth = yAxisPadding + yAxisNumbers + separator + dataWidth + padding; + + // Only log when width changes + if (this.debug && this.lastChartWidth !== totalWidth) { + console.log('getChartWidth changed:', { dataLength, totalWidth, previous: this.lastChartWidth }); + this.lastChartWidth = totalWidth; + } + + return totalWidth; + } + + /** + * Adjust font size to fit container width + * @private + */ + adjustFontSize() { + if (!this.autoFitWidth) return; + + const containerWidth = this.container.clientWidth; + const chartWidth = this.getChartWidth(); + + if (chartWidth === 0) return; + + // Calculate optimal font size + // For monospace fonts, character width is approximately 0.6 * font size + // Use a slightly smaller ratio to fit more content + const charWidthRatio = 0.7; + const padding = 30; // Reduce padding to fit more content + const availableWidth = containerWidth - padding; + const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio); + + // Set reasonable bounds (min 4px, max 20px) + const fontSize = Math.max(4, Math.min(20, optimalFontSize)); + + // Only log when font size changes + if (this.debug && this.lastFontSize !== fontSize) { + console.log('fontSize changed:', { containerWidth, chartWidth, fontSize, previous: this.lastFontSize }); + this.lastFontSize = fontSize; + } + + this.container.style.fontSize = fontSize + 'px'; + this.container.style.lineHeight = '1.0'; + } + + /** + * Render the chart to the container + * @private + */ + render() { + let dataToRender = []; + let maxValue = 0; + let minValue = 0; + let valueRange = 0; + + if (this.useBinMode) { + // Bin mode: render bin counts + if (this.bins.length === 0) { + this.container.textContent = 'No data yet. Click Start to begin.'; + return; + } + // Always create a fixed-length array filled with 0s, then overlay actual bin data + dataToRender = new Array(this.maxDataPoints).fill(0); + + // Overlay actual bin data (most recent bins, reversed for left-to-right display) + const startIndex = Math.max(0, this.bins.length - this.maxDataPoints); + const recentBins = this.bins.slice(startIndex); + + // Reverse the bins so most recent is on the left, and overlay onto the fixed array + recentBins.reverse().forEach((bin, index) => { + if (index < this.maxDataPoints) { + dataToRender[index] = bin.count; + } + }); + + if (this.debug) { + console.log('render() dataToRender:', dataToRender, 'bins length:', this.bins.length); + } + maxValue = Math.max(...dataToRender); + minValue = Math.min(...dataToRender); + valueRange = maxValue - minValue; + } else { + // Legacy mode: render individual values + if (this.data.length === 0) { + this.container.textContent = 'No data yet. Click Start to begin.'; + return; + } + dataToRender = this.data; + maxValue = Math.max(...this.data); + minValue = Math.min(...this.data); + valueRange = maxValue - minValue; + } + + let output = ''; + const scale = this.maxHeight; + + // Calculate scaling factor: each X represents at least 1 count + const maxCount = Math.max(...dataToRender); + const scaleFactor = Math.max(1, Math.ceil(maxCount / scale)); // 1 X = scaleFactor counts + const scaledMax = Math.ceil(maxCount / scaleFactor) * scaleFactor; + + // Calculate Y-axis label width (for vertical text) + const yLabelWidth = this.yAxisLabel ? 2 : 0; + const yAxisPadding = this.yAxisLabel ? ' ' : ''; + + // Add title if provided (centered) + if (this.title) { + // const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns // TEMP: commented for no-space test + const chartWidth = 4 + this.maxDataPoints; // Y-axis numbers + data columns // TEMP: adjusted for no-space columns + const titlePadding = Math.floor((chartWidth - this.title.length) / 2); + output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n'; + } + + // Draw from top to bottom + for (let row = scale; row > 0; row--) { + let line = ''; + + // Add vertical Y-axis label character + if (this.yAxisLabel) { + const L = this.yAxisLabel.length; + const startRow = Math.floor((scale - L) / 2) + 1; + const relativeRow = scale - row + 1; // 1 at top, scale at bottom + if (relativeRow >= startRow && relativeRow < startRow + L) { + const labelIndex = relativeRow - startRow; + line += this.yAxisLabel[labelIndex] + ' '; + } else { + line += ' '; + } + } + + // Calculate the actual count value this row represents (1 at bottom, increasing upward) + const rowCount = (row - 1) * scaleFactor + 1; + + // Add Y-axis label (show actual count values) + line += String(rowCount).padStart(3, ' ') + ' |'; + + // Draw each column + for (let i = 0; i < dataToRender.length; i++) { + const count = dataToRender[i]; + const scaledHeight = Math.ceil(count / scaleFactor); + + if (scaledHeight >= row) { + // line += ' X'; // TEMP: commented out space between columns + line += 'X'; // TEMP: no space between columns + } else { + // line += ' '; // TEMP: commented out space between columns + line += ' '; // TEMP: single space for empty columns + } + } + + output += line + '\n'; + } + + // Draw X-axis + // output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints * 2) + '\n'; // TEMP: commented out for no-space test + output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints) + '\n'; // TEMP: back to original length + + // Draw X-axis labels based on mode and format + let xAxisLabels = yAxisPadding + ' '; // Initial padding to align with X-axis + + // Determine label interval (every 5 columns) + const labelInterval = 5; + + // Generate all labels first and store in array + let labels = []; + for (let i = 0; i < this.maxDataPoints; i++) { + if (i % labelInterval === 0) { + let label = ''; + if (this.useBinMode) { + // For bin mode, show labels for all possible positions + // i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest) + const elapsedSec = (i * this.binDuration) / 1000; + // Format with appropriate precision for sub-second bins + if (this.binDuration < 1000) { + // Show decimal seconds for sub-second bins + label = elapsedSec.toFixed(1) + 's'; + } else { + // Show whole seconds for 1+ second bins + label = String(Math.round(elapsedSec)) + 's'; + } + } else { + // For legacy mode, show data point numbers + const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1); + label = String(startIndex + i); + } + labels.push(label); + } + } + + // Build the label string with calculated spacing + for (let i = 0; i < labels.length; i++) { + const label = labels[i]; + xAxisLabels += label; + + // Add spacing: labelInterval - label.length (except for last label) + if (i < labels.length - 1) { + const spacing = labelInterval - label.length; + xAxisLabels += ' '.repeat(spacing); + } + } + + // Ensure the label line extends to match the X-axis dash line length + // The dash line is this.maxDataPoints characters long, starting after " +" + const dashLineLength = this.maxDataPoints; + const minLabelLineLength = yAxisPadding.length + 4 + dashLineLength; // 4 for " " + if (xAxisLabels.length < minLabelLineLength) { + xAxisLabels += ' '.repeat(minLabelLineLength - xAxisLabels.length); + } + output += xAxisLabels + '\n'; + + // Add X-axis label if provided + if (this.xAxisLabel) { + // const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2); // TEMP: commented for no-space test + const labelPadding = Math.floor((this.maxDataPoints - this.xAxisLabel.length) / 2); // TEMP: adjusted for no-space columns + output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n'; + } + + this.container.textContent = output; + + // Adjust font size to fit width (only once at initialization) + if (this.autoFitWidth) { + this.adjustFontSize(); + } + + // Update the external info display + if (this.useBinMode) { + const binCounts = this.bins.map(bin => bin.count); + const scaleFactor = Math.max(1, Math.ceil(maxValue / scale)); + document.getElementById('values').textContent = `[${dataToRender.join(', ')}]`; + document.getElementById('max-value').textContent = maxValue; + document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, 1X=${scaleFactor} counts`; + } else { + document.getElementById('values').textContent = `[${this.data.join(', ')}]`; + document.getElementById('max-value').textContent = maxValue; + document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`; + } + } + + /** + * Update the info display + * @private + */ + updateInfo() { + if (this.useBinMode) { + const totalCount = this.bins.reduce((sum, bin) => sum + bin.count, 0); + document.getElementById('count').textContent = totalCount; + } else { + document.getElementById('count').textContent = this.data.length; + } + } + + /** + * Initialize the bin system + * @private + */ + initializeBins() { + this.bins = []; + this.currentBinIndex = -1; + this.binStartTime = null; + this.chartStartTime = Date.now(); + + // Create first bin + this.rotateBin(); + + // Set up automatic bin rotation check + this.binCheckInterval = setInterval(() => { + this.checkBinRotation(); + }, 100); // Check every 100ms for responsiveness + } + + /** + * Check if current bin should rotate and create new bin if needed + * @private + */ + checkBinRotation() { + if (!this.useBinMode || !this.binStartTime) return; + + const now = Date.now(); + if ((now - this.binStartTime) >= this.binDuration) { + this.rotateBin(); + } + } + + /** + * Rotate to a new bin, finalizing the current one + */ + rotateBin() { + // Finalize current bin if it exists + if (this.currentBinIndex >= 0) { + this.bins[this.currentBinIndex].isActive = false; + } + + // Create new bin + const newBin = { + startTime: Date.now(), + count: 0, + isActive: true + }; + + this.bins.push(newBin); + this.currentBinIndex = this.bins.length - 1; + this.binStartTime = newBin.startTime; + + // Keep only the most recent bins + if (this.bins.length > this.maxDataPoints) { + this.bins.shift(); + this.currentBinIndex--; + } + + // Ensure currentBinIndex points to the last bin (the active one) + this.currentBinIndex = this.bins.length - 1; + + // Force a render to update the display immediately + this.render(); + this.updateInfo(); + } + + /** + * Format X-axis label for a bin based on the configured format + * @param {number} binIndex - Index of the bin + * @returns {string} Formatted label + * @private + */ + formatBinLabel(binIndex) { + const bin = this.bins[binIndex]; + if (!bin) return ' '; + + switch (this.xAxisLabelFormat) { + case 'bins': + return String(binIndex + 1).padStart(2, ' '); + + case 'timestamps': + const time = new Date(bin.startTime); + return time.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).replace(/:/g, ''); + + case 'ranges': + const startSec = Math.floor((bin.startTime - this.chartStartTime) / 1000); + const endSec = startSec + Math.floor(this.binDuration / 1000); + return `${startSec}-${endSec}`; + + case 'elapsed': + default: + // For elapsed time, always show time relative to the first bin (index 0) + // This keeps the leftmost label as 0s and increases to the right + const firstBinTime = this.bins[0] ? this.bins[0].startTime : this.chartStartTime; + const elapsedSec = Math.floor((bin.startTime - firstBinTime) / 1000); + return String(elapsedSec).padStart(2, ' ') + 's'; + } + } +} \ No newline at end of file diff --git a/build/admin_event.o b/build/admin_event.o index 5c91026f83bce82ee1c8baa3c4c783b3de9c50a8..f699305b5791a6d13656326cc42189a4cf20b8e0 100644 GIT binary patch literal 18640 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 diff --git a/build/admin_interface.o b/build/admin_interface.o new file mode 100644 index 0000000000000000000000000000000000000000..f554ac3a4c8b4facded603ce6c3391dc979dd34e GIT binary patch literal 760064 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

KbrJ9Q?*5zr!$;ShrL0UoV{_8w`sRu zOp3{H)R}gc!{sQ$jpiqGJm`aXv07!kQ!Kl}c08CD!#S;5meXHA>BVY3n9lpfbeZ+6 zL9Yk+2SFk6qO!4oJ=~yqCXf62h&N;2djQ( z*&B=pgAv`QQ%uWpx^$~)X9SzfW{d7Dqd|8v?a#a2;cN_Jb>?}@vIlk@5urOpB$dz# z7=A=&T&%{+4kYyz_P(vmzkv&T~L(Xrb@pLfk4Oa-W zQL!3kv>x~8ak{eIW306?j*xt(H+xj z^HCwf7M)RlIO{*X zVm^iXGeq#H=ufgX?ae1FCGSkT@XlVe<9)83de9c

  • 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¬7re!n4%y}aHJXsLX^({WwH0u2V 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

    (#ku&$C%ZWAV>vk`)+Gyl{4g7 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