diff --git a/client/index.html b/client/index.html
index 4c5fb15..cba0dc0 100644
--- a/client/index.html
+++ b/client/index.html
@@ -21,9 +21,17 @@
     <h2>Server Information:</h2>
     <div style="margin-bottom: 15px;">
         <div style="margin-bottom: 10px;">
-            <label for="serverPubkey">Server Pubkey:</label><br>
-            <input type="text" id="serverPubkey" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" style="width: 100%; padding: 8px;">
+            <label for="serverPubkey">Server Pubkey or Search Term:</label><br>
+            <div class="server-input-container">
+                <input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" class="server-input">
+                <button id="searchServerBtn" class="server-search-button">Search</button>
+            </div>
+            <div id="serverSearchResult" class="server-search-result" style="display: none;">
+                <!-- Search results will be shown here -->
+            </div>
         </div>
+        
+        
         <div>
             <label for="relay">Response Relay (optional):</label><br>
             <input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
@@ -42,6 +50,23 @@ User-Agent: Browser/1.0
     <div id="output" hidden>
         <h2>Converted Event:</h2>
         <pre id="eventOutput"></pre>
+        
+        <div class="publish-container">
+            <h2>Publish to Relay:</h2>
+            <div class="publish-input-container">
+                <input type="text" id="publishRelay" value="wss://relay.nostrdev.com" placeholder="wss://relay.example.com" class="publish-input">
+                <button id="publishButton" class="publish-button">Publish Event</button>
+            </div>
+            <div id="publishResult" class="publish-result" style="display: none;">
+                <!-- Publish results will be shown here -->
+            </div>
+        </div>
+        
+        <div class="qr-container">
+            <h2>QR Code:</h2>
+            <div id="qrCode"></div>
+            <p><small>Scan this QR code to share the Nostr event</small></p>
+        </div>
     </div>
 </body>
 </html>
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index de1d90c..94b148e 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -9,9 +9,13 @@
       "version": "1.0.0",
       "license": "MIT",
       "dependencies": {
-        "nostr-tools": "^2.12.0"
+        "nostr-tools": "^2.12.0",
+        "qrcode": "^1.5.4",
+        "qrcode-generator": "^1.4.4"
       },
       "devDependencies": {
+        "@types/qrcode": "^1.5.5",
+        "@types/qrcode-generator": "^1.0.6",
         "@typescript-eslint/eslint-plugin": "^8.29.0",
         "@typescript-eslint/parser": "^8.29.0",
         "buffer": "^6.0.3",
@@ -21,6 +25,7 @@
         "eslint": "^9.24.0",
         "eslint-plugin-import": "^2.31.0",
         "husky": "^9.1.7",
+        "node-polyfill-webpack-plugin": "^4.1.0",
         "process": "^0.11.10",
         "serve": "^14.0.0",
         "stream-browserify": "^3.0.0",
@@ -728,6 +733,25 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/qrcode": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
+      "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/qrcode-generator": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/qrcode-generator/-/qrcode-generator-1.0.6.tgz",
+      "integrity": "sha512-XasuPjhHBC4hyOJ/pHaUNTj+tNxA1SyZpXaS/FOUxEVX03D1gFM8UmMKSIs+pPHLAmRZpU6j9KYxvo+lfsvhKw==",
+      "deprecated": "This is a stub types definition for qrcode-generator (https://github.com/kazuhikoarase/qrcode-generator). qrcode-generator provides its own type definitions, so you don't need @types/qrcode-generator installed!",
+      "dev": true,
+      "dependencies": {
+        "qrcode-generator": "*"
+      }
+    },
     "node_modules/@types/qs": {
       "version": "6.9.18",
       "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@@ -1661,6 +1685,19 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/assert": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
+      "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "is-nan": "^1.3.2",
+        "object-is": "^1.1.5",
+        "object.assign": "^4.1.4",
+        "util": "^0.12.5"
+      }
+    },
     "node_modules/async-function": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@@ -1836,6 +1873,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/browser-resolve": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz",
+      "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==",
+      "dev": true,
+      "dependencies": {
+        "resolve": "^1.17.0"
+      }
+    },
     "node_modules/browserify-aes": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -2002,6 +2048,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "dependencies": {
+        "pako": "~1.0.5"
+      }
+    },
     "node_modules/browserslist": {
       "version": "4.24.4",
       "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
@@ -2074,6 +2129,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
+      "dev": true
+    },
     "node_modules/bundle-name": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
@@ -2358,6 +2419,80 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/cliui/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/clone-deep": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@@ -2377,7 +2512,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
       "dependencies": {
         "color-name": "~1.1.4"
       },
@@ -2388,8 +2522,7 @@
     "node_modules/color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
     "node_modules/colorette": {
       "version": "2.0.20",
@@ -2451,6 +2584,18 @@
         "node": ">=0.8"
       }
     },
+    "node_modules/console-browserify": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+      "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
+      "dev": true
+    },
+    "node_modules/constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==",
+      "dev": true
+    },
     "node_modules/content-disposition": {
       "version": "0.5.2",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
@@ -2565,6 +2710,12 @@
         "sha.js": "^2.4.8"
       }
     },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2715,6 +2866,14 @@
         "ms": "2.0.0"
       }
     },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/deep-extend": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -2865,6 +3024,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/dijkstrajs": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
+    },
     "node_modules/dns-packet": {
       "version": "5.6.1",
       "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -2890,6 +3054,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/domain-browser": {
+      "version": "4.22.0",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
+      "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://bevry.me/fund"
+      }
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3961,6 +4137,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
     "node_modules/get-intrinsic": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -4368,6 +4552,12 @@
         }
       }
     },
+    "node_modules/https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==",
+      "dev": true
+    },
     "node_modules/human-signals": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -4551,6 +4741,22 @@
         "node": ">= 10"
       }
     },
+    "node_modules/is-arguments": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+      "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-array-buffer": {
       "version": "3.0.5",
       "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4734,7 +4940,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -4816,6 +5021,22 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-nan": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+      "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.0",
+        "define-properties": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-network-error": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
@@ -5074,6 +5295,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/isomorphic-timers-promises": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz",
+      "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/jest-worker": {
       "version": "27.5.1",
       "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@@ -5506,6 +5736,34 @@
         "node": ">= 6.13.0"
       }
     },
+    "node_modules/node-polyfill-webpack-plugin": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.1.0.tgz",
+      "integrity": "sha512-b4ei444EKkOagG/yFqojrD3QTYM5IOU1f8tn9o6uwrG4qL+brI7oVhjPVd0ZL2xy+Z6CP5bu9w8XTvlWgiXHcw==",
+      "dev": true,
+      "dependencies": {
+        "node-stdlib-browser": "^1.3.0",
+        "type-fest": "^4.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "webpack": ">=5"
+      }
+    },
+    "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": {
+      "version": "4.39.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz",
+      "integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==",
+      "dev": true,
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/node-releases": {
       "version": "2.0.19",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@@ -5513,6 +5771,86 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/node-stdlib-browser": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz",
+      "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==",
+      "dev": true,
+      "dependencies": {
+        "assert": "^2.0.0",
+        "browser-resolve": "^2.0.0",
+        "browserify-zlib": "^0.2.0",
+        "buffer": "^5.7.1",
+        "console-browserify": "^1.1.0",
+        "constants-browserify": "^1.0.0",
+        "create-require": "^1.1.1",
+        "crypto-browserify": "^3.12.1",
+        "domain-browser": "4.22.0",
+        "events": "^3.0.0",
+        "https-browserify": "^1.0.0",
+        "isomorphic-timers-promises": "^1.0.1",
+        "os-browserify": "^0.3.0",
+        "path-browserify": "^1.0.1",
+        "pkg-dir": "^5.0.0",
+        "process": "^0.11.10",
+        "punycode": "^1.4.1",
+        "querystring-es3": "^0.2.1",
+        "readable-stream": "^3.6.0",
+        "stream-browserify": "^3.0.0",
+        "stream-http": "^3.2.0",
+        "string_decoder": "^1.0.0",
+        "timers-browserify": "^2.0.4",
+        "tty-browserify": "0.0.1",
+        "url": "^0.11.4",
+        "util": "^0.12.4",
+        "vm-browserify": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/node-stdlib-browser/node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/node-stdlib-browser/node_modules/pkg-dir": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
+      "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/node-stdlib-browser/node_modules/punycode": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+      "dev": true
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -5579,6 +5917,22 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/object-is": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+      "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/object-keys": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -5754,6 +6108,12 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==",
+      "dev": true
+    },
     "node_modules/own-keys": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -5823,12 +6183,17 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
       "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
       }
     },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+      "dev": true
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5890,11 +6255,16 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
       "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -6031,6 +6401,14 @@
         "node": ">=8"
       }
     },
+    "node_modules/pngjs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/possible-typed-array-names": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -6234,6 +6612,27 @@
         "node": ">=6"
       }
     },
