feat: create signing request and send a DM to first signer with zip file url and encryption key
All checks were successful
Release / build_and_release (push) Successful in 1m2s
All checks were successful
Release / build_and_release (push) Successful in 1m2s
This commit is contained in:
parent
d4b095c5ca
commit
bd1e8417c1
146
package-lock.json
generated
146
package-lock.json
generated
@ -16,16 +16,22 @@
|
|||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "1.6.7",
|
"axios": "1.6.7",
|
||||||
|
"crypto-hash": "3.0.0",
|
||||||
|
"file-saver": "2.0.5",
|
||||||
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"mui-file-input": "4.0.4",
|
||||||
"nostr-tools": "2.3.1",
|
"nostr-tools": "2.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-redux": "9.1.0",
|
"react-redux": "9.1.0",
|
||||||
"react-router-dom": "6.22.1",
|
"react-router-dom": "6.22.1",
|
||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1"
|
"redux": "5.0.1",
|
||||||
|
"tseep": "1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/file-saver": "2.0.7",
|
||||||
"@types/lodash": "4.14.202",
|
"@types/lodash": "4.14.202",
|
||||||
"@types/react": "^18.2.56",
|
"@types/react": "^18.2.56",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.19",
|
||||||
@ -1938,6 +1944,12 @@
|
|||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/file-saver": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@ -2567,6 +2579,11 @@
|
|||||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||||
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||||
@ -2602,6 +2619,17 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-hash": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-hash/-/crypto-hash-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-5l5xGtzuvGTU28GXxGV1JYVFou68buZWpkV1Fx5hIDRPnfbQ8KzabTlNIuDIeSCYGVPFehupzDqlnbXm2IXmdQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@ -3211,6 +3239,11 @@
|
|||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@ -3474,6 +3507,11 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||||
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
"version": "10.0.3",
|
"version": "10.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
|
||||||
@ -3526,8 +3564,7 @@
|
|||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/is-arrayish": {
|
"node_modules/is-arrayish": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
@ -3601,6 +3638,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@ -3671,6 +3713,17 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@ -3693,6 +3746,14 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/light-bolt11-decoder": {
|
"node_modules/light-bolt11-decoder": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz",
|
||||||
@ -3819,6 +3880,27 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/mui-file-input": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mui-file-input/-/mui-file-input-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-WYzPqKg4lahGyuUIt7674vwbgW6WS1CO066ujuvCwcvr7mw7IDPPhCCJ/rD6i36OhVN2f+1hlTMX5dmsCyYxrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-bytes": "^6.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.5.0",
|
||||||
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@mui/material": "^5.0.0",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
@ -4002,6 +4084,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@ -4144,6 +4231,22 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-bytes": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@ -4313,6 +4416,20 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@ -4452,6 +4569,11 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.71.1",
|
"version": "1.71.1",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
|
||||||
@ -4510,6 +4632,11 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"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=="
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@ -4557,6 +4684,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
@ -4839,6 +4974,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
@ -18,16 +18,22 @@
|
|||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "1.6.7",
|
"axios": "1.6.7",
|
||||||
|
"crypto-hash": "3.0.0",
|
||||||
|
"file-saver": "2.0.5",
|
||||||
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"mui-file-input": "4.0.4",
|
||||||
"nostr-tools": "2.3.1",
|
"nostr-tools": "2.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-redux": "9.1.0",
|
"react-redux": "9.1.0",
|
||||||
"react-router-dom": "6.22.1",
|
"react-router-dom": "6.22.1",
|
||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1"
|
"redux": "5.0.1",
|
||||||
|
"tseep": "1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/file-saver": "2.0.7",
|
||||||
"@types/lodash": "4.14.202",
|
"@types/lodash": "4.14.202",
|
||||||
"@types/react": "^18.2.56",
|
"@types/react": "^18.2.56",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.19",
|
||||||
|
26
src/App.tsx
26
src/App.tsx
@ -1,10 +1,14 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { Route, Routes } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { AuthController, NostrController } from './controllers'
|
import { AuthController, NostrController } from './controllers'
|
||||||
import { MainLayout } from './layouts/Main'
|
import { MainLayout } from './layouts/Main'
|
||||||
import { LandingPage } from './pages/landing/LandingPage'
|
import {
|
||||||
import { privateRoutes, publicRoutes } from './routes'
|
appPrivateRoutes,
|
||||||
|
appPublicRoutes,
|
||||||
|
privateRoutes,
|
||||||
|
publicRoutes
|
||||||
|
} from './routes'
|
||||||
import { State } from './store/rootReducer'
|
import { State } from './store/rootReducer'
|
||||||
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
||||||
|
|
||||||
@ -32,7 +36,6 @@ const App = () => {
|
|||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
{authState?.loggedIn && <Route path='/' element={<LandingPage />} />}
|
|
||||||
{authState?.loggedIn &&
|
{authState?.loggedIn &&
|
||||||
privateRoutes.map((route, index) => (
|
privateRoutes.map((route, index) => (
|
||||||
<Route
|
<Route
|
||||||
@ -62,8 +65,19 @@ const App = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
{!authState ||
|
|
||||||
(!authState.loggedIn && <Route path='*' element={<LandingPage />} />)}
|
<Route
|
||||||
|
path='*'
|
||||||
|
element={
|
||||||
|
<Navigate
|
||||||
|
to={
|
||||||
|
authState.loggedIn
|
||||||
|
? appPrivateRoutes.homePage
|
||||||
|
: appPublicRoutes.login
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
|
import { Menu as MenuIcon } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
AppBar as AppBarMui,
|
AppBar as AppBarMui,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { setAuthState } from '../../store/actions'
|
import { setAuthState } from '../../store/actions'
|
||||||
@ -14,11 +19,15 @@ import { State } from '../../store/rootReducer'
|
|||||||
import { Dispatch } from '../../store/store'
|
import { Dispatch } from '../../store/store'
|
||||||
import Username from '../username'
|
import Username from '../username'
|
||||||
|
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import nostrichAvatar from '../../assets/images/avatar.png'
|
import nostrichAvatar from '../../assets/images/avatar.png'
|
||||||
import nostrichLogo from '../../assets/images/nostr-logo.jpg'
|
import nostrichLogo from '../../assets/images/nostr-logo.jpg'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPublicRoutes, getProfileRoute } from '../../routes'
|
import {
|
||||||
|
appPrivateRoutes,
|
||||||
|
appPublicRoutes,
|
||||||
|
getProfileRoute
|
||||||
|
} from '../../routes'
|
||||||
import {
|
import {
|
||||||
clearAuthToken,
|
clearAuthToken,
|
||||||
saveNsecBunkerDelegatedKey,
|
saveNsecBunkerDelegatedKey,
|
||||||
@ -26,13 +35,20 @@ import {
|
|||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
|
const validTabs = [appPrivateRoutes.homePage, appPrivateRoutes.decryptZip]
|
||||||
|
|
||||||
export const AppBar = () => {
|
export const AppBar = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const [tabValue, setTabValue] = useState(
|
||||||
|
validTabs.includes(pathname) ? pathname : '/'
|
||||||
|
)
|
||||||
const dispatch: Dispatch = useDispatch()
|
const dispatch: Dispatch = useDispatch()
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [userAvatar, setUserAvatar] = useState(nostrichAvatar)
|
const [userAvatar, setUserAvatar] = useState(nostrichAvatar)
|
||||||
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
|
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
|
||||||
|
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null)
|
||||||
|
|
||||||
const authState = useSelector((state: State) => state.auth)
|
const authState = useSelector((state: State) => state.auth)
|
||||||
const metadataState = useSelector((state: State) => state.metadata)
|
const metadataState = useSelector((state: State) => state.metadata)
|
||||||
@ -51,10 +67,18 @@ export const AppBar = () => {
|
|||||||
setAnchorElUser(event.currentTarget)
|
setAnchorElUser(event.currentTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorElNav(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
const handleCloseUserMenu = () => {
|
const handleCloseUserMenu = () => {
|
||||||
setAnchorElUser(null)
|
setAnchorElUser(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCloseNavMenu = () => {
|
||||||
|
setAnchorElNav(null)
|
||||||
|
}
|
||||||
|
|
||||||
const handleProfile = () => {
|
const handleProfile = () => {
|
||||||
const hexKey = authState?.usersPubkey
|
const hexKey = authState?.usersPubkey
|
||||||
if (hexKey) navigate(getProfileRoute(hexKey))
|
if (hexKey) navigate(getProfileRoute(hexKey))
|
||||||
@ -63,6 +87,7 @@ export const AppBar = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
|
handleCloseUserMenu()
|
||||||
dispatch(
|
dispatch(
|
||||||
setAuthState({
|
setAuthState({
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
@ -87,8 +112,98 @@ export const AppBar = () => {
|
|||||||
return (
|
return (
|
||||||
<AppBarMui position='fixed' className={styles.AppBar}>
|
<AppBarMui position='fixed' className={styles.AppBar}>
|
||||||
<Toolbar className={styles.toolbar}>
|
<Toolbar className={styles.toolbar}>
|
||||||
<Box className={styles.logoWrapper}>
|
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
||||||
<img src={nostrichLogo} alt='Logo' onClick={() => navigate('/')} />
|
<Box className={styles.logoWrapper}>
|
||||||
|
<img src={nostrichLogo} alt='Logo' onClick={() => navigate('/')} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{isAuthenticated && (
|
||||||
|
<Tabs
|
||||||
|
indicatorColor='secondary'
|
||||||
|
value={tabValue}
|
||||||
|
onChange={(_, value) => setTabValue(value)}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
label='Home'
|
||||||
|
value={appPrivateRoutes.homePage}
|
||||||
|
to={appPrivateRoutes.homePage}
|
||||||
|
component={Link}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label='Decrypt Zip'
|
||||||
|
value={appPrivateRoutes.decryptZip}
|
||||||
|
to={appPrivateRoutes.decryptZip}
|
||||||
|
component={Link}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
|
||||||
|
{!isAuthenticated && (
|
||||||
|
<Box className={styles.logoWrapper}>
|
||||||
|
<img
|
||||||
|
src={nostrichLogo}
|
||||||
|
alt='Logo'
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAuthenticated && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
size='large'
|
||||||
|
onClick={handleOpenNavMenu}
|
||||||
|
color='primary'
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
id='menu-appbar'
|
||||||
|
anchorEl={anchorElNav}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
open={!!anchorElNav}
|
||||||
|
onClose={handleCloseNavMenu}
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', md: 'none' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={appPrivateRoutes.homePage}
|
||||||
|
onClick={handleCloseNavMenu}
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={appPrivateRoutes.decryptZip}
|
||||||
|
onClick={handleCloseNavMenu}
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
>
|
||||||
|
Decrypt Zip
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={styles.rightSideBox}>
|
<Box className={styles.rightSideBox}>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import { EventEmitter } from 'tseep'
|
||||||
import NDK, {
|
import NDK, {
|
||||||
NDKEvent,
|
NDKEvent,
|
||||||
NDKNip46Signer,
|
NDKNip46Signer,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
|
NDKUser,
|
||||||
NostrEvent
|
NostrEvent
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import {
|
import {
|
||||||
@ -10,6 +12,7 @@ import {
|
|||||||
Relay,
|
Relay,
|
||||||
UnsignedEvent,
|
UnsignedEvent,
|
||||||
finalizeEvent,
|
finalizeEvent,
|
||||||
|
nip04,
|
||||||
nip19
|
nip19
|
||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { updateNsecbunkerPubkey } from '../store/actions'
|
import { updateNsecbunkerPubkey } from '../store/actions'
|
||||||
@ -18,13 +21,23 @@ import store from '../store/store'
|
|||||||
import { SignedEvent } from '../types'
|
import { SignedEvent } from '../types'
|
||||||
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
||||||
|
|
||||||
export class NostrController {
|
export class NostrController extends EventEmitter {
|
||||||
private static instance: NostrController
|
private static instance: NostrController
|
||||||
|
|
||||||
private bunkerNDK: NDK | undefined
|
private bunkerNDK: NDK | undefined
|
||||||
private remoteSigner: NDKNip46Signer | undefined
|
private remoteSigner: NDKNip46Signer | undefined
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNostrObject = () => {
|
||||||
|
if (window.nostr) return window.nostr
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public nsecBunkerInit = async (relays: string[]) => {
|
public nsecBunkerInit = async (relays: string[]) => {
|
||||||
// Don't reinstantiate bunker NDK if exists with same relays
|
// Don't reinstantiate bunker NDK if exists with same relays
|
||||||
@ -248,19 +261,13 @@ export class NostrController {
|
|||||||
|
|
||||||
return Promise.resolve(signedEvent)
|
return Promise.resolve(signedEvent)
|
||||||
} else if (loginMethod === LoginMethods.extension) {
|
} else if (loginMethod === LoginMethods.extension) {
|
||||||
if (!window.nostr) {
|
const nostr = this.getNostrObject()
|
||||||
return Promise.reject(
|
|
||||||
`Login method is ${loginMethod} but window.nostr is not present. Make sure extension is working properly`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await window.nostr
|
return (await nostr.signEvent(event as NostrEvent).catch((err: any) => {
|
||||||
.signEvent(event as NostrEvent)
|
console.log('Error while signing event: ', err)
|
||||||
.catch((err: any) => {
|
|
||||||
console.log('Error while signing event: ', err)
|
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
})) as Event
|
})) as Event
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
`We could not sign the event, none of the signing methods are available`
|
`We could not sign the event, none of the signing methods are available`
|
||||||
@ -268,26 +275,66 @@ export class NostrController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nip04Encrypt = async (receiver: string, content: string) => {
|
||||||
|
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||||
|
|
||||||
|
if (loginMethod === LoginMethods.extension) {
|
||||||
|
const nostr = this.getNostrObject()
|
||||||
|
|
||||||
|
if (!nostr.nip04) {
|
||||||
|
throw new Error(
|
||||||
|
`Your nostr extension does not support nip04 encryption & decryption`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const encrypted = await nostr.nip04.encrypt(receiver, content)
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginMethod === LoginMethods.privateKey) {
|
||||||
|
const keyPair = (store.getState().auth as AuthState).keyPair
|
||||||
|
|
||||||
|
if (!keyPair) {
|
||||||
|
throw new Error(
|
||||||
|
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const encrypted = await nip04.encrypt(keyPair.private, receiver, content)
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginMethod === LoginMethods.nsecBunker) {
|
||||||
|
const user = new NDKUser({ pubkey: receiver })
|
||||||
|
|
||||||
|
this.remoteSigner?.on('authUrl', (authUrl) => {
|
||||||
|
this.emit('nsecbunker-auth', authUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
|
||||||
|
const encrypted = await this.remoteSigner.encrypt(user, content)
|
||||||
|
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Login method is undefined')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function will capture the public key from the nostr extension or if no extension present
|
* Function will capture the public key from the nostr extension or if no extension present
|
||||||
* function wil capture the public key from the local storage
|
* function wil capture the public key from the local storage
|
||||||
*/
|
*/
|
||||||
capturePublicKey = async (): Promise<string> => {
|
capturePublicKey = async (): Promise<string> => {
|
||||||
if (window.nostr) {
|
const nostr = this.getNostrObject()
|
||||||
const pubKey = await window.nostr.getPublicKey().catch((err: any) => {
|
const pubKey = await nostr.getPublicKey().catch((err: any) => {
|
||||||
return Promise.reject(err.message)
|
return Promise.reject(err.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!pubKey) {
|
if (!pubKey) {
|
||||||
return Promise.reject('Error getting public key, user canceled')
|
return Promise.reject('Error getting public key, user canceled')
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(pubKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(
|
return Promise.resolve(pubKey)
|
||||||
'window.nostr object not present. Make sure you have an nostr extension installed.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,20 +1,66 @@
|
|||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
import Container from '@mui/material/Container'
|
import Container from '@mui/material/Container'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { AppBar } from '../components/AppBar/AppBar'
|
import { AppBar } from '../components/AppBar/AppBar'
|
||||||
import { restoreState } from '../store/actions'
|
import { restoreState, setAuthState } from '../store/actions'
|
||||||
import { loadState } from '../utils'
|
import { clearAuthToken, loadState, saveNsecBunkerDelegatedKey } from '../utils'
|
||||||
|
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||||
|
import { Dispatch } from '../store/store'
|
||||||
|
import { NostrController } from '../controllers'
|
||||||
|
import { LoginMethods } from '../store/auth/types'
|
||||||
|
|
||||||
export const MainLayout = () => {
|
export const MainLayout = () => {
|
||||||
const dispatch = useDispatch()
|
const dispatch: Dispatch = useDispatch()
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const logout = () => {
|
||||||
|
dispatch(
|
||||||
|
setAuthState({
|
||||||
|
loggedIn: false,
|
||||||
|
usersPubkey: undefined,
|
||||||
|
loginMethod: undefined,
|
||||||
|
nsecBunkerPubkey: undefined
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// clear authToken saved in local storage
|
||||||
|
clearAuthToken()
|
||||||
|
|
||||||
|
// update nsecBunker delegated key
|
||||||
|
const newDelegatedKey =
|
||||||
|
NostrController.getInstance().generateDelegatedKey()
|
||||||
|
saveNsecBunkerDelegatedKey(newDelegatedKey)
|
||||||
|
}
|
||||||
|
|
||||||
const restoredState = loadState()
|
const restoredState = loadState()
|
||||||
if (restoredState) dispatch(restoreState(restoredState))
|
if (restoredState) {
|
||||||
|
dispatch(restoreState(restoredState))
|
||||||
|
|
||||||
|
const { loggedIn, loginMethod, usersPubkey, nsecBunkerRelays } =
|
||||||
|
restoredState.auth
|
||||||
|
|
||||||
|
if (loggedIn) {
|
||||||
|
if (!loginMethod || !usersPubkey) return logout()
|
||||||
|
|
||||||
|
if (loginMethod === LoginMethods.nsecBunker) {
|
||||||
|
if (!nsecBunkerRelays) return logout()
|
||||||
|
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
nostrController.nsecBunkerInit(nsecBunkerRelays).then(() => {
|
||||||
|
nostrController.createNsecBunkerSigner(usersPubkey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
|
if (isLoading) return <LoadingSpinner desc='Loading App' />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBar />
|
<AppBar />
|
||||||
@ -22,7 +68,10 @@ export const MainLayout = () => {
|
|||||||
<Box className='main'>
|
<Box className='main'>
|
||||||
<Container
|
<Container
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative'
|
position: 'relative',
|
||||||
|
maxWidth: {
|
||||||
|
xs: '550px'
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
93
src/pages/decrypt/index.tsx
Normal file
93
src/pages/decrypt/index.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { Box, Button, TextField, Typography } from '@mui/material'
|
||||||
|
import saveAs from 'file-saver'
|
||||||
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
|
import { decryptArrayBuffer } from '../../utils'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
|
export const DecryptZip = () => {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
|
const [encryptionKey, setEncryptionKey] = useState('')
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||||
|
|
||||||
|
const handleDecrypt = async () => {
|
||||||
|
if (!selectedFile || !encryptionKey) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Decrypting zip file')
|
||||||
|
|
||||||
|
const encryptedArrayBuffer = await selectedFile.arrayBuffer()
|
||||||
|
|
||||||
|
const arrayBuffer = await decryptArrayBuffer(
|
||||||
|
encryptedArrayBuffer,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
const blob = new Blob([arrayBuffer])
|
||||||
|
saveAs(blob, 'decrypted.zip')
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setIsDraggingOver(false)
|
||||||
|
const file = event.dataTransfer.files[0]
|
||||||
|
if (file.type === 'application/zip') setSelectedFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setIsDraggingOver(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
|
<Box className={styles.container}>
|
||||||
|
<Typography component='label' variant='h6'>
|
||||||
|
Select encrypted zip file
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className={styles.inputBlock}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
>
|
||||||
|
{isDraggingOver && (
|
||||||
|
<Box className={styles.fileDragOver}>
|
||||||
|
<Typography variant='body1'>Drop file here</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<MuiFileInput
|
||||||
|
placeholder='Drop file here, or click to select'
|
||||||
|
value={selectedFile}
|
||||||
|
onChange={(value) => setSelectedFile(value)}
|
||||||
|
InputProps={{
|
||||||
|
inputProps: {
|
||||||
|
accept: '.zip'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label='Encryption Key'
|
||||||
|
variant='outlined'
|
||||||
|
value={encryptionKey}
|
||||||
|
onChange={(e) => setEncryptionKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button onClick={handleDecrypt} variant='contained'>
|
||||||
|
Decrypt
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
27
src/pages/decrypt/style.module.scss
Normal file
27
src/pages/decrypt/style.module.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@import '../../colors.scss';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: $text-color;
|
||||||
|
|
||||||
|
.inputBlock {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileDragOver {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
589
src/pages/home/index.tsx
Normal file
589
src/pages/home/index.tsx
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
InputLabel,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListSubheader,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material'
|
||||||
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
|
||||||
|
import placeholderAvatar from '../../assets/images/nostr-logo.jpg'
|
||||||
|
import { ProfileMetadata } from '../../types'
|
||||||
|
import { MetadataController, NostrController } from '../../controllers'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
encryptArrayBuffer,
|
||||||
|
generateEncryptionKey,
|
||||||
|
getFileHash,
|
||||||
|
pubToHex,
|
||||||
|
queryNip05,
|
||||||
|
shorten
|
||||||
|
} from '../../utils'
|
||||||
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
|
import { getProfileRoute } from '../../routes'
|
||||||
|
import { Clear } from '@mui/icons-material'
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { State } from '../../store/rootReducer'
|
||||||
|
import { EventTemplate } from 'nostr-tools'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
enum SelectionType {
|
||||||
|
signer = 'Signer',
|
||||||
|
viewer = 'Viewer'
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetadataMap = {
|
||||||
|
[key: string]: ProfileMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HomePage = () => {
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const [type, setType] = useState<SelectionType>(SelectionType.signer)
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
|
const [signers, setSigners] = useState<string[]>([])
|
||||||
|
const [viewers, setViewers] = useState<string[]>([])
|
||||||
|
|
||||||
|
const [metadataMap, setMetadataMap] = useState<MetadataMap>({})
|
||||||
|
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
const [authUrl, setAuthUrl] = useState<string>()
|
||||||
|
|
||||||
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
|
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
|
const handleAddClick = async () => {
|
||||||
|
setError(undefined)
|
||||||
|
|
||||||
|
const addPubkey = (pubkey: string) => {
|
||||||
|
const addElement = (prev: string[]) => {
|
||||||
|
// if key is already in the list just return that
|
||||||
|
if (prev.includes(pubkey)) return prev
|
||||||
|
|
||||||
|
return [...prev, pubkey]
|
||||||
|
}
|
||||||
|
if (type === SelectionType.signer) {
|
||||||
|
setSigners(addElement)
|
||||||
|
} else {
|
||||||
|
setViewers(addElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue.startsWith('npub')) {
|
||||||
|
const pubkey = await pubToHex(inputValue)
|
||||||
|
if (pubkey) {
|
||||||
|
addPubkey(pubkey)
|
||||||
|
setInputValue('')
|
||||||
|
} else {
|
||||||
|
setError('Provided npub is not valid. Please enter correct npub.')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue.includes('@')) {
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Querying for nip05')
|
||||||
|
const nip05Profile = await queryNip05(inputValue)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`error occurred in querying nip05: ${inputValue}`, err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nip05Profile) {
|
||||||
|
const pubkey = nip05Profile.pubkey
|
||||||
|
addPubkey(pubkey)
|
||||||
|
setInputValue('')
|
||||||
|
} else {
|
||||||
|
setError('Provided nip05 is not valid. Please enter correct nip05.')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError('Invalid input! Make sure to provide correct npub or nip05.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemove = (pubkey: string, selectionType: SelectionType) => {
|
||||||
|
if (selectionType === SelectionType.signer) {
|
||||||
|
setSigners((prev) => prev.filter((signer) => signer !== pubkey))
|
||||||
|
} else {
|
||||||
|
setViewers((prev) => prev.filter((viewer) => viewer !== pubkey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectFiles = (files: File[]) => {
|
||||||
|
setSelectedFiles((prev) => {
|
||||||
|
const prevFileNames = prev.map((file) => file.name)
|
||||||
|
|
||||||
|
const newFiles = files.filter(
|
||||||
|
(file) => !prevFileNames.includes(file.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [...prev, ...newFiles]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveFile = (fileToRemove: File) => {
|
||||||
|
setSelectedFiles((prevFiles) =>
|
||||||
|
prevFiles.filter((file) => file.name !== fileToRemove.name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (signers.length === 0) {
|
||||||
|
toast.error('No signer is provided. At least provide one signer.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewers.length === 0) {
|
||||||
|
toast.error('No viewer is provided. At least provide one viewer.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
toast.error('No file is provided. At least provide one file.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Generating hashes for files')
|
||||||
|
|
||||||
|
const fileHashes: { [key: string]: string } = {}
|
||||||
|
|
||||||
|
for (const file of selectedFiles) {
|
||||||
|
const hash = await getFileHash(file)
|
||||||
|
|
||||||
|
fileHashes[file.name] = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
const zip = new JSZip()
|
||||||
|
|
||||||
|
selectedFiles.forEach((file) => {
|
||||||
|
zip.file(`files/${file.name}`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
const event: EventTemplate = {
|
||||||
|
kind: 1,
|
||||||
|
tags: [['r', signers[0]]],
|
||||||
|
content: JSON.stringify(fileHashes),
|
||||||
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
const signedEvent = await nostrController.signEvent(event).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(err.message || 'Error occurred in signing nostr event')
|
||||||
|
setIsLoading(false)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!signedEvent) return
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
fileHashes,
|
||||||
|
submittedBy: usersPubkey,
|
||||||
|
signedEvents: {
|
||||||
|
[signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
||||||
|
|
||||||
|
zip.file('meta.json', stringifiedMeta)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('An error occurred in converting meta json to string')
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Generating zip file')
|
||||||
|
|
||||||
|
const arraybuffer = await zip
|
||||||
|
.generateAsync({
|
||||||
|
type: 'arraybuffer',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: {
|
||||||
|
level: 6
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err in zip:>> ', err)
|
||||||
|
setIsLoading(false)
|
||||||
|
toast.error(err.message || 'Error occurred in generating zip file')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!arraybuffer) return
|
||||||
|
|
||||||
|
const encryptionKey = await generateEncryptionKey()
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Encrypting zip file')
|
||||||
|
const encryptedArrayBuffer = await encryptArrayBuffer(
|
||||||
|
arraybuffer,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
||||||
|
const fileUrl = await uploadToFileStorage(blob)
|
||||||
|
.then((url) => {
|
||||||
|
toast.success('zip file uploaded to file storage')
|
||||||
|
return url
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err in upload:>> ', err)
|
||||||
|
toast.error(err.message || 'Error occurred in uploading zip file')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!fileUrl) return
|
||||||
|
|
||||||
|
await sendDMToFirstSigner(fileUrl, encryptionKey, signers[0])
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadToFileStorage = async (blob: Blob) => {
|
||||||
|
const unixNow = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
const file = new File([blob], `zipped-${unixNow}.zip`, {
|
||||||
|
type: 'application/zip'
|
||||||
|
})
|
||||||
|
|
||||||
|
const event: EventTemplate = {
|
||||||
|
kind: 24242,
|
||||||
|
content: 'Authorize Upload',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['t', 'upload'],
|
||||||
|
['expiration', String(unixNow + 60 * 5)],
|
||||||
|
['name', file.name],
|
||||||
|
['size', String(file.size)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Signing auth event for uploading zip')
|
||||||
|
const authEvent = await nostrController.signEvent(event)
|
||||||
|
|
||||||
|
const FILE_STORAGE_URL = 'https://blossom.sigit.io'
|
||||||
|
|
||||||
|
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)),
|
||||||
|
'Content-Type': 'application/zip'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data.url as string
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendDMToFirstSigner = async (
|
||||||
|
fileUrl: string,
|
||||||
|
encryptionKey: string,
|
||||||
|
pubkey: string
|
||||||
|
) => {
|
||||||
|
const content = `You have been requested for a signature.\nHere is the url for zip file that you can download.\n
|
||||||
|
${fileUrl}\nHowever this zip file is encrypted and you need to decrypt it using https://app.sigit.io\n Encryption key: ${encryptionKey}`
|
||||||
|
|
||||||
|
nostrController.on('nsecbunker-auth', (url) => {
|
||||||
|
setAuthUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('encrypting content for DM')
|
||||||
|
|
||||||
|
// todo: add timeout
|
||||||
|
const encrypted = await nostrController
|
||||||
|
.nip04Encrypt(pubkey, content)
|
||||||
|
.then((res) => {
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err :>> ', err)
|
||||||
|
toast.error(
|
||||||
|
err.message || 'An error occurred while encrypting DM content'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setAuthUrl(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!encrypted) return
|
||||||
|
|
||||||
|
const event: EventTemplate = {
|
||||||
|
kind: 4,
|
||||||
|
content: encrypted,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', signers[0]]]
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('signing event for DM')
|
||||||
|
const signedEvent = await nostrController.signEvent(event).catch((err) => {
|
||||||
|
console.log('err :>> ', err)
|
||||||
|
toast.error(err.message || 'An error occurred while signing event for DM')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!signedEvent) return
|
||||||
|
|
||||||
|
// const metadata = metadataMap[pubkey]
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Publishing encrypted DM')
|
||||||
|
|
||||||
|
// todo: do not use hardcoded relay
|
||||||
|
await nostrController
|
||||||
|
.publishEvent(signedEvent, 'wss://relayable.org')
|
||||||
|
.then(() => {
|
||||||
|
toast.success('DM sent to first signer')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err :>> ', err)
|
||||||
|
toast.error(err.message || 'An error occurred while publishing DM')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authUrl) {
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
title='Nsecbunker auth'
|
||||||
|
src={authUrl}
|
||||||
|
width='100%'
|
||||||
|
height='500px'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
|
<Box className={styles.container}>
|
||||||
|
<Typography component='label' variant='h6'>
|
||||||
|
Select signers and viewers
|
||||||
|
</Typography>
|
||||||
|
<Box className={styles.inputBlock}>
|
||||||
|
<TextField
|
||||||
|
label='nip05 / npub'
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
helperText={error}
|
||||||
|
error={!!error}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='select-type-label'>Type</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId='select-type-label'
|
||||||
|
id='demo-simple-select'
|
||||||
|
value={type}
|
||||||
|
label='Type'
|
||||||
|
onChange={(e) => setType(e.target.value as SelectionType)}
|
||||||
|
>
|
||||||
|
<MenuItem value={SelectionType.signer}>
|
||||||
|
{SelectionType.signer}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={SelectionType.viewer}>
|
||||||
|
{SelectionType.viewer}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
disabled={!inputValue}
|
||||||
|
onClick={handleAddClick}
|
||||||
|
variant='contained'
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography component='label' variant='h6'>
|
||||||
|
Select files
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<MuiFileInput
|
||||||
|
multiple
|
||||||
|
placeholder='Choose Files'
|
||||||
|
value={selectedFiles}
|
||||||
|
onChange={(value) => handleSelectFiles(value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{selectedFiles.map((file, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<Typography component='label'>{file.name}</Typography>
|
||||||
|
<IconButton onClick={() => handleRemoveFile(file)}>
|
||||||
|
<Clear style={{ color: 'red' }} />{' '}
|
||||||
|
</IconButton>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{signers.length > 0 && (
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
marginTop: 2
|
||||||
|
}}
|
||||||
|
subheader={
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
paddingBottom: 1,
|
||||||
|
paddingTop: 1,
|
||||||
|
fontSize: '1.5rem'
|
||||||
|
}}
|
||||||
|
className={styles.subHeader}
|
||||||
|
>
|
||||||
|
Signers
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{signers.map((signer, index) => (
|
||||||
|
<DisplaySignerOrViewer
|
||||||
|
key={`signer-${index}`}
|
||||||
|
pubkey={signer}
|
||||||
|
metadataMap={metadataMap}
|
||||||
|
setMetadataMap={setMetadataMap}
|
||||||
|
remove={() => handleRemove(signer, SelectionType.signer)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewers.length > 0 && (
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
marginTop: 2
|
||||||
|
}}
|
||||||
|
subheader={
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
paddingBottom: 1,
|
||||||
|
paddingTop: 1,
|
||||||
|
fontSize: '1.5rem'
|
||||||
|
}}
|
||||||
|
className={styles.subHeader}
|
||||||
|
>
|
||||||
|
Viewers
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{viewers.map((viewer, index) => (
|
||||||
|
<DisplaySignerOrViewer
|
||||||
|
key={`viewer-${index}`}
|
||||||
|
pubkey={viewer}
|
||||||
|
metadataMap={metadataMap}
|
||||||
|
setMetadataMap={setMetadataMap}
|
||||||
|
remove={() => handleRemove(viewer, SelectionType.viewer)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button onClick={handleSubmit} variant='contained'>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplaySignerOrViewerProps = {
|
||||||
|
pubkey: string
|
||||||
|
metadataMap: MetadataMap
|
||||||
|
setMetadataMap: Dispatch<SetStateAction<MetadataMap>>
|
||||||
|
remove: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplaySignerOrViewer = ({
|
||||||
|
pubkey,
|
||||||
|
metadataMap,
|
||||||
|
setMetadataMap,
|
||||||
|
remove
|
||||||
|
}: DisplaySignerOrViewerProps) => {
|
||||||
|
const [metadata, setMetadata] = useState<ProfileMetadata>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getMetadata = async (pubkey: string) => {
|
||||||
|
console.log('1 :>> ', 1)
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
const metadataEvent = await metadataController
|
||||||
|
.findMetadata(pubkey)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(
|
||||||
|
`error occurred in finding metadata for: ${pubkey}`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (metadataEvent) {
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(metadataEvent)
|
||||||
|
if (metadataContent) {
|
||||||
|
setMetadata(metadataContent)
|
||||||
|
setMetadataMap((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[pubkey]: metadataContent
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMetadata = metadataMap[pubkey]
|
||||||
|
|
||||||
|
console.log('metadataMap :>> ', metadataMap)
|
||||||
|
|
||||||
|
if (existingMetadata) {
|
||||||
|
setMetadata(existingMetadata)
|
||||||
|
} else {
|
||||||
|
getMetadata(pubkey)
|
||||||
|
}
|
||||||
|
}, [pubkey, metadataMap])
|
||||||
|
|
||||||
|
const imageLoadError = (event: any) => {
|
||||||
|
event.target.src = placeholderAvatar
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem sx={{ marginTop: 1 }} className={styles.listItem}>
|
||||||
|
<img
|
||||||
|
onError={imageLoadError}
|
||||||
|
src={metadata?.picture || placeholderAvatar}
|
||||||
|
alt='Profile Image'
|
||||||
|
className={styles.img}
|
||||||
|
/>
|
||||||
|
<Link to={getProfileRoute(pubkey)}>
|
||||||
|
<Typography component='label' className={styles.name}>
|
||||||
|
{metadata?.display_name || metadata?.name || shorten(pubkey)}
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
<IconButton onClick={remove}>
|
||||||
|
<Clear style={{ color: 'red' }} />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
33
src/pages/home/style.module.scss
Normal file
33
src/pages/home/style.module.scss
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@import '../../colors.scss';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: $text-color;
|
||||||
|
|
||||||
|
.inputBlock {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subHeader {
|
||||||
|
border-bottom: 0.5px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
updateKeyPair,
|
updateKeyPair,
|
||||||
updateLoginMethod,
|
updateLoginMethod,
|
||||||
updateNsecbunkerPubkey
|
updateNsecbunkerPubkey,
|
||||||
|
updateNsecbunkerRelays
|
||||||
} from '../../store/actions'
|
} from '../../store/actions'
|
||||||
import { LoginMethods } from '../../store/auth/types'
|
import { LoginMethods } from '../../store/auth/types'
|
||||||
import { Dispatch } from '../../store/store'
|
import { Dispatch } from '../../store/store'
|
||||||
@ -183,6 +184,7 @@ export const Login = () => {
|
|||||||
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||||
|
dispatch(updateNsecbunkerRelays(relays))
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
@ -242,6 +244,7 @@ export const Login = () => {
|
|||||||
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||||
|
dispatch(updateNsecbunkerRelays([relay]))
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
|
import { DecryptZip } from '../pages/decrypt'
|
||||||
|
import { HomePage } from '../pages/home'
|
||||||
import { LandingPage } from '../pages/landing/LandingPage'
|
import { LandingPage } from '../pages/landing/LandingPage'
|
||||||
import { Login } from '../pages/login'
|
import { Login } from '../pages/login'
|
||||||
import { ProfilePage } from '../pages/profile'
|
import { ProfilePage } from '../pages/profile'
|
||||||
import { hexToNpub } from '../utils'
|
import { hexToNpub } from '../utils'
|
||||||
|
|
||||||
export const appPrivateRoutes = {
|
export const appPrivateRoutes = {
|
||||||
homePage: '/'
|
homePage: '/',
|
||||||
|
decryptZip: '/decrypt-zip'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appPublicRoutes = {
|
export const appPublicRoutes = {
|
||||||
profile: '/profile/:npub',
|
profile: '/profile/:npub',
|
||||||
|
landingPage: '/',
|
||||||
login: '/login',
|
login: '/login',
|
||||||
help: 'https://help.sigit.io'
|
help: 'https://help.sigit.io'
|
||||||
}
|
}
|
||||||
@ -17,6 +21,11 @@ export const getProfileRoute = (hexKey: string) =>
|
|||||||
appPublicRoutes.profile.replace(':npub', hexToNpub(hexKey))
|
appPublicRoutes.profile.replace(':npub', hexToNpub(hexKey))
|
||||||
|
|
||||||
export const publicRoutes = [
|
export const publicRoutes = [
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.landingPage,
|
||||||
|
hiddenWhenLoggedIn: true,
|
||||||
|
element: <LandingPage />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: appPublicRoutes.login,
|
path: appPublicRoutes.login,
|
||||||
hiddenWhenLoggedIn: true,
|
hiddenWhenLoggedIn: true,
|
||||||
@ -31,6 +40,10 @@ export const publicRoutes = [
|
|||||||
export const privateRoutes = [
|
export const privateRoutes = [
|
||||||
{
|
{
|
||||||
path: appPrivateRoutes.homePage,
|
path: appPrivateRoutes.homePage,
|
||||||
element: <LandingPage />
|
element: <HomePage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.decryptZip,
|
||||||
|
element: <DecryptZip />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -4,5 +4,6 @@ export const SET_AUTH_STATE = 'SET_AUTH_STATE'
|
|||||||
export const UPDATE_LOGIN_METHOD = 'UPDATE_LOGIN_METHOD'
|
export const UPDATE_LOGIN_METHOD = 'UPDATE_LOGIN_METHOD'
|
||||||
export const UPDATE_KEYPAIR = 'UPDATE_KEYPAIR'
|
export const UPDATE_KEYPAIR = 'UPDATE_KEYPAIR'
|
||||||
export const UPDATE_NSECBUNKER_PUBKEY = 'UPDATE_NSECBUNKER_PUBKEY'
|
export const UPDATE_NSECBUNKER_PUBKEY = 'UPDATE_NSECBUNKER_PUBKEY'
|
||||||
|
export const UPDATE_NSECBUNKER_RELAYS = 'UPDATE_NSECBUNKER_RELAYS'
|
||||||
|
|
||||||
export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'
|
export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'
|
||||||
|
@ -6,7 +6,8 @@ import {
|
|||||||
SetAuthState,
|
SetAuthState,
|
||||||
UpdateKeyPair,
|
UpdateKeyPair,
|
||||||
UpdateLoginMethod,
|
UpdateLoginMethod,
|
||||||
UpdateNsecBunkerPubkey
|
UpdateNsecBunkerPubkey,
|
||||||
|
UpdateNsecBunkerRelays
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
export const setAuthState = (payload: AuthState): SetAuthState => ({
|
export const setAuthState = (payload: AuthState): SetAuthState => ({
|
||||||
@ -32,3 +33,10 @@ export const updateNsecbunkerPubkey = (
|
|||||||
type: ActionTypes.UPDATE_NSECBUNKER_PUBKEY,
|
type: ActionTypes.UPDATE_NSECBUNKER_PUBKEY,
|
||||||
payload
|
payload
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const updateNsecbunkerRelays = (
|
||||||
|
payload: string[] | undefined
|
||||||
|
): UpdateNsecBunkerRelays => ({
|
||||||
|
type: ActionTypes.UPDATE_NSECBUNKER_RELAYS,
|
||||||
|
payload
|
||||||
|
})
|
||||||
|
@ -11,12 +11,13 @@ const reducer = (
|
|||||||
): AuthState | null => {
|
): AuthState | null => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.SET_AUTH_STATE: {
|
case ActionTypes.SET_AUTH_STATE: {
|
||||||
const { loginMethod, keyPair, nsecBunkerPubkey } = state
|
const { loginMethod, keyPair, nsecBunkerPubkey, nsecBunkerRelays } = state
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loginMethod,
|
loginMethod,
|
||||||
keyPair,
|
keyPair,
|
||||||
nsecBunkerPubkey,
|
nsecBunkerPubkey,
|
||||||
|
nsecBunkerRelays,
|
||||||
...action.payload
|
...action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,6 +48,18 @@ const reducer = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ActionTypes.UPDATE_NSECBUNKER_RELAYS: {
|
||||||
|
const { payload } = action
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nsecBunkerRelays: payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.RESTORE_STATE:
|
||||||
|
return action.payload.auth
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as ActionTypes from '../actionTypes'
|
import * as ActionTypes from '../actionTypes'
|
||||||
|
import { RestoreState } from '../actions'
|
||||||
|
|
||||||
export enum LoginMethods {
|
export enum LoginMethods {
|
||||||
extension = 'extension',
|
extension = 'extension',
|
||||||
@ -18,6 +19,7 @@ export interface AuthState {
|
|||||||
loginMethod?: LoginMethods
|
loginMethod?: LoginMethods
|
||||||
keyPair?: Keys
|
keyPair?: Keys
|
||||||
nsecBunkerPubkey?: string
|
nsecBunkerPubkey?: string
|
||||||
|
nsecBunkerRelays?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetAuthState {
|
export interface SetAuthState {
|
||||||
@ -40,8 +42,15 @@ export interface UpdateNsecBunkerPubkey {
|
|||||||
payload: string | undefined
|
payload: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateNsecBunkerRelays {
|
||||||
|
type: typeof ActionTypes.UPDATE_NSECBUNKER_RELAYS
|
||||||
|
payload: string[] | undefined
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthDispatchTypes =
|
export type AuthDispatchTypes =
|
||||||
|
| RestoreState
|
||||||
| SetAuthState
|
| SetAuthState
|
||||||
| UpdateLoginMethod
|
| UpdateLoginMethod
|
||||||
| UpdateKeyPair
|
| UpdateKeyPair
|
||||||
| UpdateNsecBunkerPubkey
|
| UpdateNsecBunkerPubkey
|
||||||
|
| UpdateNsecBunkerRelays
|
||||||
|
71
src/utils/crypto.ts
Normal file
71
src/utils/crypto.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { hexToString, stringToHex } from '.'
|
||||||
|
|
||||||
|
const ENCRYPTION_ALGO_NAME = 'AES-GCM'
|
||||||
|
|
||||||
|
export const generateEncryptionKey = async () => {
|
||||||
|
const cryptoKey = await window.crypto.subtle.generateKey(
|
||||||
|
{ name: ENCRYPTION_ALGO_NAME, length: 128 },
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
|
||||||
|
const key = await window.crypto.subtle.exportKey('jwk', cryptoKey)
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(key)
|
||||||
|
const hexKey = stringToHex(jsonString)
|
||||||
|
|
||||||
|
const iv = new TextDecoder().decode(
|
||||||
|
window.crypto.getRandomValues(new Uint8Array(16))
|
||||||
|
)
|
||||||
|
|
||||||
|
return `${hexKey}?iv=${iv}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importKey = async (key: string) => {
|
||||||
|
const splittedKey = key.split('?iv=')
|
||||||
|
const keyString = hexToString(splittedKey[0])
|
||||||
|
const jsonWebKey = JSON.parse(keyString)
|
||||||
|
const iv = new TextEncoder().encode(splittedKey[1])
|
||||||
|
|
||||||
|
const cryptoKey = await window.crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
jsonWebKey,
|
||||||
|
{ name: ENCRYPTION_ALGO_NAME },
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
|
||||||
|
return { cryptoKey, iv }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const encryptArrayBuffer = async (
|
||||||
|
arrayBuffer: ArrayBuffer,
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
|
const { cryptoKey, iv } = await importKey(key)
|
||||||
|
|
||||||
|
// Encrypt the data
|
||||||
|
const encryptedData = await window.crypto.subtle.encrypt(
|
||||||
|
{ name: ENCRYPTION_ALGO_NAME, iv },
|
||||||
|
cryptoKey,
|
||||||
|
arrayBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
return encryptedData
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decryptArrayBuffer = async (
|
||||||
|
encryptedData: ArrayBuffer,
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
|
const { cryptoKey, iv } = await importKey(key)
|
||||||
|
|
||||||
|
// Decrypt the data
|
||||||
|
const decryptedData = await window.crypto.subtle.decrypt(
|
||||||
|
{ name: ENCRYPTION_ALGO_NAME, iv },
|
||||||
|
cryptoKey,
|
||||||
|
encryptedData
|
||||||
|
)
|
||||||
|
|
||||||
|
return decryptedData
|
||||||
|
}
|
16
src/utils/file.ts
Normal file
16
src/utils/file.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { sha256 } from 'crypto-hash'
|
||||||
|
|
||||||
|
export const getFileHash = (file: File) => {
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = async () => {
|
||||||
|
if (reader.result) {
|
||||||
|
const hash = await sha256(reader.result)
|
||||||
|
resolve(hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsBinaryString(file)
|
||||||
|
})
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
export * from './crypto'
|
||||||
|
export * from './file'
|
||||||
export * from './localStorage'
|
export * from './localStorage'
|
||||||
export * from './nostr'
|
export * from './nostr'
|
||||||
export * from './string'
|
export * from './string'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { nip05, nip19, verifyEvent } from 'nostr-tools'
|
import { nip19, verifyEvent } from 'nostr-tools'
|
||||||
import { SignedEvent } from '../types'
|
import { SignedEvent } from '../types'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@ -12,19 +12,12 @@ const validateHex = (hexKey: string) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* NPUB provided - it will convert NPUB to HEX
|
* NPUB provided - it will convert NPUB to HEX
|
||||||
* NIP-05 provided - it will query NIP-05 profile and return HEX key if found
|
|
||||||
* HEX provided - it will return HEX
|
* HEX provided - it will return HEX
|
||||||
*
|
*
|
||||||
* @param pubKey in NPUB, NIP-05 or HEX format
|
* @param pubKey in NPUB, HEX format
|
||||||
* @returns HEX format
|
* @returns HEX format
|
||||||
*/
|
*/
|
||||||
export const pubToHex = async (pubKey: string): Promise<string | null> => {
|
export const pubToHex = async (pubKey: string): Promise<string | null> => {
|
||||||
// If key is NIP-05
|
|
||||||
if (pubKey.indexOf('@') !== -1)
|
|
||||||
return Promise.resolve(
|
|
||||||
(await nip05.queryProfile(pubKey).then((res) => res?.pubkey)) || null
|
|
||||||
)
|
|
||||||
|
|
||||||
// If key is NPUB
|
// If key is NPUB
|
||||||
if (pubKey.startsWith('npub')) {
|
if (pubKey.startsWith('npub')) {
|
||||||
try {
|
try {
|
||||||
|
@ -7,3 +7,33 @@ export const shorten = (str: string, offset = 9) => {
|
|||||||
str.length
|
str.length
|
||||||
)}`
|
)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const stringToHex = (str: string) => {
|
||||||
|
// Convert the string to an array of UTF-16 code units using the spread operator
|
||||||
|
const codeUnits = [...str]
|
||||||
|
|
||||||
|
// Map each code unit to its hexadecimal representation
|
||||||
|
const hexChars = codeUnits.map((codeUnit) => {
|
||||||
|
// Convert the code unit to its hexadecimal representation with leading zeros
|
||||||
|
const hex = codeUnit.charCodeAt(0).toString(16).padStart(2, '0')
|
||||||
|
return hex
|
||||||
|
})
|
||||||
|
|
||||||
|
// Join the hexadecimal characters into a single string
|
||||||
|
const hexString = hexChars.join('')
|
||||||
|
|
||||||
|
// Return the resulting hexadecimal string
|
||||||
|
return hexString
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hexToString = (hex: string) => {
|
||||||
|
// Split the hex string into pairs of two characters
|
||||||
|
const pairs = hex.match(/.{1,2}/g) || []
|
||||||
|
|
||||||
|
// Convert each pair from hexadecimal to its decimal equivalent,
|
||||||
|
// then convert each decimal value to its character representation
|
||||||
|
const chars = pairs.map((pair) => String.fromCharCode(parseInt(pair, 16)))
|
||||||
|
|
||||||
|
// Join the resulting characters into a single string
|
||||||
|
return chars.join('')
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user