+    "node_modules/qrcode": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+      "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+      "dependencies": {
+        "dijkstrajs": "^1.0.1",
+        "pngjs": "^5.0.0",
+        "yargs": "^15.3.1"
+      },
+      "bin": {
+        "qrcode": "bin/qrcode"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/qrcode-generator": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
+      "integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw=="
+    },
     "node_modules/qs": {
       "version": "6.13.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -6250,6 +6649,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.4.x"
+      }
+    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6446,6 +6854,14 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/require-from-string": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -6455,6 +6871,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+    },
     "node_modules/requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -6907,6 +7328,11 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+    },
     "node_modules/set-function-length": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -6953,6 +7379,12 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+      "dev": true
+    },
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -7255,6 +7687,18 @@
         "readable-stream": "^3.5.0"
       }
     },
+    "node_modules/stream-http": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz",
+      "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==",
+      "dev": true,
+      "dependencies": {
+        "builtin-status-codes": "^3.0.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.6.0",
+        "xtend": "^4.0.2"
+      }
+    },
     "node_modules/string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -7526,6 +7970,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/timers-browserify": {
+      "version": "2.0.12",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
+      "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
+      "dev": true,
+      "dependencies": {
+        "setimmediate": "^1.0.4"
+      },
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
     "node_modules/tinyglobby": {
       "version": "0.2.12",
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
@@ -7695,6 +8151,12 @@
       "dev": true,
       "license": "0BSD"
     },
+    "node_modules/tty-browserify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
+      "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==",
+      "dev": true
+    },
     "node_modules/type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -7905,6 +8367,38 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/url": {
+      "version": "0.11.4",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
+      "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
+      "dev": true,
+      "dependencies": {
+        "punycode": "^1.4.1",
+        "qs": "^6.12.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/url/node_modules/punycode": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+      "dev": true
+    },
+    "node_modules/util": {
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+      "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "is-arguments": "^1.0.4",
+        "is-generator-function": "^1.0.7",
+        "is-typed-array": "^1.1.3",
+        "which-typed-array": "^1.1.2"
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -7941,6 +8435,12 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/vm-browserify": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+      "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+      "dev": true
+    },
     "node_modules/watchpack": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
@@ -8316,6 +8816,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
+    },
     "node_modules/which-typed-array": {
       "version": "1.1.19",
       "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
@@ -8407,6 +8912,146 @@
         }
       }
     },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
+    },
+    "node_modules/yargs": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+      "dependencies": {
+        "cliui": "^6.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^4.1.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^4.2.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^18.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "dependencies": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/yargs-parser/node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/yargs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/yargs/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yargs/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -8923,6 +9568,24 @@
         "@types/node": "*"
       }
     },
+    "@types/qrcode": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
+      "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/qrcode-generator": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/qrcode-generator/-/qrcode-generator-1.0.6.tgz",
+      "integrity": "sha512-XasuPjhHBC4hyOJ/pHaUNTj+tNxA1SyZpXaS/FOUxEVX03D1gFM8UmMKSIs+pPHLAmRZpU6j9KYxvo+lfsvhKw==",
+      "dev": true,
+      "requires": {
+        "qrcode-generator": "*"
+      }
+    },
     "@types/qs": {
       "version": "6.9.18",
       "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@@ -9387,9 +10050,7 @@
       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
       "dev": true,
-      "requires": {
-        "ajv": "^8.0.0"
-      }
+      "requires": {}
     },
     "ajv-keywords": {
       "version": "5.1.0",
@@ -9592,6 +10253,19 @@
         }
       }
     },
+    "assert": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
+      "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.2",
+        "is-nan": "^1.3.2",
+        "object-is": "^1.1.5",
+        "object.assign": "^4.1.4",
+        "util": "^0.12.5"
+      }
+    },
     "async-function": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@@ -9716,6 +10390,15 @@
       "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
       "dev": true
     },
+    "browser-resolve": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz",
+      "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==",
+      "dev": true,
+      "requires": {
+        "resolve": "^1.17.0"
+      }
+    },
     "browserify-aes": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -9844,6 +10527,15 @@
         }
       }
     },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "requires": {
+        "pako": "~1.0.5"
+      }
+    },
     "browserslist": {
       "version": "4.24.4",
       "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
@@ -9878,6 +10570,12 @@
       "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
       "dev": true
     },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
+      "dev": true
+    },
     "bundle-name": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
@@ -10047,6 +10745,64 @@
         "is-wsl": "^2.2.0"
       }
     },
+    "cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "requires": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+          "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+        },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+        },
+        "string-width": {
+          "version": "4.2.3",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+          "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.1"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+          "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+          "requires": {
+            "ansi-regex": "^5.0.1"
+          }
+        },
+        "wrap-ansi": {
+          "version": "6.2.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "string-width": "^4.1.0",
+            "strip-ansi": "^6.0.0"
+          }
+        }
+      }
+    },
     "clone-deep": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@@ -10062,7 +10818,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
       "requires": {
         "color-name": "~1.1.4"
       }
@@ -10070,8 +10825,7 @@
     "color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
     "colorette": {
       "version": "2.0.20",
@@ -10121,6 +10875,18 @@
       "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
       "dev": true
     },
+    "console-browserify": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+      "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
+      "dev": true
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==",
+      "dev": true
+    },
     "content-disposition": {
       "version": "0.5.2",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
@@ -10209,6 +10975,12 @@
         "sha.js": "^2.4.8"
       }
     },
+    "create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true
+    },
     "cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -10304,6 +11076,11 @@
         "ms": "2.0.0"
       }
     },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
+    },
     "deep-extend": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -10407,6 +11184,11 @@
         }
       }
     },
+    "dijkstrajs": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
+    },
     "dns-packet": {
       "version": "5.6.1",
       "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -10425,6 +11207,12 @@
         "esutils": "^2.0.2"
       }
     },
+    "domain-browser": {
+      "version": "4.22.0",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
+      "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==",
+      "dev": true
+    },
     "dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -11217,6 +12005,11 @@
       "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
       "dev": true
     },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+    },
     "get-intrinsic": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -11503,6 +12296,12 @@
         "micromatch": "^4.0.2"
       }
     },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==",
+      "dev": true
+    },
     "human-signals": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -11610,6 +12409,16 @@
       "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
       "dev": true
     },
+    "is-arguments": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+      "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+      "dev": true,
+      "requires": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
+      }
+    },
     "is-array-buffer": {
       "version": "3.0.5",
       "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -11722,8 +12531,7 @@
     "is-fullwidth-code-point": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
     },
     "is-generator-function": {
       "version": "1.1.0",
@@ -11769,6 +12577,16 @@
       "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
       "dev": true
     },
+    "is-nan": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+      "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.0",
+        "define-properties": "^1.1.3"
+      }
+    },
     "is-network-error": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
@@ -11927,6 +12745,12 @@
       "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
       "dev": true
     },
+    "isomorphic-timers-promises": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz",
+      "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==",
+      "dev": true
+    },
     "jest-worker": {
       "version": "27.5.1",
       "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@@ -12242,12 +13066,92 @@
       "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
       "dev": true
     },
+    "node-polyfill-webpack-plugin": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.1.0.tgz",
+      "integrity": "sha512-b4ei444EKkOagG/yFqojrD3QTYM5IOU1f8tn9o6uwrG4qL+brI7oVhjPVd0ZL2xy+Z6CP5bu9w8XTvlWgiXHcw==",
+      "dev": true,
+      "requires": {
+        "node-stdlib-browser": "^1.3.0",
+        "type-fest": "^4.27.0"
+      },
+      "dependencies": {
+        "type-fest": {
+          "version": "4.39.1",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz",
+          "integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==",
+          "dev": true
+        }
+      }
+    },
     "node-releases": {
       "version": "2.0.19",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
       "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
       "dev": true
     },
+    "node-stdlib-browser": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz",
+      "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==",
+      "dev": true,
+      "requires": {
+        "assert": "^2.0.0",
+        "browser-resolve": "^2.0.0",
+        "browserify-zlib": "^0.2.0",
+        "buffer": "^5.7.1",
+        "console-browserify": "^1.1.0",
+        "constants-browserify": "^1.0.0",
+        "create-require": "^1.1.1",
+        "crypto-browserify": "^3.12.1",
+        "domain-browser": "4.22.0",
+        "events": "^3.0.0",
+        "https-browserify": "^1.0.0",
+        "isomorphic-timers-promises": "^1.0.1",
+        "os-browserify": "^0.3.0",
+        "path-browserify": "^1.0.1",
+        "pkg-dir": "^5.0.0",
+        "process": "^0.11.10",
+        "punycode": "^1.4.1",
+        "querystring-es3": "^0.2.1",
+        "readable-stream": "^3.6.0",
+        "stream-browserify": "^3.0.0",
+        "stream-http": "^3.2.0",
+        "string_decoder": "^1.0.0",
+        "timers-browserify": "^2.0.4",
+        "tty-browserify": "0.0.1",
+        "url": "^0.11.4",
+        "util": "^0.12.4",
+        "vm-browserify": "^1.0.1"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+          "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+          "dev": true,
+          "requires": {
+            "base64-js": "^1.3.1",
+            "ieee754": "^1.1.13"
+          }
+        },
+        "pkg-dir": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
+          "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
+          "dev": true,
+          "requires": {
+            "find-up": "^5.0.0"
+          }
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+          "dev": true
+        }
+      }
+    },
     "normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -12289,6 +13193,16 @@
       "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
       "dev": true
     },
+    "object-is": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+      "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1"
+      }
+    },
     "object-keys": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -12411,6 +13325,12 @@
         "word-wrap": "^1.2.5"
       }
     },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==",
+      "dev": true
+    },
     "own-keys": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -12454,7 +13374,12 @@
     "p-try": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+    },
+    "pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
       "dev": true
     },
     "parent-module": {
@@ -12494,11 +13419,16 @@
       "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
       "dev": true
     },
+    "path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true
+    },
     "path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-      "dev": true
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
     },
     "path-is-inside": {
       "version": "1.0.2",
@@ -12597,6 +13527,11 @@
         }
       }
     },
+    "pngjs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
+    },
     "possible-typed-array-names": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -12730,6 +13665,21 @@
       "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
       "dev": true
     },
+    "qrcode": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+      "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+      "requires": {
+        "dijkstrajs": "^1.0.1",
+        "pngjs": "^5.0.0",
+        "yargs": "^15.3.1"
+      }
+    },
+    "qrcode-generator": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
+      "integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw=="
+    },
     "qs": {
       "version": "6.13.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -12739,6 +13689,12 @@
         "side-channel": "^1.0.6"
       }
     },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==",
+      "dev": true
+    },
     "queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -12880,12 +13836,22 @@
         "rc": "^1.0.1"
       }
     },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
+    },
     "require-from-string": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
       "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
       "dev": true
     },
+    "require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+    },
     "requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -13209,6 +14175,11 @@
         "send": "0.19.0"
       }
     },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+    },
     "set-function-length": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -13246,6 +14217,12 @@
         "es-object-atoms": "^1.0.0"
       }
     },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+      "dev": true
+    },
     "setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -13464,6 +14441,18 @@
         "readable-stream": "^3.5.0"
       }
     },
+    "stream-http": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz",
+      "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==",
+      "dev": true,
+      "requires": {
+        "builtin-status-codes": "^3.0.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.6.0",
+        "xtend": "^4.0.2"
+      }
+    },
     "string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -13623,6 +14612,15 @@
       "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
       "dev": true
     },
+    "timers-browserify": {
+      "version": "2.0.12",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
+      "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
+      "dev": true,
+      "requires": {
+        "setimmediate": "^1.0.4"
+      }
+    },
     "tinyglobby": {
       "version": "0.2.12",
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
@@ -13729,6 +14727,12 @@
       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
       "dev": true
     },
+    "tty-browserify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
+      "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==",
+      "dev": true
+    },
     "type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -13866,6 +14870,37 @@
         "punycode": "^2.1.0"
       }
     },
+    "url": {
+      "version": "0.11.4",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
+      "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
+      "dev": true,
+      "requires": {
+        "punycode": "^1.4.1",
+        "qs": "^6.12.3"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+          "dev": true
+        }
+      }
+    },
+    "util": {
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+      "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "is-arguments": "^1.0.4",
+        "is-generator-function": "^1.0.7",
+        "is-typed-array": "^1.1.3",
+        "which-typed-array": "^1.1.2"
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -13890,6 +14925,12 @@
       "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
       "dev": true
     },
+    "vm-browserify": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+      "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+      "dev": true
+    },
     "watchpack": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
@@ -14134,6 +15175,11 @@
         "is-weakset": "^2.0.3"
       }
     },
+    "which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
+    },
     "which-typed-array": {
       "version": "1.1.19",
       "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
@@ -14188,6 +15234,114 @@
       "dev": true,
       "requires": {}
     },
+    "xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true
+    },
+    "y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
+    },
+    "yargs": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+      "requires": {
+        "cliui": "^6.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^4.1.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^4.2.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^18.1.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+          "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+        },
+        "find-up": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+          "requires": {
+            "locate-path": "^5.0.0",
+            "path-exists": "^4.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+          "requires": {
+            "p-locate": "^4.1.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+          "requires": {
+            "p-limit": "^2.2.0"
+          }
+        },
+        "string-width": {
+          "version": "4.2.3",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+          "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.1"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+          "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+          "requires": {
+            "ansi-regex": "^5.0.1"
+          }
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "requires": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "5.3.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+          "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
+        }
+      }
+    },
     "yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/client/package.json b/client/package.json
index 28f7ff1..ae4a305 100644
--- a/client/package.json
+++ b/client/package.json
@@ -22,6 +22,8 @@
   "author": "",
   "license": "MIT",
   "devDependencies": {
+    "@types/qrcode": "^1.5.5",
+    "@types/qrcode-generator": "^1.0.6",
     "@typescript-eslint/eslint-plugin": "^8.29.0",
     "@typescript-eslint/parser": "^8.29.0",
     "buffer": "^6.0.3",
@@ -31,6 +33,7 @@
     "eslint": "^9.24.0",
     "eslint-plugin-import": "^2.31.0",
     "husky": "^9.1.7",
+    "node-polyfill-webpack-plugin": "^4.1.0",
     "process": "^0.11.10",
     "serve": "^14.0.0",
     "stream-browserify": "^3.0.0",
@@ -42,6 +45,8 @@
     "webpack-dev-server": "^5.2.1"
   },
   "dependencies": {
-    "nostr-tools": "^2.12.0"
+    "nostr-tools": "^2.12.0",
+    "qrcode": "^1.5.4",
+    "qrcode-generator": "^1.4.4"
   }
 }
diff --git a/client/src/client.ts b/client/src/client.ts
index 9a77177..ef5ca45 100644
--- a/client/src/client.ts
+++ b/client/src/client.ts
@@ -1,16 +1,258 @@
 // client.ts - External TypeScript file for HTTP to Nostr converter
 // This follows strict CSP policies by avoiding inline scripts
 
-// Import the converter function
+// Import functions from other modules
 import { displayConvertedEvent } from './converter';
+import { lookupNip05, searchUsers } from './search';
+import { publishToRelay, convertNpubToHex, verifyEvent } from './relay';
+import { 
+    setDefaultHttpRequest, 
+    sanitizeText, 
+    processTags, 
+    standardizeEvent,
+    showError,
+    showSuccess,
+    showLoading
+} from './utils';
+
+/**
+ * Handle the server search button click
+ */
+async function handleServerSearch(): Promise<void> {
+    const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
+    const resultDiv = document.getElementById('serverSearchResult');
+    
+    if (!serverPubkeyInput || !resultDiv) {
+        return;
+    }
+    
+    const searchTerm = serverPubkeyInput.value.trim();
+    if (!searchTerm) {
+        showError(resultDiv, 'Please enter a search term');
+        return;
+    }
+    
+    // If it's a valid npub, no need to search
+    if (searchTerm.startsWith('npub')) {
+        try {
+            const hexPubkey = convertNpubToHex(searchTerm);
+            if (hexPubkey) {
+                // It's a valid npub, hide any existing results
+                resultDiv.style.display = 'none';
+                return;
+            }
+        } catch (error) {
+            // Not a valid npub, continue with search
+        }
+    }
+    
+    // Display loading state
+    showLoading(resultDiv, 'Searching relays...');
+    
+    try {
+        const results = await searchUsers(searchTerm);
+        
+        if (results.length > 0) {
+            // If there's only one result and it's a valid npub, use it directly
+            if (results.length === 1 && results[0].name === 'Valid npub') {
+                serverPubkeyInput.value = results[0].npub;
+                resultDiv.style.display = 'none';
+                return;
+            }
+            
+            // Create the results list
+            let resultsHtml = '<div class="search-results-list">';
+            
+            results.forEach(result => {
+                const truncatedNpub = `${result.npub.substring(0, 10)}...${result.npub.substring(result.npub.length - 5)}`;
+                resultsHtml += `
+                    <div class="search-result-item" data-npub="${result.npub}">
+                        <div class="result-name">${result.name}</div>
+                        <div class="result-npub">${truncatedNpub}</div>
+                        ${result.nip05 ? `<div class="result-nip05">${result.nip05}</div>` : ''}
+                        <button class="use-npub-btn">Use</button>
+                    </div>
+                `;
+            });
+            
+            resultsHtml += '</div>';
+            resultDiv.innerHTML = resultsHtml;
+            
+            // Add click handlers for the "Use" buttons
+            document.querySelectorAll('.use-npub-btn').forEach(button => {
+                button.addEventListener('click', (e) => {
+                    const resultItem = (e.target as HTMLElement).closest('.search-result-item');
+                    if (resultItem) {
+                        const npub = resultItem.getAttribute('data-npub');
+                        if (npub) {
+                            serverPubkeyInput.value = npub;
+                            resultDiv.innerHTML += '<br><span style="color: #008800;">✓ Applied!</span>';
+                        }
+                    }
+                });
+            });
+        } else {
+            showError(resultDiv, 'No users found matching your search term');
+        }
+    } catch (error) {
+        showError(resultDiv, String(error));
+    }
+}
+
+/**
+ * Handle the publish button click
+ */
+async function handlePublishEvent(): Promise<void> {
+    const eventOutputPre = document.getElementById('eventOutput') as HTMLElement;
+    const publishRelayInput = document.getElementById('publishRelay') as HTMLInputElement;
+    const publishResultDiv = document.getElementById('publishResult') as HTMLElement;
+    
+    if (!eventOutputPre || !publishRelayInput || !publishResultDiv) {
+        return;
+    }
+    
+    // Check if we have a stored event from the creation process
+    if ((window as any).currentSignedEvent) {
+        console.log("Using stored signed event from creation process");
+        
+        try {
+            const event = (window as any).currentSignedEvent;
+            const relayUrl = publishRelayInput.value.trim();
+            
+            if (!relayUrl || !relayUrl.startsWith('wss://')) {
+                showError(publishResultDiv, 'Please enter a valid relay URL (must start with wss://)');
+                return;
+            }
+            
+            // Display loading state
+            showLoading(publishResultDiv, 'Publishing to relay...');
+            
+            try {
+                const result = await publishToRelay(event, relayUrl);
+                console.log('Publish success:', result);
+                showSuccess(publishResultDiv, result);
+            } catch (publishError) {
+                console.error('Publish error:', publishError);
+                showError(publishResultDiv, String(publishError));
+                // Don't rethrow, just handle it here so the UI shows the error
+            }
+            return;
+        } catch (error) {
+            console.error("Error using stored event, falling back to parsed event");
+            // Continue with normal flow if this fails
+        }
+    }
+    
+    const eventText = eventOutputPre.textContent || '';
+    if (!eventText) {
+        showError(publishResultDiv, 'No event to publish');
+        return;
+    }
+    
+    console.log('Raw event text:', eventText);
+    
+    let event;
+    try {
+        // Check for non-printable characters or hidden characters that might cause issues
+        const sanitizedEventText = sanitizeText(eventText);
+        console.log('Sanitized event text:', sanitizedEventText);
+        
+        event = JSON.parse(sanitizedEventText);
+        console.log('Parsed event:', event);
+        
+        // Validate that it's a proper Nostr event
+        if (!event || typeof event !== 'object') {
+            throw new Error('Invalid event object');
+        }
+        
+        // Check if it has id, pubkey, and sig properties which are required for a valid Nostr event
+        if (!event.id || !event.pubkey || !event.sig) {
+            throw new Error('Event is missing required properties (id, pubkey, or sig)');
+        }
+        
+        // Check if pubkey is in npub format and convert it if needed
+        if (event.pubkey.startsWith('npub')) {
+            const hexPubkey = convertNpubToHex(event.pubkey);
+            if (hexPubkey) {
+                console.log('Converting npub to hex pubkey...');
+                event.pubkey = hexPubkey;
+            } else {
+                throw new Error('Invalid npub format in pubkey');
+            }
+        }
+        
+        // Create a clean event with exactly the fields we need
+        console.log("Creating a clean event for publishing...");
+        event = standardizeEvent(event);
+        console.log("Clean event created:", JSON.stringify(event, null, 2));
+        
+    } catch (error) {
+        showError(publishResultDiv, `Invalid event: ${String(error)}`);
+        return;
+    }
+    
+    const relayUrl = publishRelayInput.value.trim();
+    if (!relayUrl || !relayUrl.startsWith('wss://')) {
+        showError(publishResultDiv, 'Please enter a valid relay URL (must start with wss://)');
+        return;
+    }
+    
+    // Display loading state
+    showLoading(publishResultDiv, 'Publishing to relay...');
+    
+    try {
+        // Check the event structure
+        console.log('Full event object before publish:', JSON.stringify(event, null, 2));
+        
+        // Verify event first
+        console.log('Verifying event...');
+        const isValid = verifyEvent(event);
+        console.log('Event verification result:', isValid);
+        
+        if (!isValid) {
+            console.log('Event verification failed. Proceeding anyway as the relay will validate.');
+        }
+        
+        // Proceed with publish even if verification failed - the relay will validate
+        publishResultDiv.innerHTML += '<br><span>Attempting to publish...</span>';
+        console.log('Attempting to publish event to relay:', relayUrl);
+        
+        try {
+            const result = await publishToRelay(event, relayUrl);
+            console.log('Publish success:', result);
+            showSuccess(publishResultDiv, result);
+        } catch (publishError) {
+            console.error('Publish error:', publishError);
+            showError(publishResultDiv, String(publishError));
+            // Don't rethrow, just handle it here so the UI shows the error
+        }
+    } catch (error) {
+        console.error('Error preparing event for publish:', error);
+        showError(publishResultDiv, `Error preparing event: ${String(error)}`);
+    }
+}
 
 // Initialize the event handlers when the DOM is loaded
 document.addEventListener('DOMContentLoaded', function(): void {
     // Set up the convert button click handler
     const convertButton = document.getElementById('convertButton');
+    const searchButton = document.getElementById('searchServerBtn');
+    const publishButton = document.getElementById('publishButton');
+    
     if (convertButton) {
         convertButton.addEventListener('click', displayConvertedEvent);
     }
     
+    if (searchButton) {
+        searchButton.addEventListener('click', handleServerSearch);
+    }
+    
+    if (publishButton) {
+        publishButton.addEventListener('click', handlePublishEvent);
+    }
+    
+    // Set default HTTP request
+    setDefaultHttpRequest();
+    
     console.log('HTTP to Nostr converter initialized');
 });
\ No newline at end of file
diff --git a/client/src/converter.ts b/client/src/converter.ts
index 7126110..bf5171c 100644
--- a/client/src/converter.ts
+++ b/client/src/converter.ts
@@ -7,6 +7,9 @@ declare global {
 }
 import { defaultServerConfig, appSettings } from './config';
 import * as nostrTools from 'nostr-tools';
+import qrcode from 'qrcode-generator';
+import { convertNpubToHex } from './relay';
+import { processTags, showSuccess } from './utils';
 
 // Generate a keypair for standalone mode (when no extension is available)
 let standaloneSecretKey: Uint8Array | null = null;
@@ -60,16 +63,13 @@ export function convertToEvent(
             // The second argument MUST be the server's public key in hex format
             let serverPubkeyHex = serverPubkey;
             
-            // Convert npub to hex if needed using nostr-tools
+            // Convert npub to hex if needed
             if (serverPubkey.startsWith('npub')) {
-                try {
-                    const decoded = nostrTools.nip19.decode(serverPubkey);
-                    if (decoded.type === 'npub' && decoded.data) {
-                        serverPubkeyHex = decoded.data;
-                        console.log("Converted npub to hex format:", serverPubkeyHex);
-                    }
-                } catch (decodeError) {
-                    console.error("Failed to decode npub:", decodeError);
+                const hexPubkey = convertNpubToHex(serverPubkey);
+                if (hexPubkey) {
+                    serverPubkeyHex = hexPubkey;
+                    console.log("Converted npub to hex format:", serverPubkeyHex);
+                } else {
                     throw new Error("Failed to decode npub. Please use a valid npub format.");
                 }
             }
@@ -89,15 +89,31 @@ export function convertToEvent(
         console.warn("NIP-44 encryption not available. Content will not be encrypted properly.");
     }
     
+    // Convert serverPubkey to hex if it's an npub
+    let pTagValue = serverPubkey;
+    if (serverPubkey.startsWith('npub')) {
+        try {
+            console.log(`Converting p tag npub to hex: ${serverPubkey}`);
+            const decoded = nostrTools.nip19.decode(serverPubkey);
+            if (decoded.type === 'npub' && decoded.data) {
+                pTagValue = decoded.data as string;
+                console.log(`Converted to hex pubkey: ${pTagValue}`);
+            }
+        } catch (error) {
+            console.error(`Error decoding npub: ${serverPubkey}`, error);
+        }
+    }
+    
+    // Create the event with the proper structure
     const event = {
         kind: 21120,
         pubkey: pubkey,
         content: encryptedContent, // Encrypted HTTP request using NIP-44
         tags: [
             // Required tags per README specification
-            ["p", serverPubkey], // P tag to indicate this is a REQUEST
+            ["p", pTagValue], // P tag with hex pubkey (converted from npub if needed)
             ["key", decryptkey], // Key for decryption
-            ["expiration", Math.floor(Date.now() / 1000) + appSettings.expirationTime]
+            ["expiration", String(Math.floor(Date.now() / 1000) + appSettings.expirationTime)]
         ]
     };
 
@@ -105,6 +121,9 @@ export function convertToEvent(
     if (relay) {
         event.tags.push(["r", relay]);
     }
+    
+    // Process tags to ensure proper format (convert any npub in tags to hex, etc.)
+    event.tags = processTags(event.tags);
 
     return JSON.stringify(event, null, 2);
 }
@@ -156,6 +175,9 @@ export async function displayConvertedEvent(): Promise<void> {
         );
         
         if (convertedEvent) {
+            // Store the original event in case we need to reference it
+            (window as any).originalEvent = convertedEvent;
+            
             // Parse the event to create a proper Nostr event object for signing
             const parsedEvent = JSON.parse(convertedEvent);
             const nostrEvent = {
@@ -165,6 +187,9 @@ export async function displayConvertedEvent(): Promise<void> {
                 created_at: Math.floor(Date.now() / 1000),
                 pubkey: parsedEvent.pubkey
             };
+            
+            // Log the event being signed
+            console.log("Event to be signed:", JSON.stringify(nostrEvent, null, 2));
 
             let signedEvent;
 
@@ -172,6 +197,7 @@ export async function displayConvertedEvent(): Promise<void> {
                 try {
                     // Try to sign with the NIP-07 extension
                     signedEvent = await window.nostr.signEvent(nostrEvent);
+                    console.log("Event signed with extension:", signedEvent);
                 } catch (error) {
                     console.error("Error signing event with extension:", error);
                     // Fall back to signing with nostr-tools
@@ -191,9 +217,211 @@ export async function displayConvertedEvent(): Promise<void> {
                 outputDiv.hidden = false;
                 return;
             }
+// Store the event in a global variable for easier access during publishing
+(window as any).currentSignedEvent = signedEvent;
+
+// Display the event JSON
+eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
+
+// Add a helpful message about publishing the event
+const publishRelayInput = document.getElementById('publishRelay') as HTMLInputElement;
+if (publishRelayInput) {
+    const publishResult = document.getElementById('publishResult');
+    if (publishResult) {
+        showSuccess(publishResult, 'Event created successfully. Ready to publish!');
+    }
+}
 
-            eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
             
+            // Generate animated QR code using multiple frames
+            const qrCodeContainer = document.getElementById('qrCode') as HTMLElement;
+            if (qrCodeContainer) {
+                try {
+                    // Convert the event to a JSON string
+                    const eventJson = JSON.stringify(signedEvent);
+                    
+                    // Calculate how many QR codes we need based on the data size
+                    // Use a much smaller chunk size to ensure it fits in the QR code
+                    const maxChunkSize = 500; // Significantly reduced chunk size
+                    const chunks = [];
+                    
+                    // Split the data into chunks
+                    for (let i = 0; i < eventJson.length; i += maxChunkSize) {
+                        chunks.push(eventJson.slice(i, i + maxChunkSize));
+                    }
+                    
+                    // Prepare container for the animated QR code
+                    qrCodeContainer.innerHTML = '';
+                    const qrFrameContainer = document.createElement('div');
+                    qrFrameContainer.className = 'qr-frame-container';
+                    qrCodeContainer.appendChild(qrFrameContainer);
+                    
+                    // Create QR codes for each chunk with chunk number and total chunks
+                    const qrFrames: HTMLElement[] = [];
+                    chunks.forEach((chunk, index) => {
+                        // Add metadata to identify part of sequence: [current/total]:[data]
+                        const dataWithMeta = `${index + 1}/${chunks.length}:${chunk}`;
+                        
+                        try {
+                            // Create QR code with maximum version and lower error correction
+                            const qr = qrcode(15, 'L'); // Version 15 (higher capacity), Low error correction
+                            qr.addData(dataWithMeta);
+                            qr.make();
+                            
+                            // Create frame
+                            const frameDiv = document.createElement('div');
+                            frameDiv.className = 'qr-frame';
+                            frameDiv.style.display = index === 0 ? 'block' : 'none';
+                            frameDiv.innerHTML = qr.createSvgTag({
+                                cellSize: 3, // Smaller cell size
+                                margin: 2
+                            });
+                            
+                            qrFrameContainer.appendChild(frameDiv);
+                            qrFrames.push(frameDiv);
+                        } catch (qrError) {
+                            console.error(`Error generating QR code for chunk ${index + 1}:`, qrError);
+                            // Create an error frame instead
+                            const errorDiv = document.createElement('div');
+                            errorDiv.className = 'qr-frame qr-error';
+                            errorDiv.style.display = index === 0 ? 'block' : 'none';
+                            errorDiv.innerHTML = `
+                                <div class="error-message">
+                                    <p>Error in frame ${index + 1}</p>
+                                    <p>Chunk too large for QR code</p>
+                                </div>
+                            `;
+                            qrFrameContainer.appendChild(errorDiv);
+                            qrFrames.push(errorDiv);
+                        }
+                        
+                    });
+                    
+                    // Add information about the animated QR code
+                    const infoElement = document.createElement('div');
+                    infoElement.innerHTML = `
+                        <p class="qr-info">Animated QR code: ${chunks.length} frames containing full event data</p>
+                        <p class="qr-info current-frame">Showing frame 1 of ${chunks.length}</p>
+                        <p class="qr-info"><small>The QR code will cycle through all frames automatically</small></p>
+                    `;
+                    qrCodeContainer.appendChild(infoElement);
+                    
+                    // Animation controls
+                    const controlsDiv = document.createElement('div');
+                    controlsDiv.className = 'qr-controls';
+                    controlsDiv.innerHTML = `
+                        <button id="qrPauseBtn">Pause</button>
+                        <button id="qrPrevBtn">◀ Prev</button>
+                        <button id="qrNextBtn">Next ▶</button>
+                    `;
+                    qrCodeContainer.appendChild(controlsDiv);
+                    
+                    // Set up animation
+                    let currentFrame = 0;
+                    let animationInterval: number | null = null;
+                    let isPaused = false;
+                    
+                    const updateFrameInfo = () => {
+                        const frameInfo = qrCodeContainer.querySelector('.current-frame');
+                        if (frameInfo) {
+                            frameInfo.textContent = `Showing frame ${currentFrame + 1} of ${chunks.length}`;
+                        }
+                    };
+                    
+                    const showFrame = (index: number) => {
+                        qrFrames.forEach((frame, i) => {
+                            frame.style.display = i === index ? 'block' : 'none';
+                        });
+                        currentFrame = index;
+                        updateFrameInfo();
+                    };
+                    
+                    const nextFrame = () => {
+                        showFrame((currentFrame + 1) % qrFrames.length);
+                    };
+                    
+                    const prevFrame = () => {
+                        showFrame((currentFrame - 1 + qrFrames.length) % qrFrames.length);
+                    };
+                    
+                    // Start the animation
+                    animationInterval = window.setInterval(nextFrame, 2000); // Change frame every 2 seconds
+                    
+                    // Set up button event handlers
+                    const pauseBtn = document.getElementById('qrPauseBtn');
+                    const prevBtn = document.getElementById('qrPrevBtn');
+                    const nextBtn = document.getElementById('qrNextBtn');
+                    
+                    if (pauseBtn) {
+                        pauseBtn.addEventListener('click', () => {
+                            if (isPaused) {
+                                animationInterval = window.setInterval(nextFrame, 2000);
+                                pauseBtn.textContent = 'Pause';
+                            } else {
+                                if (animationInterval !== null) {
+                                    clearInterval(animationInterval);
+                                    animationInterval = null;
+                                }
+                                pauseBtn.textContent = 'Play';
+                            }
+                            isPaused = !isPaused;
+                        });
+                    }
+                    
+                    if (prevBtn) {
+                        prevBtn.addEventListener('click', () => {
+                            if (isPaused) {
+                                prevFrame();
+                            }
+                        });
+                    }
+                    
+                    if (nextBtn) {
+                        nextBtn.addEventListener('click', () => {
+                            if (isPaused) {
+                                nextFrame();
+                            }
+                        });
+                    }
+                } catch (error) {
+                    console.error("Error generating QR code:", error);
+                    
+                    // Create a fallback display with error information and a more compact representation
+                    qrCodeContainer.innerHTML = '';
+                    
+                    // Create a backup QR code with just the event ID and relay
+                    try {
+                        const eventId = signedEvent.id || '';
+                        const relay = encodeURIComponent(defaultServerConfig.defaultRelay);
+                        const nostrUri = `nostr:${eventId}?relay=${relay}`;
+                        
+                        const qr = qrcode(10, 'M');
+                        qr.addData(nostrUri);
+                        qr.make();
+                        
+                        qrCodeContainer.innerHTML = `
+                            <div class="qr-error-container">
+                                <h3>Event Too Large for Animated QR</h3>
+                                <p>Error: ${String(error)}</p>
+                                <p>Using event reference instead:</p>
+                                ${qr.createSvgTag({ cellSize: 4, margin: 4 })}
+                                <p class="qr-info">This QR code contains a reference to the event: ${eventId.substring(0, 8)}...</p>
+                            </div>
+                        `;
+                    } catch (fallbackError) {
+                        qrCodeContainer.innerHTML = `
+                            <div class="qr-error-container">
+                                <h3>QR Generation Failed</h3>
+                                <p>Error: ${String(error)}</p>
+                                <p>Try sharing the event ID manually: ${signedEvent.id || 'Unknown'}</p>
+                            </div>
+                        `;
+                    }
+                    qrCodeContainer.innerHTML = `<p>Error generating QR code: ${error}</p>
+                        <p>Try using a URL shortener with the event ID instead.</p>`;
+                }
+            }
+
             outputDiv.hidden = false;
         }
     }
@@ -201,9 +429,28 @@ export async function displayConvertedEvent(): Promise<void> {
 
 // Initialize event listeners when the DOM is fully loaded
 document.addEventListener('DOMContentLoaded', () => {
-    // Only set up the click event handler without any automatic encryption
+    // Set up the click event handler without any automatic encryption
     const convertButton = document.getElementById('convertButton');
+    const publishButton = document.getElementById('publishButton');
+    
     if (convertButton) {
         convertButton.addEventListener('click', displayConvertedEvent);
     }
+    
+    // Add a handler for the publish button to check if an event is available
+    if (publishButton) {
+        publishButton.addEventListener('click', () => {
+            const eventOutput = document.getElementById('eventOutput');
+            const publishResult = document.getElementById('publishResult');
+            
+            if (!eventOutput || !publishResult) {
+                return;
+            }
+            
+            if (!eventOutput.textContent || eventOutput.textContent.trim() === '') {
+                publishResult.innerHTML = '<span style="color: #cc0000;">You need to convert an HTTP request first</span>';
+                publishResult.style.display = 'block';
+            }
+        });
+    }
 });
\ No newline at end of file
diff --git a/client/src/relay.ts b/client/src/relay.ts
new file mode 100644
index 0000000..bd96255
--- /dev/null
+++ b/client/src/relay.ts
@@ -0,0 +1,258 @@
+// relay.ts - Functions for communicating with Nostr relays
+import * as nostrTools from 'nostr-tools';
+
+/**
+ * Validate a hex string
+ * @param str The string to validate
+ * @param expectedLength The expected length of the hex string
+ * @returns true if valid, false otherwise
+ */
+export function isValidHexString(str: string, expectedLength?: number): boolean {
+    const hexRegex = /^[0-9a-f]*$/i;
+    if (!hexRegex.test(str)) {
+        return false;
+    }
+    
+    // Check if it has even length (hex strings should have even length)
+    if (str.length % 2 !== 0) {
+        return false;
+    }
+    
+    // Check expected length if provided
+    if (expectedLength !== undefined && str.length !== expectedLength) {
+        return false;
+    }
+    
+    return true;
+}
+
+/**
+ * Publish an event to a Nostr relay
+ * @param event The Nostr event to publish
+ * @param relayUrl The URL of the relay to publish to
+ * @returns Promise that resolves to a success or error message
+ */
+export async function publishToRelay(event: any, relayUrl: string): Promise<string> {
+    return new Promise<string>((resolve, reject) => {
+        try {
+            // Additional validation specifically for publishing
+            if (!event || typeof event !== 'object') {
+                reject(new Error('Invalid event object'));
+                return;
+            }
+            
+            // Check required fields
+            if (!event.id || !event.pubkey || !event.sig) {
+                reject(new Error('Event missing required fields (id, pubkey, sig)'));
+                return;
+            }
+            
+            // Validate hex strings
+            if (!isValidHexString(event.id, 64)) {
+                reject(new Error(`Invalid event ID: ${event.id} (must be 64 hex chars)`));
+                return;
+            }
+            
+            if (!isValidHexString(event.pubkey, 64)) {
+                reject(new Error(`Invalid pubkey: ${event.pubkey} (must be 64 hex chars)`));
+                return;
+            }
+            
+            if (!isValidHexString(event.sig, 128)) {
+                reject(new Error(`Invalid signature: ${event.sig} (must be 128 hex chars)`));
+                return;
+            }
+            
+            // Create a relay pool for publishing
+            const relayPool = new nostrTools.SimplePool();
+            
+            // Set a timeout for the publish operation
+            const timeout = setTimeout(() => {
+                relayPool.close([relayUrl]);
+                reject(new Error(`Timed out connecting to relay: ${relayUrl}`));
+            }, 10000); // 10 second timeout
+            
+            // Publish the event to the relay
+            console.log(`Publishing event to relay: ${relayUrl}`);
+            
+            // Create a standard formatted event to ensure it follows the relay protocol
+            const standardEvent = {
+                id: event.id,
+                pubkey: event.pubkey,
+                created_at: Number(event.created_at),
+                kind: Number(event.kind),
+                tags: event.tags,
+                content: event.content,
+                sig: event.sig
+            };
+            
+            // Make sure all fields are in the correct format
+            if (typeof standardEvent.created_at !== 'number') {
+                standardEvent.created_at = Math.floor(Date.now() / 1000);
+            }
+            
+            if (typeof standardEvent.kind !== 'number') {
+                standardEvent.kind = 21120;
+            }
+            
+            console.log('Standard event format:', JSON.stringify(standardEvent, null, 2));
+            
+            // Debug log the important hex values
+            console.log('ID:', standardEvent.id, 'length:', standardEvent.id.length, 'valid hex?', isValidHexString(standardEvent.id, 64));
+            console.log('Pubkey:', standardEvent.pubkey, 'length:', standardEvent.pubkey.length, 'valid hex?', isValidHexString(standardEvent.pubkey, 64));
+            console.log('Sig:', standardEvent.sig, 'length:', standardEvent.sig.length, 'valid hex?', isValidHexString(standardEvent.sig, 128));
+            
+            // Use the standardized event for publishing
+            event = standardEvent;
+            
+            // Ensure all hex strings are valid (this is a sanity check beyond our validation)
+            if (!isValidHexString(event.id, 64)) {
+                reject(new Error(`ID is not a valid hex string: ${event.id}`));
+                return;
+            }
+            
+            if (!isValidHexString(event.pubkey, 64)) {
+                reject(new Error(`Pubkey is not a valid hex string: ${event.pubkey}`));
+                return;
+            }
+            
+            if (!isValidHexString(event.sig, 128)) {
+                reject(new Error(`Sig is not a valid hex string: ${event.sig}`));
+                return;
+            }
+            
+            // Try direct WebSocket approach as a fallback
+            try {
+                // Use the WebSocket API directly
+                const ws = new WebSocket(relayUrl);
+                
+                let wsTimeout = setTimeout(() => {
+                    try {
+                        ws.close();
+                    } catch (e) {}
+                    reject(new Error("WebSocket connection timed out"));
+                }, 10000);
+                
+                // Create a flag to track if we've handled response
+                let responseHandled = false;
+                
+                ws.onopen = () => {
+                    // Send the event directly using the relay protocol format ["EVENT", event]
+                    const messageToSend = JSON.stringify(["EVENT", event]);
+                    console.log("Sending WebSocket message:", messageToSend);
+                    ws.send(messageToSend);
+                };
+                
+                ws.onmessage = (msg) => {
+                    console.log("WebSocket message received:", msg.data);
+                    
+                    if (responseHandled) {
+                        return; // Skip if we've already handled a response
+                    }
+                    
+                    if (typeof msg.data === 'string' && msg.data.startsWith('["OK"')) {
+                        responseHandled = true;
+                        clearTimeout(wsTimeout);
+                        resolve(`Event published successfully via WebSocket`);
+                        try {
+                            ws.close();
+                        } catch (e) {}
+                    } else if (typeof msg.data === 'string' && (msg.data.includes('invalid') || msg.data.includes('error'))) {
+                        responseHandled = true;
+                        clearTimeout(wsTimeout);
+                        console.error("WebSocket error response:", msg.data);
+                        reject(new Error(`Relay rejected event: ${msg.data}`));
+                        try {
+                            ws.close();
+                        } catch (e) {}
+                    } else {
+                        console.log("Received other message, waiting for OK or error response");
+                    }
+                };
+                
+                ws.onerror = (error) => {
+                    if (responseHandled) {
+                        return; // Skip if we've already handled a response
+                    }
+                    
+                    responseHandled = true;
+                    clearTimeout(wsTimeout);
+                    console.error("WebSocket error:", error);
+                    reject(new Error(`WebSocket error: ${String(error)}`));
+                    try {
+                        ws.close();
+                    } catch (e) {}
+                };
+                
+                ws.onclose = () => {
+                    clearTimeout(wsTimeout);
+                    console.log("WebSocket closed");
+                };
+            } catch (wsError) {
+                // If WebSocket fails, try the regular method
+                console.error("WebSocket approach failed:", wsError);
+                
+                // Use the nostr-tools publish method as a fallback
+                console.log("Trying nostr-tools publish method...");
+                
+                const publishPromises = relayPool.publish([relayUrl], event);
+                
+                // Use Promise.all to wait for all promises to resolve
+                Promise.all(publishPromises)
+                    .then((relayResults: string[]) => {
+                        clearTimeout(timeout);
+                        relayPool.close([relayUrl]);
+                        if (relayResults && relayResults.length > 0) {
+                            resolve(`Event published to ${relayResults[0]}`);
+                        } else {
+                            resolve(`Event published successfully`);
+                        }
+                    })
+                    .catch((error: Error) => {
+                        clearTimeout(timeout);
+                        relayPool.close([relayUrl]);
+                        console.error('Error details:', error);
+                        reject(new Error(`Failed to publish event: ${error.message}`));
+                    });
+            }
+        } catch (error) {
+            reject(new Error(`Error setting up relay connection: ${String(error)}`));
+        }
+    });
+}
+
+/**
+ * Convert an npub to a hex pubkey
+ * @param npub The npub to convert
+ * @returns The hex pubkey
+ */
+export function convertNpubToHex(npub: string): string | null {
+    if (!npub.startsWith('npub')) {
+        return null;
+    }
+    
+    try {
+        const decoded = nostrTools.nip19.decode(npub);
+        if (decoded.type === 'npub') {
+            return decoded.data as string;
+        }
+    } catch (error) {
+        console.error('Error decoding npub:', error);
+    }
+    
+    return null;
+}
+
+/**
+ * Verify a Nostr event
+ * @param event The event to verify
+ * @returns True if valid, false otherwise
+ */
+export function verifyEvent(event: any): boolean {
+    try {
+        return nostrTools.verifyEvent(event);
+    } catch (error) {
+        console.error('Error verifying event:', error);
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/client/src/search.ts b/client/src/search.ts
new file mode 100644
index 0000000..5847e0e
--- /dev/null
+++ b/client/src/search.ts
@@ -0,0 +1,154 @@
+// search.ts - Functions for searching for users on Nostr
+
+import * as nostrTools from 'nostr-tools';
+
+// Define popular relays to search for users
+export const POPULAR_RELAYS = [
+    "wss://relay.damus.io",
+    "wss://relay.nostr.band",
+    "wss://nos.lol",
+    "wss://nostr.wine"
+];
+
+/**
+ * Look up a NIP-05 address to find the corresponding public key
+ * @param nip05Address The NIP-05 address to look up (e.g. user@example.com)
+ * @returns Promise that resolves to the public key if found, or null if not found
+ */
+export async function lookupNip05(nip05Address: string): Promise<{pubkey: string, relays?: string[]} | null> {
+    try {
+        // Use the NIP-05 lookup function from nostr-tools
+        const result = await nostrTools.nip05.queryProfile(nip05Address);
+        
+        // If the result has a pubkey, return it
+        if (result && result.pubkey) {
+            return result;
+        }
+        
+        return null;
+    } catch (error) {
+        console.error("Error looking up NIP-05 address:", error);
+        return null;
+    }
+}
+
+/**
+ * Search for users based on a search term across popular relays
+ * @param searchTerm The username or NIP-05 identifier to search for
+ * @returns Promise that resolves to an array of user profiles
+ */
+export async function searchUsers(searchTerm: string): Promise<Array<{name: string, pubkey: string, npub: string, nip05?: string}>> {
+    const results: Array<{name: string, pubkey: string, npub: string, nip05?: string}> = [];
+    const processedPubkeys = new Set<string>();
+    
+    // First, check if the input might already be an npub
+    if (searchTerm.startsWith('npub')) {
+        try {
+            const decoded = nostrTools.nip19.decode(searchTerm);
+            if (decoded.type === 'npub' && decoded.data) {
+                // It's a valid npub, no need to search
+                return [{
+                    name: 'Valid npub',
+                    pubkey: decoded.data,
+                    npub: searchTerm
+                }];
+            }
+        } catch (error) {
+            // Not a valid npub, continue with search
+            console.log("Not a valid npub, continuing with search");
+        }
+    }
+    
+    // Check if the search term looks like a NIP-05 identifier
+    if (searchTerm.includes('@')) {
+        try {
+            const nip05Result = await lookupNip05(searchTerm);
+            if (nip05Result && nip05Result.pubkey) {
+                const npub = nostrTools.nip19.npubEncode(nip05Result.pubkey);
+                results.push({
+                    name: searchTerm,
+                    pubkey: nip05Result.pubkey,
+                    npub: npub,
+                    nip05: searchTerm
+                });
+                processedPubkeys.add(nip05Result.pubkey);
+            }
+        } catch (error) {
+            console.error("Error looking up NIP-05:", error);
+        }
+    }
+    
+    // Create a pool of relays to search
+    const relayPool = new nostrTools.SimplePool();
+    
+    try {
+        // Create a filter for the subscription
+        const filter = {
+            kinds: [0], // Only metadata events
+            limit: 20 // Limit to 20 results
+        };
+        
+        // Set a timeout to ensure we don't wait forever
+        const timeoutPromise = new Promise<void>((resolve) => {
+            setTimeout(() => resolve(), 5000); // 5 second timeout
+        });
+        
+        // Create an event handler
+        const eventHandler = (event: any) => {
+            try {
+                // Skip if we've already processed this pubkey
+                if (processedPubkeys.has(event.pubkey)) {
+                    return;
+                }
+                
+                // Parse the profile metadata from content
+                const profile = JSON.parse(event.content);
+                
+                // Check if the profile matches our search term
+                const searchTermLower = searchTerm.toLowerCase();
+                const nameLower = (profile.name || '').toLowerCase();
+                const displayNameLower = (profile.display_name || '').toLowerCase();
+                const nip05Lower = (profile.nip05 || '').toLowerCase();
+                
+                if (
+                    nameLower.includes(searchTermLower) ||
+                    displayNameLower.includes(searchTermLower) ||
+                    nip05Lower.includes(searchTermLower)
+                ) {
+                    // Add to results
+                    const npub = nostrTools.nip19.npubEncode(event.pubkey);
+                    results.push({
+                        name: profile.display_name || profile.name || 'Unknown',
+                        pubkey: event.pubkey,
+                        npub: npub,
+                        nip05: profile.nip05
+                    });
+                    
+                    // Mark as processed
+                    processedPubkeys.add(event.pubkey);
+                }
+            } catch (error) {
+                console.error("Error processing event:", error);
+            }
+        };
+        
+        // Subscribe to events matching the filter
+        const sub = relayPool.subscribeMany(
+            POPULAR_RELAYS,
+            [filter],
+            { onevent: eventHandler }
+        );
+        
+        // Wait for timeout
+        await timeoutPromise;
+        
+        // Close the subscription
+        sub.close();
+        relayPool.close(POPULAR_RELAYS);
+        
+    } catch (error) {
+        console.error("Error searching relays:", error);
+    }
+    
+    return results;
+}
\ No newline at end of file
diff --git a/client/src/styles.css b/client/src/styles.css
index d5de8d9..a15ff58 100644
--- a/client/src/styles.css
+++ b/client/src/styles.css
@@ -1,3 +1,213 @@
+/* Styles for the HTTP to Nostr converter */
+
+/* General layout */
+body {
+    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 20px;
+    background-color: #f8f9fa;
+    color: #212529;
+    line-height: 1.6;
+}
+
+/* Headings */
+h1 {
+    color: #343a40;
+    margin-bottom: 20px;
+    border-bottom: 2px solid #6c757d;
+    padding-bottom: 10px;
+}
+
+h2 {
+    color: #495057;
+    margin-top: 25px;
+    margin-bottom: 15px;
+}
+
+/* Info box */
+.info-box {
+    background-color: #e2f0fd;
+    border-left: 4px solid #0d6efd;
+    padding: 10px 15px;
+    margin-bottom: 20px;
+    border-radius: 0 4px 4px 0;
+}
+
+/* Form elements */
+input[type="text"], textarea {
+    width: 100%;
+    padding: 8px 10px;
+    margin-bottom: 15px;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 14px;
+}
+
+textarea {
+    height: 150px;
+    font-family: monospace;
+}
+
+button {
+    background-color: #0d6efd;
+    color: white;
+    border: none;
+    padding: 10px 20px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: background-color 0.2s;
+}
+
+button:hover {
+    background-color: #0b5ed7;
+}
+
+/* Server input section */
+.server-input-container {
+    display: flex;
+    gap: 8px;
+    margin-top: 5px;
+}
+
+.server-input {
+    flex-grow: 1;
+    margin-bottom: 0;
+}
+
+.server-search-button {
+    padding: 8px 15px;
+}
+
+.server-search-result {
+    margin-top: 10px;
+    padding: 10px;
+    background-color: #ffffff;
+    border-radius: 4px;
+    border: 1px solid #dee2e6;
+}
+
+/* Output section */
+#output {
+    margin-top: 30px;
+    padding: 15px;
+    background-color: #ffffff;
+    border-radius: 4px;
+    border: 1px solid #dee2e6;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+}
+
+pre {
+    background-color: #f8f9fa;
+    padding: 15px;
+    border-radius: 4px;
+    overflow-x: auto;
+    font-family: 'Courier New', Courier, monospace;
+    font-size: 14px;
+    line-height: 1.4;
+    border: 1px solid #dee2e6;
+}
+
+/* Publish container */
+.publish-container {
+    margin-top: 20px;
+    margin-bottom: 20px;
+    padding: 15px;
+    background-color: #f1f8ff;
+    border-radius: 4px;
+    border: 1px solid #b3d7ff;
+}
+
+.publish-input-container {
+    display: flex;
+    gap: 8px;
+    margin-top: 10px;
+    margin-bottom: 10px;
+}
+
+.publish-input {
+    flex-grow: 1;
+    padding: 8px 10px;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 14px;
+}
+
+.publish-button {
+    background-color: #28a745;
+    color: white;
+    border: none;
+    padding: 8px 15px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: background-color 0.2s;
+}
+
+.publish-button:hover {
+    background-color: #218838;
+}
+
+.publish-result {
+    margin-top: 10px;
+    padding: 10px;
+    background-color: #ffffff;
+    border-radius: 4px;
+    border: 1px solid #dee2e6;
+}
+
+/* QR code container */
+.qr-container {
+    margin-top: 20px;
+    text-align: center;
+}
+
+#qrCode {
+    margin: 0 auto;
+    max-width: 100%;
+}
+
+/* Search results styling */
+.search-results-list {
+    max-height: 300px;
+    overflow-y: auto;
+}
+
+.search-result-item {
+    padding: 10px;
+    border-bottom: 1px solid #dee2e6;
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+    position: relative;
+}
+
+.search-result-item:last-child {
+    border-bottom: none;
+}
+
+.result-name {
+    font-weight: bold;
+}
+
+.result-npub {
+    font-family: monospace;
+    font-size: 12px;
+    color: #6c757d;
+}
+
+.result-nip05 {
+    font-size: 12px;
+    color: #0d6efd;
+}
+
+.use-npub-btn {
+    align-self: flex-end;
+    padding: 5px 10px;
+    font-size: 12px;
+    margin-top: 5px;
+}
 body {
     font-family: Arial, sans-serif;
     max-width: 800px;
@@ -38,6 +248,91 @@ button:hover {
     background-color: #f9f9f9;
 }
 
+.qr-container {
+    margin-top: 30px;
+    text-align: center;
+}
+
+#qrCode {
+    margin: 0 auto;
+    display: block;
+    background-color: white;
+    padding: 15px;
+    border-radius: 4px;
+    text-align: center;
+}
+
+.qr-info {
+    margin-top: 8px;
+    font-size: 14px;
+    color: #555;
+    text-align: center;
+}
+
+.qr-frame-container {
+    width: 300px;
+    height: 300px;
+    margin: 0 auto;
+    position: relative;
+}
+
+.qr-frame {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    transition: opacity 0.5s ease;
+}
+
+.qr-controls {
+    margin: 15px auto;
+    text-align: center;
+}
+
+.qr-controls button {
+    margin: 0 5px;
+    padding: 8px 15px;
+    background-color: #4CAF50;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.qr-controls button:hover {
+    background-color: #45a049;
+}
+
+.qr-error {
+    background-color: #ffeeee;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border: 1px solid #ffcccc;
+    border-radius: 4px;
+}
+
+.error-message {
+    text-align: center;
+    color: #cc0000;
+    padding: 20px;
+}
+
+.qr-error-container {
+    padding: 20px;
+    background-color: #f8f8f8;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    text-align: center;
+}
+
+.qr-error-container h3 {
+    color: #cc0000;
+    margin-top: 0;
+}
+
 pre {
     background-color: #f5f5f5;
     padding: 10px;
@@ -60,4 +355,70 @@ h1 {
 
 h2 {
     color: #555;
+}
+
+/* NIP-05 lookup styles */
+.nip05-section {
+    margin: 10px 0;
+    padding: 10px;
+    background-color: #f5f5f5;
+    border-radius: 4px;
+    border-left: 3px solid #4CAF50;
+}
+
+.nip05-input-container {
+    display: flex;
+    margin-top: 5px;
+}
+
+.nip05-input {
+    flex-grow: 1;
+    padding: 8px;
+    margin-right: 8px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+}
+
+.nip05-button {
+    white-space: nowrap;
+    background-color: #4CAF50;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    padding: 8px 15px;
+    cursor: pointer;
+}
+
+.nip05-button:hover {
+    background-color: #45a049;
+}
+
+.nip05-result {
+    margin-top: 8px;
+    font-size: 14px;
+    padding: 5px;
+    border-radius: 4px;
+}
+
+.nip05-success {
+    color: #008800;
+}
+
+.nip05-error {
+    color: #cc0000;
+}
+
+.use-pubkey-button {
+    margin-left: 10px;
+    font-size: 12px;
+    padding: 3px 8px;
+    background-color: #4CAF50;
+    color: white;
+    border: none;
+    border-radius: 3px;
+    cursor: pointer;
+}
+
+.use-pubkey-button:hover {
+    background-color: #45a049;
 }
\ No newline at end of file
diff --git a/client/src/utils.ts b/client/src/utils.ts
new file mode 100644
index 0000000..6072183
--- /dev/null
+++ b/client/src/utils.ts
@@ -0,0 +1,124 @@
+// utils.ts - Utility functions for the HTTP to Nostr converter
+
+/**
+ * Populate the HTTP request textarea with a default example
+ */
+export function setDefaultHttpRequest(): void {
+    const httpRequestBox = document.getElementById('httpRequest') as HTMLTextAreaElement;
+    if (httpRequestBox) {
+        // Only set default if the textarea is empty
+        if (!httpRequestBox.value.trim()) {
+            const defaultRequest = `GET /index.html HTTP/1.1
+Host: example.com
+User-Agent: NostrClient/1.0
+Accept: text/html,application/xhtml+xml,application/xml
+Connection: keep-alive
+
+`;
+            httpRequestBox.value = defaultRequest;
+        }
+    }
+}
+
+/**
+ * Sanitize text to remove non-printable characters
+ * @param text Text to sanitize
+ * @returns Sanitized text
+ */
+export function sanitizeText(text: string): string {
+    return text.replace(/[^\x20-\x7E]/g, '');
+}
+
+/**
+ * Process event tags to convert any npub values in "p" tags to hex pubkeys
+ * @param tags The event tags to process
+ * @returns The processed tags
+ */
+export function processTags(tags: string[][]): string[][] {
+    if (!tags || !Array.isArray(tags)) {
+        return tags;
+    }
+    
+    // Make a deep copy of tags to avoid modifying the original
+    const processedTags = JSON.parse(JSON.stringify(tags));
+    
+    for (let i = 0; i < processedTags.length; i++) {
+        const tag = processedTags[i];
+        if (!tag || !Array.isArray(tag) || tag.length < 2) {
+            continue;
+        }
+        
+        // Convert npub in "p" tags to hex pubkeys
+        if (tag[0] === 'p' && typeof tag[1] === 'string' && tag[1].startsWith('npub')) {
+            try {
+                console.log(`Processing p tag: ${tag[1]}`);
+                const { convertNpubToHex } = require('./relay');
+                const hexPubkey = convertNpubToHex(tag[1]);
+                if (hexPubkey) {
+                    processedTags[i][1] = hexPubkey;
+                    console.log(`Converted npub to hex: ${hexPubkey}`);
+                }
+            } catch (error) {
+                console.error(`Error processing p tag: ${tag[1]}`, error);
+            }
+        }
+        
+        // Ensure expiration tag value is a string
+        if (tag[0] === 'expiration' && tag[1]) {
+            processedTags[i][1] = String(tag[1]);
+        }
+    }
+    
+    return processedTags;
+}
+
+/**
+ * Create a standardized event object with proper types
+ * @param event The event to standardize
+ * @returns Standardized event
+ */
+export function standardizeEvent(event: any): any {
+    if (!event || typeof event !== 'object') {
+        return event;
+    }
+    
+    return {
+        id: event.id,
+        pubkey: event.pubkey,
+        created_at: Number(event.created_at),
+        kind: Number(event.kind),
+        tags: processTags(event.tags),
+        content: event.content,
+        sig: event.sig
+    };
+}
+
+/**
+ * Display an error message in the specified element
+ * @param element The element to display the error in
+ * @param message The error message
+ */
+export function showError(element: HTMLElement, message: string): void {
+    element.innerHTML = `<span style="color: #cc0000;">Error: ${message}</span>`;
+    element.style.display = 'block';
+}
+
+/**
+ * Display a success message in the specified element
+ * @param element The element to display the success message in
+ * @param message The success message
+ */
+export function showSuccess(element: HTMLElement, message: string): void {
+    element.innerHTML = `<span style="color: #008800;">✓ ${message}</span>`;
+    element.style.display = 'block';
+}
+
+/**
+ * Display a loading message in the specified element
+ * @param element The element to display the loading message in
+ * @param message The loading message
+ */
+export function showLoading(element: HTMLElement, message: string = 'Processing...'): void {
+    element.innerHTML = `<span>${message}</span>`;
+    element.style.display = 'block';
+}
\ No newline at end of file
diff --git a/client/webpack.config.js b/client/webpack.config.js
index 24be23e..d2b9796 100644
--- a/client/webpack.config.js
+++ b/client/webpack.config.js
@@ -1,6 +1,8 @@
 const path = require('path');
 const CopyPlugin = require('copy-webpack-plugin');
 const webpack = require('webpack');
+// Polyfills for Node.js core modules in the browser
+const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
 
 module.exports = {
   mode: 'development',
@@ -21,10 +23,7 @@ module.exports = {
     ],
   },
   plugins: [
-    new webpack.ProvidePlugin({
-      Buffer: ['buffer', 'Buffer'],
-      process: 'process/browser',
-    }),
+    new NodePolyfillPlugin(),
     new CopyPlugin({
       patterns: [
         { from: 'src/styles.css', to: 'styles.css' },
@@ -33,20 +32,12 @@ module.exports = {
       ],
     }),
   ],
-  resolve: {
-    extensions: ['.tsx', '.ts', '.js'],
-  },
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   resolve: {
     extensions: ['.tsx', '.ts', '.js'],
-    fallback: {
-      "crypto": false,
-      "buffer": false,
-      "stream": false
-    }
   },
   devServer: {
     static: {