Compare commits

..

68 Commits

Author SHA1 Message Date
19b815e528 feat(opentimestamps): updates tooltip
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 27s
2024-10-24 12:42:21 +03:00
54047740f9 chore: updates packages 2024-10-18 11:26:25 +03:00
7f411f09a7 chore: merge branch 'main' into issue-166-open-timestamps 2024-10-18 11:24:31 +03:00
849e47da00 chore: updates packages 2024-10-18 11:03:51 +03:00
b
bb323be87c Merge pull request 'Release' (#228) from staging into main
All checks were successful
Release to Production / build_and_release (push) Successful in 1m8s
Reviewed-on: #228
2024-10-14 09:02:41 +00:00
b
fd2f179273 Merge branch 'main' into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m18s
2024-10-14 09:02:14 +00:00
b
4559f16d86 Merge pull request 'fix: add files and marked to sign page exports' (#226) from fixes-10-11 into staging
Some checks failed
Release to Staging / build_and_release (push) Has been cancelled
Reviewed-on: #226
2024-10-14 09:01:42 +00:00
d6f92accb0 chore(git): merge branch 'origin/fixes-10-11' into fixes-10-11
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-10-14 09:56:22 +02:00
b
ee03cc545e Merge branch 'staging' into fixes-10-11
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-13 12:47:38 +00:00
b
70e525357c Merge pull request 'feat: handle root _@ users on add counterpart' (#225) from adding-domain-user-10-10 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m23s
Reviewed-on: #225
Reviewed-by: eugene <eugene@nostrdev.com>
2024-10-13 12:46:55 +00:00
b
3eed2964a0 Merge branch 'staging' into fixes-10-11
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-12 11:19:14 +00:00
b
3a0f155010 Merge pull request 'fix: processing events, stale sigits' (#227) from hotfix-processing-events-10-12 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m21s
Reviewed-on: #227
2024-10-12 11:18:46 +00:00
1d1986f082 fix: clear hasSubscribed after the logout
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 35s
2024-10-12 12:05:55 +02:00
25764c7ab4 fix: processing events
Partially revert to before 23a04faad8
2024-10-12 11:52:43 +02:00
cc382f0726 fix: show error if decrypt fails 2024-10-11 16:43:55 +02:00
9dd190d65b fix: add files and marked to sign page exports
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
Skip marked if the file contains  no marks
2024-10-11 16:16:59 +02:00
c3dacbe111 fix: add mark label
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-10-11 15:05:28 +02:00
897daaa1fa feat: handle root _@ users on add counterpart
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 35s
2024-10-10 13:56:08 +02:00
b
ed90168e5d Merge pull request 'staging' (#223) from staging into main
All checks were successful
Release to Production / build_and_release (push) Successful in 1m8s
Reviewed-on: #223
2024-10-09 13:50:23 +00:00
b
7f5fd4534f Merge branch 'main' into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m20s
2024-10-09 13:48:05 +00:00
b
7f172178a1 Merge pull request 'feat: include marked and original files in zip' (#222) from 203-export-original-and-modified into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m23s
Reviewed-on: #222
Reviewed-by: b <b@4j.cx>
2024-10-09 13:43:02 +00:00
1116867224 refactor: rename functions and labels
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-09 11:52:32 +02:00
db9cf9d20c feat: include the original files always 2024-10-09 11:51:26 +02:00
58c457b62c fix: profile image scale 2024-10-09 10:58:31 +02:00
b6846c0006 chore(git): merge pull request #217 from nostr-login-9-30 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m18s
Reviewed-on: #217
Reviewed-by: eugene <eugene@nostrdev.com>
2024-10-09 08:54:33 +00:00
8deb5bd7cd Merge branch 'staging' into nostr-login-9-30
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-09 08:35:13 +00:00
7b7f23a779 chore(git): merge pull request #221 from beta-release into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m16s
Reviewed-on: #221
2024-10-09 08:33:52 +00:00
db4a202363 refactor: update banner and package for beta release
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-08 20:31:03 +02:00
3a507246ca refactor: add comments
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 32s
2024-10-08 20:25:34 +02:00
f09d9b2378 refactor(ts): remove type assertion
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-08 20:14:44 +02:00
fe9f282984 Merge branch 'staging' into nostr-login-9-30
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-08 18:06:40 +00:00
aa4637dd0d refactor(login): add comments
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-10-08 19:12:21 +02:00
23a04faad8 refactor(auth): main effect order and deps
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-10-08 18:50:33 +02:00
ad2ec070be refactor(reducers): match state types and reducers 2024-10-08 18:42:34 +02:00
d610c79cad refactor: use useAppDispatch, useAppSelector hooks 2024-10-08 17:08:43 +02:00
6c5ed3a69c fix: typo 2024-10-08 13:47:30 +02:00
862012e405 refactor: update kind 27235 auth event with recommendations from nip98 2024-10-08 13:46:54 +02:00
8689c7f753 fix: remove screen on nostr-login launch
Ignores the init options param when screen is passed
2024-10-08 13:45:58 +02:00
b
a3c45b504e Merge pull request 'Release Oct 7th' (#218) from staging into main
All checks were successful
Release to Production / build_and_release (push) Successful in 1m10s
Reviewed-on: #218
2024-10-08 09:02:11 +00:00
b
da30dba368 Merge branch 'main' into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m15s
2024-10-08 08:59:35 +00:00
.
51e2ab6f8a chore: lint fix
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m18s
2024-10-08 09:57:44 +01:00
.
9091bbc251 chore: landing page wording
Some checks failed
Release to Staging / build_and_release (push) Failing after 33s
2024-10-07 22:55:48 +01:00
331759de5c refactor: add useCallback, add methods and split effects
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-10-07 20:37:46 +02:00
995c7ce293 feat(auth): nsec login with url params
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-10-07 19:17:19 +02:00
532cdaed8e refactor(auth): open nostr-login directly 2024-10-07 15:36:29 +02:00
67d545de2f fix: show import/export only for local 2024-10-07 13:44:04 +02:00
637e26bf35 refactor: init nostr-login, login method strategies, remove bunker 2024-10-07 13:36:45 +02:00
110621a125 feat: add nostrLoginAuthMethod to state 2024-10-07 13:36:45 +02:00
59e153595a refactor: z-index levels 2024-10-07 13:36:45 +02:00
0b79ebd909 refactor: z-index levels 2024-10-07 13:36:45 +02:00
e2dbed2b03 refactor: add nostr-login package, show not-allowed on disabled form fields 2024-10-07 13:36:45 +02:00
7c26edf84e chore(git): merge pull request #219 from fixes-10-7 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m20s
Reviewed-on: #219
Reviewed-by: eugene <eugene@nostrdev.com>
2024-10-07 11:27:41 +00:00
2d7bb234f4 refactor: next on each mark, including the final one
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-10-07 12:59:55 +02:00
c4d50293ff refactor: toolbox order 2024-10-07 12:53:43 +02:00
b
89971fb176 Merge pull request 'Add Sigit ID as a path param' (#195) from issue-171 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m17s
Reviewed-on: #195
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-10-07 09:16:30 +00:00
b
acad24dc06 Merge branch 'staging' into issue-171
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-10-07 09:15:38 +00:00
.
55abe814c9 fix: AGPL Licence, closes #197
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m21s
2024-10-06 20:28:27 +01:00
b
e33996c1f9 Merge branch 'staging' into issue-171
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 35s
2024-10-05 21:02:40 +00:00
6ba3b6ec89 Merge branch 'staging' into issue-171
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-16 12:37:39 +00:00
b
aa8214d015 Merge branch 'staging' into issue-171
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-13 10:07:24 +00:00
e48a396990 fix: verify/sign link
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 17:29:47 +02:00
79e14d45a1 chore: comment fix
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 15:41:42 +02:00
64e8ebba85 chore: renamed sigitKey to sigitCreateId
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 13:30:39 +02:00
5dc8d53503 chore: sigitCreateId naming
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-11 12:33:40 +02:00
86a16c13ce chore: comments and lint (typing)
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-11 12:29:38 +02:00
7c027825cd style: lint fix
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-11 12:03:23 +02:00
8e71592d88 fix: routing, removed useEffect 2024-09-11 11:59:12 +02:00
75a715d002 feat: Add Sigit ID as a path param
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-10 16:00:48 +02:00
55 changed files with 1371 additions and 1397 deletions

497
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "sigit", "name": "sigit",
"version": "0.0.0", "version": "0.0.0-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sigit", "name": "sigit",
"version": "0.0.0", "version": "0.0.0-beta",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-or-later ", "license": "AGPL-3.0-or-later ",
"dependencies": { "dependencies": {
@ -32,6 +32,7 @@
"jszip": "3.10.1", "jszip": "3.10.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"mui-file-input": "4.0.4", "mui-file-input": "4.0.4",
"nostr-login": "^1.6.6",
"nostr-tools": "2.7.0", "nostr-tools": "2.7.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.4.168", "pdfjs-dist": "^4.4.168",
@ -614,13 +615,14 @@
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"aix" "aix"
@ -630,13 +632,14 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
@ -646,13 +649,14 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
@ -662,13 +666,14 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
@ -678,13 +683,14 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@ -694,13 +700,14 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@ -710,13 +717,14 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
@ -726,13 +734,14 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
@ -742,13 +751,14 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -758,13 +768,14 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -774,13 +785,14 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -790,13 +802,14 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -806,13 +819,14 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -822,13 +836,14 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -838,13 +853,14 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -854,13 +870,14 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -870,13 +887,14 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -886,13 +904,14 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"netbsd" "netbsd"
@ -902,13 +921,14 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"openbsd" "openbsd"
@ -918,13 +938,14 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"sunos" "sunos"
@ -934,13 +955,14 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@ -950,13 +972,14 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@ -966,13 +989,14 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@ -1833,208 +1857,224 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz",
"integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz",
"integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz",
"integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz",
"integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz",
"integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz",
"integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz",
"integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz",
"integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz",
"integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz",
"integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz",
"integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz",
"integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz",
"integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz",
"integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz",
"integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz",
"integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@ -2192,10 +2232,11 @@
"dev": true "dev": true
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/@types/file-saver": { "node_modules/@types/file-saver": {
"version": "2.0.7", "version": "2.0.7",
@ -3380,11 +3421,12 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
@ -3392,29 +3434,29 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2", "@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.20.2", "@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.20.2", "@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.20.2", "@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.20.2", "@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.20.2", "@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.20.2", "@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.20.2", "@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.20.2", "@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.20.2" "@esbuild/win32-x64": "0.21.5"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -4930,9 +4972,9 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.7", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5236,6 +5278,72 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/nostr-login": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.6.tgz",
"integrity": "sha512-XOpB9nG3Qgt7iea7gA1zn4TaTfUKCKGdCHKwErqLPtMk/q1Rhkzj5cq/66iU0WqC6mSiwENfTy1p4qaM7HzMtg==",
"license": "MIT",
"dependencies": {
"@nostr-dev-kit/ndk": "^2.3.1",
"nostr-tools": "^1.17.0",
"tseep": "^1.2.1"
}
},
"node_modules/nostr-login/node_modules/@noble/ciphers": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-login/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-login/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-login/node_modules/nostr-tools": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz",
"integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "0.2.0",
"@noble/curves": "1.1.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-tools": { "node_modules/nostr-tools": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.0.tgz", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.0.tgz",
@ -5545,10 +5653,11 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true "dev": true,
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -5576,9 +5685,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.38", "version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -5594,10 +5703,11 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.1.0",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@ -6172,12 +6282,13 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.18.0", "version": "4.22.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz",
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.6"
}, },
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@ -6187,22 +6298,22 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.18.0", "@rollup/rollup-android-arm-eabi": "4.22.5",
"@rollup/rollup-android-arm64": "4.18.0", "@rollup/rollup-android-arm64": "4.22.5",
"@rollup/rollup-darwin-arm64": "4.18.0", "@rollup/rollup-darwin-arm64": "4.22.5",
"@rollup/rollup-darwin-x64": "4.18.0", "@rollup/rollup-darwin-x64": "4.22.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.18.0", "@rollup/rollup-linux-arm-gnueabihf": "4.22.5",
"@rollup/rollup-linux-arm-musleabihf": "4.18.0", "@rollup/rollup-linux-arm-musleabihf": "4.22.5",
"@rollup/rollup-linux-arm64-gnu": "4.18.0", "@rollup/rollup-linux-arm64-gnu": "4.22.5",
"@rollup/rollup-linux-arm64-musl": "4.18.0", "@rollup/rollup-linux-arm64-musl": "4.22.5",
"@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5",
"@rollup/rollup-linux-riscv64-gnu": "4.18.0", "@rollup/rollup-linux-riscv64-gnu": "4.22.5",
"@rollup/rollup-linux-s390x-gnu": "4.18.0", "@rollup/rollup-linux-s390x-gnu": "4.22.5",
"@rollup/rollup-linux-x64-gnu": "4.18.0", "@rollup/rollup-linux-x64-gnu": "4.22.5",
"@rollup/rollup-linux-x64-musl": "4.18.0", "@rollup/rollup-linux-x64-musl": "4.22.5",
"@rollup/rollup-win32-arm64-msvc": "4.18.0", "@rollup/rollup-win32-arm64-msvc": "4.22.5",
"@rollup/rollup-win32-ia32-msvc": "4.18.0", "@rollup/rollup-win32-ia32-msvc": "4.22.5",
"@rollup/rollup-win32-x64-msvc": "4.18.0", "@rollup/rollup-win32-x64-msvc": "4.22.5",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -6432,10 +6543,11 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true, "dev": true,
"license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -6930,14 +7042,15 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.12", "version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.21.3",
"postcss": "^8.4.38", "postcss": "^8.4.43",
"rollup": "^4.13.0" "rollup": "^4.20.0"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@ -6956,6 +7069,7 @@
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.4.0"
@ -6973,6 +7087,9 @@
"sass": { "sass": {
"optional": true "optional": true
}, },
"sass-embedded": {
"optional": true
},
"stylus": { "stylus": {
"optional": true "optional": true
}, },

View File

@ -1,7 +1,7 @@
{ {
"name": "sigit", "name": "sigit",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0-beta",
"type": "module", "type": "module",
"homepage": "https://sigit.io/", "homepage": "https://sigit.io/",
"license": "AGPL-3.0-or-later ", "license": "AGPL-3.0-or-later ",
@ -42,6 +42,7 @@
"jszip": "3.10.1", "jszip": "3.10.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"mui-file-input": "4.0.4", "mui-file-input": "4.0.4",
"nostr-login": "^1.6.6",
"nostr-tools": "2.7.0", "nostr-tools": "2.7.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.4.168", "pdfjs-dist": "^4.4.168",

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useSelector } from 'react-redux' import { useAppSelector } from './hooks/store'
import { Navigate, Route, Routes } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { AuthController, NostrController } from './controllers' import { AuthController } from './controllers'
import { MainLayout } from './layouts/Main' import { MainLayout } from './layouts/Main'
import { import {
appPrivateRoutes, appPrivateRoutes,
@ -10,12 +10,10 @@ import {
publicRoutes, publicRoutes,
recursiveRouteRenderer recursiveRouteRenderer
} from './routes' } from './routes'
import { State } from './store/rootReducer'
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
import './App.scss' import './App.scss'
const App = () => { const App = () => {
const authState = useSelector((state: State) => state.auth) const authState = useAppSelector((state) => state.auth)
useEffect(() => { useEffect(() => {
if (window.location.hostname === '0.0.0.0') { if (window.location.hostname === '0.0.0.0') {
@ -25,23 +23,10 @@ const App = () => {
window.location.hostname = 'localhost' window.location.hostname = 'localhost'
} }
generateBunkerDelegatedKey()
const authController = new AuthController() const authController = new AuthController()
authController.checkSession() authController.checkSession()
}, []) }, [])
const generateBunkerDelegatedKey = () => {
const existingKey = getNsecBunkerDelegatedKey()
if (!existingKey) {
const nostrController = NostrController.getInstance()
const newDelegatedKey = nostrController.generateDelegatedKey()
saveNsecBunkerDelegatedKey(newDelegatedKey)
}
}
const handleRootRedirect = () => { const handleRootRedirect = () => {
if (authState.loggedIn) return appPrivateRoutes.homePage if (authState.loggedIn) return appPrivateRoutes.homePage
const callbackPathEncoded = btoa( const callbackPathEncoded = btoa(

View File

@ -9,51 +9,36 @@ import {
} from '@mui/material' } from '@mui/material'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useAppSelector } from '../../hooks/store'
import {
setAuthState,
setMetadataEvent,
userLogOutAction
} from '../../store/actions'
import { State } from '../../store/rootReducer'
import { Dispatch } from '../../store/store'
import Username from '../username' import Username from '../username'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { MetadataController, NostrController } from '../../controllers'
import { import {
appPublicRoutes, appPublicRoutes,
appPrivateRoutes, appPrivateRoutes,
getProfileRoute getProfileRoute
} from '../../routes' } from '../../routes'
import { import { getProfileUsername, hexToNpub } from '../../utils'
clearAuthToken,
getProfileUsername,
hexToNpub,
saveNsecBunkerDelegatedKey
} from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { setUserRobotImage } from '../../store/userRobotImage/action'
import { Container } from '../Container' import { Container } from '../Container'
import { ButtonIcon } from '../ButtonIcon' import { ButtonIcon } from '../ButtonIcon'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClose } from '@fortawesome/free-solid-svg-icons' import { faClose } from '@fortawesome/free-solid-svg-icons'
import useMediaQuery from '@mui/material/useMediaQuery' import useMediaQuery from '@mui/material/useMediaQuery'
import { useLogout } from '../../hooks/useLogout'
const metadataController = MetadataController.getInstance() import { launch as launchNostrLoginDialog } from 'nostr-login'
export const AppBar = () => { export const AppBar = () => {
const navigate = useNavigate() const navigate = useNavigate()
const logout = useLogout()
const dispatch: Dispatch = useDispatch()
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [userAvatar, setUserAvatar] = useState('') const [userAvatar, setUserAvatar] = useState('')
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null) const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
const authState = useSelector((state: State) => state.auth) const authState = useAppSelector((state) => state.auth)
const metadataState = useSelector((state: State) => state.metadata) const metadataState = useAppSelector((state) => state.metadata)
const userRobotImage = useSelector((state: State) => state.userRobotImage) const userRobotImage = useAppSelector((state) => state.userRobotImage)
useEffect(() => { useEffect(() => {
if (metadataState) { if (metadataState) {
@ -94,28 +79,7 @@ export const AppBar = () => {
const handleLogout = () => { const handleLogout = () => {
handleCloseUserMenu() handleCloseUserMenu()
dispatch( logout()
setAuthState({
keyPair: undefined,
loggedIn: false,
usersPubkey: undefined,
loginMethod: undefined,
nsecBunkerPubkey: undefined
})
)
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
dispatch(setUserRobotImage(null))
// clear authToken saved in local storage
clearAuthToken()
dispatch(userLogOutAction())
// update nsecBunker delegated key after logout
const nostrController = NostrController.getInstance()
const newDelegatedKey = nostrController.generateDelegatedKey()
saveNsecBunkerDelegatedKey(newDelegatedKey)
navigate('/') navigate('/')
} }
const isAuthenticated = authState?.loggedIn === true const isAuthenticated = authState?.loggedIn === true
@ -133,7 +97,7 @@ export const AppBar = () => {
<Container> <Container>
<div className={styles.bannerInner}> <div className={styles.bannerInner}>
<p className={styles.bannerText}> <p className={styles.bannerText}>
SIGit is currently Alpha software (available for internal SIGit is currently Beta software (available for user experience
testing), use at your own risk! testing), use at your own risk!
</p> </p>
<Button <Button
@ -165,7 +129,7 @@ export const AppBar = () => {
<Button <Button
startIcon={<ButtonIcon />} startIcon={<ButtonIcon />}
onClick={() => { onClick={() => {
navigate(appPublicRoutes.nostr) launchNostrLoginDialog()
}} }}
variant="contained" variant="contained"
> >

View File

@ -22,11 +22,16 @@ import { useSigitMeta } from '../../hooks/useSigitMeta'
import { extractFileExtensions } from '../../utils/file' import { extractFileExtensions } from '../../utils/file'
type SigitProps = { type SigitProps = {
sigitCreateId: string
meta: Meta meta: Meta
parsedMeta: SigitCardDisplayInfo parsedMeta: SigitCardDisplayInfo
} }
export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { export const DisplaySigit = ({
meta,
parsedMeta,
sigitCreateId: sigitCreateId
}: SigitProps) => {
const { title, createdAt, submittedBy, signers, signedStatus, isValid } = const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
parsedMeta parsedMeta
@ -35,15 +40,19 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
return ( return (
<div className={styles.itemWrapper}> <div className={styles.itemWrapper}>
{signedStatus === SigitStatus.Complete && (
<Link <Link
to={ to={appPublicRoutes.verify}
signedStatus === SigitStatus.Complete
? appPublicRoutes.verify
: appPrivateRoutes.sign
}
state={{ meta }} state={{ meta }}
className={styles.insetLink} className={styles.insetLink}
></Link> ></Link>
)}
{signedStatus !== SigitStatus.Complete && (
<Link
to={`${appPrivateRoutes.sign}/${sigitCreateId}`}
className={styles.insetLink}
></Link>
)}
<p className={`line-clamp-2 ${styles.title}`}>{title}</p> <p className={`line-clamp-2 ${styles.title}`}>{title}</p>
<div className={styles.users}> <div className={styles.users}>
{submittedBy && ( {submittedBy && (

View File

@ -22,7 +22,7 @@
.drawingRectangle { .drawingRectangle {
position: absolute; position: absolute;
outline: 1px solid #01aaad; outline: 1px solid #01aaad;
z-index: 50; z-index: 40;
background-color: #01aaad4b; background-color: #01aaad4b;
cursor: pointer; cursor: pointer;
display: flex; display: flex;

View File

@ -121,7 +121,7 @@ export const Footer = () =>
</Container> </Container>
<div className={`${styles.borderTop} ${styles.credits}`}> <div className={`${styles.borderTop} ${styles.credits}`}>
Built by&nbsp; Built by&nbsp;
<a href="https://nostrdev.com/" target="_blank"> <a rel="noopener" href="https://nostrdev.com/" target="_blank">
Nostr Dev Nostr Dev
</a>{' '} </a>{' '}
2024. 2024.

View File

@ -7,7 +7,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
z-index: 9999; z-index: 50;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }

View File

@ -1,9 +1,8 @@
import { CurrentUserMark } from '../../types/mark.ts' import { CurrentUserMark } from '../../types/mark.ts'
import styles from './style.module.scss' import styles from './style.module.scss'
import { MARK_TYPE_TRANSLATION, NEXT, SIGN } from '../../utils/const.ts'
import { import {
findNextIncompleteCurrentUserMark, findNextIncompleteCurrentUserMark,
getToolboxLabelByMarkType,
isCurrentUserMarksComplete, isCurrentUserMarksComplete,
isCurrentValueLast isCurrentValueLast
} from '../../utils' } from '../../utils'
@ -32,7 +31,6 @@ const MarkFormField = ({
handleCurrentUserMarkChange handleCurrentUserMarkChange
}: MarkFormFieldProps) => { }: MarkFormFieldProps) => {
const [displayActions, setDisplayActions] = useState(true) const [displayActions, setDisplayActions] = useState(true)
const getSubmitButtonText = () => (isReadyToSign() ? SIGN : NEXT)
const isReadyToSign = () => const isReadyToSign = () =>
isCurrentUserMarksComplete(currentUserMarks) || isCurrentUserMarksComplete(currentUserMarks) ||
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue) isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
@ -54,6 +52,7 @@ const MarkFormField = ({
: handleCurrentUserMarkChange(findNext()!) : handleCurrentUserMarkChange(findNext()!)
} }
const toggleActions = () => setDisplayActions(!displayActions) const toggleActions = () => setDisplayActions(!displayActions)
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.trigger}> <div className={styles.trigger}>
@ -61,6 +60,7 @@ const MarkFormField = ({
onClick={toggleActions} onClick={toggleActions}
className={styles.triggerBtn} className={styles.triggerBtn}
type="button" type="button"
title="Toggle"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -78,22 +78,20 @@ const MarkFormField = ({
<div className={styles.actionsWrapper}> <div className={styles.actionsWrapper}>
<div className={styles.actionsTop}> <div className={styles.actionsTop}>
<div className={styles.actionsTopInfo}> <div className={styles.actionsTopInfo}>
<p className={styles.actionsTopInfoText}>Add your signature</p> <p className={styles.actionsTopInfoText}>Add {markLabel}</p>
</div> </div>
</div> </div>
<div className={styles.inputWrapper}> <div className={styles.inputWrapper}>
<form onSubmit={(e) => handleFormSubmit(e)}> <form onSubmit={(e) => handleFormSubmit(e)}>
<input <input
className={styles.input} className={styles.input}
placeholder={ placeholder={markLabel}
MARK_TYPE_TRANSLATION[selectedMark.mark.type.valueOf()]
}
onChange={handleSelectedMarkValueChange} onChange={handleSelectedMarkValueChange}
value={selectedMarkValue} value={selectedMarkValue}
/> />
<div className={styles.actionsBottom}> <div className={styles.actionsBottom}>
<button type="submit" className={styles.submitButton}> <button type="submit" className={styles.submitButton}>
{getSubmitButtonText()} NEXT
</button> </button>
</div> </div>
</form> </form>

View File

@ -15,7 +15,7 @@
left: 5px; left: 5px;
align-items: center; align-items: center;
z-index: 1000; z-index: 40;
button { button {
transition: ease 0.2s; transition: ease 0.2s;

View File

@ -6,7 +6,7 @@
.otherUserMarksDisplay { .otherUserMarksDisplay {
position: absolute; position: absolute;
z-index: 50; z-index: 40;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -4,6 +4,7 @@ import {
fromUnixTimestamp, fromUnixTimestamp,
hexToNpub, hexToNpub,
npubToHex, npubToHex,
SigitStatus,
SignStatus SignStatus
} from '../../utils' } from '../../utils'
import { useSigitMeta } from '../../hooks/useSigitMeta' import { useSigitMeta } from '../../hooks/useSigitMeta'
@ -15,15 +16,16 @@ import {
faCalendar, faCalendar,
faCalendarCheck, faCalendarCheck,
faCalendarPlus, faCalendarPlus,
faCheck,
faClock,
faEye, faEye,
faFile, faFile,
faFileCircleExclamation faFileCircleExclamation
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { getExtensionIconLabel } from '../getExtensionIconLabel' import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSelector } from 'react-redux' import { useAppSelector } from '../../hooks/store'
import { State } from '../../store/rootReducer'
import { DisplaySigner } from '../DisplaySigner' import { DisplaySigner } from '../DisplaySigner'
import { Meta } from '../../types' import { Meta, OpenTimestamp } from '../../types'
import { extractFileExtensions } from '../../utils/file' import { extractFileExtensions } from '../../utils/file'
import { UserAvatar } from '../UserAvatar' import { UserAvatar } from '../UserAvatar'
@ -43,15 +45,61 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
completedAt, completedAt,
parsedSignatureEvents, parsedSignatureEvents,
signedStatus, signedStatus,
isValid isValid,
id,
timestamps
} = useSigitMeta(meta) } = useSigitMeta(meta)
const { usersPubkey } = useSelector((state: State) => state.auth) const { usersPubkey } = useAppSelector((state) => state.auth)
const userCanSign = const userCanSign =
typeof usersPubkey !== 'undefined' && typeof usersPubkey !== 'undefined' &&
signers.includes(hexToNpub(usersPubkey)) signers.includes(hexToNpub(usersPubkey))
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
const isTimestampVerified = (
timestamps: OpenTimestamp[],
nostrId: string
): boolean => {
const matched = timestamps.find((t) => t.nostrId === nostrId)
return !!(matched && matched.verification)
}
const getOpenTimestampsInfo = (
timestamps: OpenTimestamp[],
nostrId: string
) => {
if (isTimestampVerified(timestamps, nostrId)) {
return <FontAwesomeIcon icon={faCheck} />
} else {
return <FontAwesomeIcon icon={faClock} />
}
}
const getCompletedOpenTimestampsInfo = (timestamp: OpenTimestamp) => {
if (timestamp.verification) {
return <FontAwesomeIcon icon={faCheck} />
} else {
return <FontAwesomeIcon icon={faClock} />
}
}
const getTimestampTooltipTitle = (label: string, isVerified: boolean) => {
return `${label} / Open Timestamp ${isVerified ? 'Verified' : 'Pending'}`
}
const isUserSignatureTimestampVerified = () => {
if (
userCanSign &&
hexToNpub(usersPubkey) in parsedSignatureEvents &&
timestamps &&
timestamps.length > 0
) {
const nostrId = parsedSignatureEvents[hexToNpub(usersPubkey)].id
return isTimestampVerified(timestamps, nostrId)
}
return false
}
return submittedBy ? ( return submittedBy ? (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.section}> <div className={styles.section}>
@ -116,19 +164,36 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<p>Details</p> <p>Details</p>
<Tooltip <Tooltip
title={'Publication date'} title={getTimestampTooltipTitle(
'Publication date',
!!(timestamps && id && isTimestampVerified(timestamps, id))
)}
placement="top" placement="top"
arrow arrow
disableInteractive disableInteractive
> >
<span className={styles.detailsItem}> <span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarPlus} />{' '} <FontAwesomeIcon icon={faCalendarPlus} />{' '}
{createdAt ? formatTimestamp(createdAt) : <>&mdash;</>} {createdAt ? formatTimestamp(createdAt) : <>&mdash;</>}{' '}
{timestamps && timestamps.length > 0 && id && (
<span className={styles.ticket}>
{getOpenTimestampsInfo(timestamps, id)}
</span>
)}
</span> </span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={'Completion date'} title={getTimestampTooltipTitle(
'Completion date',
!!(
signedStatus === SigitStatus.Complete &&
completedAt &&
timestamps &&
timestamps.length > 0 &&
timestamps[timestamps.length - 1].verification
)
)}
placement="top" placement="top"
arrow arrow
disableInteractive disableInteractive
@ -136,13 +201,26 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<span className={styles.detailsItem}> <span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarCheck} />{' '} <FontAwesomeIcon icon={faCalendarCheck} />{' '}
{completedAt ? formatTimestamp(completedAt) : <>&mdash;</>} {completedAt ? formatTimestamp(completedAt) : <>&mdash;</>}
{signedStatus === SigitStatus.Complete &&
completedAt &&
timestamps &&
timestamps.length > 0 && (
<span className={styles.ticket}>
{getCompletedOpenTimestampsInfo(
timestamps[timestamps.length - 1]
)}
</span>
)}
</span> </span>
</Tooltip> </Tooltip>
{/* User signed date */} {/* User signed date */}
{userCanSign ? ( {userCanSign ? (
<Tooltip <Tooltip
title={'Your signature date'} title={getTimestampTooltipTitle(
'Your signature date',
isUserSignatureTimestampVerified()
)}
placement="top" placement="top"
arrow arrow
disableInteractive disableInteractive
@ -162,6 +240,16 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
) : ( ) : (
<>&mdash;</> <>&mdash;</>
)} )}
{hexToNpub(usersPubkey) in parsedSignatureEvents &&
timestamps &&
timestamps.length > 0 && (
<span className={styles.ticket}>
{getOpenTimestampsInfo(
timestamps,
parsedSignatureEvents[hexToNpub(usersPubkey)].id
)}
</span>
)}
</span> </span>
</Tooltip> </Tooltip>
) : null} ) : null}

View File

@ -31,8 +31,6 @@
padding: 5px; padding: 5px;
display: flex; display: flex;
align-items: center;
justify-content: start;
> :first-child { > :first-child {
padding: 5px; padding: 5px;
@ -44,3 +42,7 @@
color: white; color: white;
} }
} }
.ticket {
margin-left: auto;
}

View File

@ -1,6 +1,5 @@
import { Typography } from '@mui/material' import { Typography } from '@mui/material'
import { useSelector } from 'react-redux' import { useAppSelector } from '../hooks/store'
import { State } from '../store/rootReducer'
import styles from './username.module.scss' import styles from './username.module.scss'
import { AvatarIconButton } from './UserAvatarIconButton' import { AvatarIconButton } from './UserAvatarIconButton'
@ -16,7 +15,7 @@ type Props = {
* Clicking will open the menu. * Clicking will open the menu.
*/ */
const Username = ({ username, avatarContent, handleClick }: Props) => { const Username = ({ username, avatarContent, handleClick }: Props) => {
const hexKey = useSelector((state: State) => state.auth.usersPubkey) const hexKey = useAppSelector((state) => state.auth.usersPubkey)
return ( return (
<div className={styles.container}> <div className={styles.container}>

View File

@ -31,7 +31,7 @@ export class AuthController {
/** /**
* Function will authenticate user by signing an auth event * Function will authenticate user by signing an auth event
* which is done by calling the sign() function, where appropriate * which is done by calling the sign() function, where appropriate
* method will be chosen (extension, nsecbunker or keys) * method will be chosen (extension or keys)
* *
* @param pubkey of the user trying to login * @param pubkey of the user trying to login
* @returns url to redirect if authentication successfull * @returns url to redirect if authentication successfull
@ -57,12 +57,15 @@ export class AuthController {
// Nostr uses unix timestamps // Nostr uses unix timestamps
const timestamp = unixNow() const timestamp = unixNow()
const { hostname } = window.location const { href } = window.location
const authEvent: EventTemplate = { const authEvent: EventTemplate = {
kind: 27235, kind: 27235,
tags: [], tags: [
content: `${hostname}-${timestamp}`, ['u', href],
['method', 'GET']
],
content: '',
created_at: timestamp created_at: timestamp
} }
@ -83,7 +86,7 @@ export class AuthController {
return Promise.resolve(appPrivateRoutes.relays) return Promise.resolve(appPrivateRoutes.relays)
} }
if (store.getState().auth?.loggedIn) { if (store.getState().auth.loggedIn) {
if (!compareObjects(store.getState().relays?.map, relayMap.map)) if (!compareObjects(store.getState().relays?.map, relayMap.map))
store.dispatch(setRelayMapAction(relayMap.map)) store.dispatch(setRelayMapAction(relayMap.map))
} }

View File

@ -1,194 +1,24 @@
import NDK, { import { EventTemplate, UnsignedEvent } from 'nostr-tools'
NDKEvent, import { WindowNostr } from 'nostr-tools/nip07'
NDKNip46Signer,
NDKPrivateKeySigner,
NDKUser,
NostrEvent
} from '@nostr-dev-kit/ndk'
import {
Event,
EventTemplate,
UnsignedEvent,
finalizeEvent,
nip04,
nip19,
nip44
} from 'nostr-tools'
import { EventEmitter } from 'tseep' import { EventEmitter } from 'tseep'
import { updateNsecbunkerPubkey } from '../store/actions'
import { AuthState, LoginMethods } from '../store/auth/types'
import store from '../store/store' import store from '../store/store'
import { SignedEvent } from '../types' import { SignedEvent } from '../types'
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils' import { LoginMethodContext } from '../services/LoginMethodStrategy/loginMethodContext'
export class NostrController extends EventEmitter { export class NostrController extends EventEmitter {
private static instance: NostrController private static instance: NostrController
private bunkerNDK: NDK | undefined
private remoteSigner: NDKNip46Signer | undefined
private constructor() { private constructor() {
super() super()
} }
private getNostrObject = () => { private getNostrObject = () => {
// fix: this is not picking up type declaration from src/system/index.d.ts if (window.nostr) return window.nostr as WindowNostr
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (window.nostr) return window.nostr as any
throw new Error( throw new Error(
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.` `window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
) )
} }
public nsecBunkerInit = async (relays: string[]) => {
// Don't reinstantiate bunker NDK if exists with same relays
if (
this.bunkerNDK &&
this.bunkerNDK.explicitRelayUrls?.length === relays.length &&
this.bunkerNDK.explicitRelayUrls?.every((relay) => relays.includes(relay))
)
return
this.bunkerNDK = new NDK({
explicitRelayUrls: relays
})
try {
await this.bunkerNDK
.connect(2000)
.then(() => {
console.log(
`Successfully connected to the nsecBunker relays: ${relays.join(
','
)}`
)
})
.catch((err) => {
console.error(
`Error connecting to the nsecBunker relays: ${relays.join(
','
)} ${err}`
)
})
} catch (err) {
console.error(err)
}
}
/**
* Creates nSecBunker signer instance for the given npub
* Or if npub omitted it will return existing signer
* If neither, error will be thrown
* @param npub nPub / public key in hex format
* @returns nsecBunker Signer instance
*/
public createNsecBunkerSigner = async (
npub: string | undefined
): Promise<NDKNip46Signer> => {
const nsecBunkerDelegatedKey = getNsecBunkerDelegatedKey()
return new Promise((resolve, reject) => {
if (!nsecBunkerDelegatedKey) {
reject('nsecBunker delegated key is not found in the browser.')
return
}
const localSigner = new NDKPrivateKeySigner(nsecBunkerDelegatedKey)
if (!npub) {
if (this.remoteSigner) resolve(this.remoteSigner)
const npubFromStorage = (store.getState().auth as AuthState)
.nsecBunkerPubkey
if (npubFromStorage) {
npub = npubFromStorage
} else {
reject(
'No signer instance present, no npub provided by user or found in the browser.'
)
return
}
} else {
store.dispatch(updateNsecbunkerPubkey(npub))
}
// Pubkey of a key pair stored in nsecbunker that will be used to sign event with
const appPubkeyOrToken = npub.includes('npub')
? npub
: nip19.npubEncode(npub)
/**
* When creating and NDK instance we create new connection to the relay
* To prevent too much connections and hitting rate limits, if npub against which we sign
* we will reuse existing instance. Otherwise we will create new NDK and signer instance.
*/
if (!this.remoteSigner || this.remoteSigner?.remotePubkey !== npub) {
this.remoteSigner = new NDKNip46Signer(
this.bunkerNDK!,
appPubkeyOrToken,
localSigner
)
}
/**
* when nsecbunker-delegated-key is regenerated we have to reinitialize the remote signer
*/
if (this.remoteSigner.localSigner !== localSigner) {
this.remoteSigner = new NDKNip46Signer(
this.bunkerNDK!,
appPubkeyOrToken,
localSigner
)
}
resolve(this.remoteSigner)
})
}
/**
* Signs the nostr event and returns the sig and id or full raw nostr event
* @param npub stored in nsecBunker to sign with
* @param event to be signed
* @param returnFullEvent whether to return full raw nostr event or just SIG and ID values
*/
public signWithNsecBunker = async (
npub: string | undefined,
event: NostrEvent,
returnFullEvent = true
): Promise<{ id: string; sig: string } | NostrEvent> => {
return new Promise((resolve, reject) => {
this.createNsecBunkerSigner(npub)
.then(async (signer) => {
const ndkEvent = new NDKEvent(undefined, event)
const timeout = setTimeout(() => {
reject('Timeout occurred while waiting for event signing')
}, 60000) // 60000 ms (1 min) = 1000 * 60
await ndkEvent.sign(signer).catch((err) => {
clearTimeout(timeout)
reject(err)
return
})
clearTimeout(timeout)
if (returnFullEvent) {
resolve(ndkEvent.rawEvent())
} else {
resolve({
id: ndkEvent.id,
sig: ndkEvent.sig!
})
}
})
.catch((err) => {
reject(err)
})
})
}
public static getInstance(): NostrController { public static getInstance(): NostrController {
if (!NostrController.instance) { if (!NostrController.instance) {
NostrController.instance = new NostrController() NostrController.instance = new NostrController()
@ -206,60 +36,11 @@ export class NostrController extends EventEmitter {
*/ */
nip44Encrypt = async (receiver: string, content: string) => { nip44Encrypt = async (receiver: string, content: string) => {
// Retrieve the current login method from the application's redux state. // Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
// Handle encryption when the login method is via an extension. // Handle encryption when the login method is via an extension.
if (loginMethod === LoginMethods.extension) { return await context.nip44Encrypt(receiver, content)
const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 encryption.
if (!nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Encrypt the content using NIP-44 provided by the nostr extension.
const encrypted = await nostr.nip44.encrypt(receiver, content)
return encrypted as string
}
// Handle encryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
receiver
)
// Encrypt the content using the generated conversation key.
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
return encrypted
}
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) {
throw new Error(
`nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
)
}
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined')
} }
/** /**
@ -272,180 +53,33 @@ export class NostrController extends EventEmitter {
*/ */
nip44Decrypt = async (sender: string, content: string) => { nip44Decrypt = async (sender: string, content: string) => {
// Retrieve the current login method from the application's redux state. // Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
// Handle decryption when the login method is via an extension. // Handle decryption
if (loginMethod === LoginMethods.extension) { return await context.nip44Decrypt(sender, content)
const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 decryption.
if (!nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Decrypt the content using NIP-44 provided by the nostr extension.
const decrypted = await nostr.nip44.decrypt(sender, content)
return decrypted as string
}
// Handle decryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
sender
)
// Decrypt the content using the generated conversation key.
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
return decrypted
}
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) {
throw new Error(
`nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
)
}
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined')
} }
/** /**
* Signs an event with private key (if it is present in local storage) or * Signs an event with private key (if it is present in local storage) or
* with browser extension (if it is present) or * with browser extension (if it is present)
* with nSecBunker instance.
* @param event - unsigned nostr event. * @param event - unsigned nostr event.
* @returns - a promised that is resolved with signed nostr event. * @returns - a promised that is resolved with signed nostr event.
*/ */
signEvent = async ( signEvent = async (
event: UnsignedEvent | EventTemplate event: UnsignedEvent | EventTemplate
): Promise<SignedEvent> => { ): Promise<SignedEvent> => {
const loginMethod = (store.getState().auth as AuthState).loginMethod const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
if (!loginMethod) { return await context.signEvent(event)
return Promise.reject('No login method found in the browser storage')
}
if (loginMethod === LoginMethods.nsecBunker) {
// Check if nsecBunker is available
if (!this.bunkerNDK) {
return Promise.reject(
`Login method is ${loginMethod} but bunkerNDK is not created`
)
}
if (!this.remoteSigner) {
return Promise.reject(
`Login method is ${loginMethod} but bunkerNDK is not created`
)
}
const signedEvent = await this.signWithNsecBunker(
'',
event as NostrEvent
).catch((err) => {
throw err
})
return Promise.resolve(signedEvent as SignedEvent)
} else if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
return Promise.reject(
`Login method is ${loginMethod}, but keys are not found`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const signedEvent = finalizeEvent(event, privateKey)
verifySignedEvent(signedEvent)
return Promise.resolve(signedEvent)
} else if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
return (await nostr
.signEvent(event as NostrEvent)
.catch((err: unknown) => {
console.log('Error while signing event: ', err)
throw err
})) as Event
} else {
return Promise.reject(
`We could not sign the event, none of the signing methods are available`
)
}
} }
nip04Encrypt = async (receiver: string, content: string): Promise<string> => { nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
const loginMethod = (store.getState().auth as AuthState).loginMethod const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
if (loginMethod === LoginMethods.extension) { return await context.nip04Encrypt(receiver, content)
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 keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const encrypted = await nip04.encrypt(privateKey, 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')
} }
/** /**
@ -456,51 +90,10 @@ export class NostrController extends EventEmitter {
* @returns A promise that resolves to the decrypted content. * @returns A promise that resolves to the decrypted content.
*/ */
nip04Decrypt = async (sender: string, content: string): Promise<string> => { nip04Decrypt = async (sender: string, content: string): Promise<string> => {
const loginMethod = (store.getState().auth as AuthState).loginMethod const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
if (loginMethod === LoginMethods.extension) { return await context.nip04Decrypt(sender, content)
const nostr = this.getNostrObject()
if (!nostr.nip04) {
throw new Error(
`Your nostr extension does not support nip04 encryption & decryption`
)
}
const decrypted = await nostr.nip04.decrypt(sender, content)
return decrypted
}
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const decrypted = await nip04.decrypt(privateKey, sender, content)
return decrypted
}
if (loginMethod === LoginMethods.nsecBunker) {
const user = new NDKUser({ pubkey: sender })
this.remoteSigner?.on('authUrl', (authUrl) => {
this.emit('nsecbunker-auth', authUrl)
})
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
const decrypted = await this.remoteSigner.decrypt(user, content)
return decrypted
}
throw new Error('Login method is undefined')
} }
/** /**
@ -523,12 +116,4 @@ export class NostrController extends EventEmitter {
return Promise.resolve(pubKey) return Promise.resolve(pubKey)
} }
/**
* Generates NDK Private Signer
* @returns nSecBunker delegated key
*/
generateDelegatedKey = (): string => {
return NDKPrivateKeySigner.generate().privateKey!
}
} }

26
src/hooks/useLogout.tsx Normal file
View File

@ -0,0 +1,26 @@
import { logout as nostrLogout } from 'nostr-login'
import { clear } from '../utils/localStorage'
import { userLogOutAction } from '../store/actions'
import { LoginMethod } from '../store/auth/types'
import { useAppDispatch, useAppSelector } from './store'
import { useCallback } from 'react'
export const useLogout = () => {
const loginMethod = useAppSelector((state) => state.auth?.loginMethod)
const dispatch = useAppDispatch()
const logout = useCallback(() => {
// Log out of the nostr-login
if (loginMethod === LoginMethod.nostrLogin) {
nostrLogout()
}
// Reset redux state with the logout
dispatch(userLogOutAction())
// Clear the local storage states
clear()
}, [dispatch, loginMethod])
return logout
}

View File

@ -19,7 +19,6 @@ import { toast } from 'react-toastify'
import { verifyEvent } from 'nostr-tools' import { verifyEvent } from 'nostr-tools'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import store from '../store/store' import store from '../store/store'
import { AuthState } from '../store/auth/types'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
import { MetaParseError } from '../types/errors/MetaParseError' import { MetaParseError } from '../types/errors/MetaParseError'
@ -146,7 +145,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
if (meta.keys) { if (meta.keys) {
const { sender, keys } = meta.keys const { sender, keys } = meta.keys
// Retrieve the user's public key from the state // Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = store.getState().auth.usersPubkey!
const usersNpub = hexToNpub(usersPubkey) const usersNpub = hexToNpub(usersPubkey)
// Check if the user's public key is in the keys object // Check if the user's public key is in the keys object

View File

@ -27,7 +27,7 @@ body {
position: fixed; position: fixed;
top: 80px; top: 80px;
right: 20px; right: 20px;
z-index: 100; z-index: 40;
} }
#root { #root {

View File

@ -1,87 +1,160 @@
import { Event, kinds } from 'nostr-tools' import { Event, getPublicKey, kinds, nip19 } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
import { Outlet } from 'react-router-dom'
import { AppBar } from '../components/AppBar/AppBar' import { AppBar } from '../components/AppBar/AppBar'
import { LoadingSpinner } from '../components/LoadingSpinner' import { LoadingSpinner } from '../components/LoadingSpinner'
import { MetadataController, NostrController } from '../controllers' import {
AuthController,
MetadataController,
NostrController
} from '../controllers'
import { import {
restoreState, restoreState,
setAuthState,
setMetadataEvent, setMetadataEvent,
updateKeyPair,
updateLoginMethod,
updateNostrLoginAuthMethod,
updateUserAppData updateUserAppData
} from '../store/actions' } from '../store/actions'
import { LoginMethods } from '../store/auth/types'
import { State } from '../store/rootReducer'
import { Dispatch } from '../store/store'
import { setUserRobotImage } from '../store/userRobotImage/action' import { setUserRobotImage } from '../store/userRobotImage/action'
import { import {
clearAuthToken,
clearState,
getRoboHashPicture, getRoboHashPicture,
getUsersAppData, getUsersAppData,
loadState, loadState,
saveNsecBunkerDelegatedKey,
subscribeForSigits subscribeForSigits
} from '../utils' } from '../utils'
import { useAppSelector } from '../hooks' import { useAppDispatch, useAppSelector } from '../hooks'
import styles from './style.module.scss' import styles from './style.module.scss'
import { useLogout } from '../hooks/useLogout'
import { LoginMethod } from '../store/auth/types'
import { NostrLoginAuthOptions } from 'nostr-login/dist/types'
import { init as initNostrLogin } from 'nostr-login'
export const MainLayout = () => { export const MainLayout = () => {
const dispatch: Dispatch = useDispatch() const [searchParams, setSearchParams] = useSearchParams()
const navigate = useNavigate()
const dispatch = useAppDispatch()
const logout = useLogout()
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
const authState = useSelector((state: State) => state.auth) const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
const authState = useAppSelector((state) => state.auth)
const usersAppData = useAppSelector((state) => state.userAppData) const usersAppData = useAppSelector((state) => state.userAppData)
// Ref to track if `subscribeForSigits` has been called // Ref to track if `subscribeForSigits` has been called
const hasSubscribed = useRef(false) const hasSubscribed = useRef(false)
useEffect(() => { const navigateAfterLogin = (path: string) => {
const metadataController = MetadataController.getInstance() const callbackPath = searchParams.get('callbackPath')
if (callbackPath) {
// base64 decoded path
const path = atob(callbackPath)
navigate(path)
return
}
navigate(path)
}
const login = useCallback(async () => {
const nostrController = NostrController.getInstance()
const authController = new AuthController()
const pubkey = await nostrController.capturePublicKey()
dispatch(updateLoginMethod(LoginMethod.nostrLogin))
const redirectPath =
await authController.authAndGetMetadataAndRelaysMap(pubkey)
if (redirectPath) {
navigateAfterLogin(redirectPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch])
useEffect(() => {
// Developer login with ?nsec= (not recommended)
const nsec = searchParams.get('nsec')
if (!nsec) return
// Clear nsec from the url immediately
searchParams.delete('nsec')
setSearchParams(searchParams)
if (!authState?.loggedIn) {
if (!nsec.startsWith('nsec')) {
console.error('Invalid format, use private key (nsec)')
return
}
try {
const privateKey = nip19.decode(nsec).data as Uint8Array
if (!privateKey) {
console.error('Failed to convert the private key.')
return
}
const publickey = getPublicKey(privateKey)
const logout = () => {
dispatch( dispatch(
setAuthState({ updateKeyPair({
keyPair: undefined, private: nsec,
loggedIn: false, public: publickey
usersPubkey: undefined,
loginMethod: undefined,
nsecBunkerPubkey: undefined
}) })
) )
dispatch(updateLoginMethod(LoginMethod.privateKey))
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent())) const authController = new AuthController()
authController
// clear authToken saved in local storage .authAndGetMetadataAndRelaysMap(publickey)
clearAuthToken() .catch((err) => {
clearState() console.error('Error occurred in authentication: ' + err)
return null
// update nsecBunker delegated key })
const newDelegatedKey = } catch (err) {
NostrController.getInstance().generateDelegatedKey() console.error(`Error decoding the nsec. ${err}`)
saveNsecBunkerDelegatedKey(newDelegatedKey)
} }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, searchParams])
useEffect(() => {
const handleNostrAuth = (_: string, opts: NostrLoginAuthOptions) => {
if (opts.type === 'login' || opts.type === 'signup') {
dispatch(updateNostrLoginAuthMethod(opts.method))
login()
} else if (opts.type === 'logout') {
// Clear `subscribeForSigits` as called after the logout
hasSubscribed.current = false
}
}
// Initialize the nostr-login
initNostrLogin({
methods: ['connect', 'extension', 'local'],
noBanner: true,
onAuth: handleNostrAuth
}).catch((error) => {
console.error('Failed to initialize Nostr-Login', error)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch])
useEffect(() => {
const metadataController = MetadataController.getInstance()
const restoredState = loadState() const restoredState = loadState()
if (restoredState) { if (restoredState) {
dispatch(restoreState(restoredState)) dispatch(restoreState(restoredState))
const { loggedIn, loginMethod, usersPubkey, nsecBunkerRelays } = const { loggedIn, loginMethod, usersPubkey } = restoredState.auth
restoredState.auth
if (loggedIn) { if (loggedIn) {
if (!loginMethod || !usersPubkey) return logout() if (!loginMethod || !usersPubkey) return logout()
if (loginMethod === LoginMethods.nsecBunker) { // Update user profile metadata, old state might be outdated
if (!nsecBunkerRelays) return logout()
const nostrController = NostrController.getInstance()
nostrController.nsecBunkerInit(nsecBunkerRelays).then(() => {
nostrController.createNsecBunkerSigner(usersPubkey)
})
}
const handleMetadataEvent = (event: Event) => { const handleMetadataEvent = (event: Event) => {
dispatch(setMetadataEvent(event)) dispatch(setMetadataEvent(event))
} }
@ -101,10 +174,14 @@ export const MainLayout = () => {
} else { } else {
setIsLoading(false) setIsLoading(false)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch]) }, [dispatch])
/**
* Subscribe for the sigits
*/
useEffect(() => { useEffect(() => {
if (authState.loggedIn && usersAppData) { if (authState && isLoggedIn && usersAppData) {
const pubkey = authState.usersPubkey || authState.keyPair?.public const pubkey = authState.usersPubkey || authState.keyPair?.public
if (pubkey && !hasSubscribed.current) { if (pubkey && !hasSubscribed.current) {
@ -116,7 +193,7 @@ export const MainLayout = () => {
hasSubscribed.current = true hasSubscribed.current = true
} }
} }
}, [authState, usersAppData]) }, [authState, isLoggedIn, usersAppData])
/** /**
* When authState change user logged in / or app reloaded * When authState change user logged in / or app reloaded
@ -124,7 +201,7 @@ export const MainLayout = () => {
* so that avatar will be consistent across the app when kind 0 is empty * so that avatar will be consistent across the app when kind 0 is empty
*/ */
useEffect(() => { useEffect(() => {
if (authState && authState.loggedIn) { if (authState && isLoggedIn) {
const pubkey = authState.usersPubkey || authState.keyPair?.public const pubkey = authState.usersPubkey || authState.keyPair?.public
if (pubkey) { if (pubkey) {
@ -141,7 +218,8 @@ export const MainLayout = () => {
}) })
.finally(() => setIsLoading(false)) .finally(() => setIsLoading(false))
} }
}, [authState, dispatch]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, isLoggedIn])
if (isLoading) return <LoadingSpinner desc={loadingSpinnerDesc} /> if (isLoading) return <LoadingSpinner desc={loadingSpinnerDesc} />

View File

@ -33,7 +33,7 @@ export const Modal = () => {
{ to: appPublicRoutes.register, title: 'Register', label: 'Register' }, { to: appPublicRoutes.register, title: 'Register', label: 'Register' },
{ {
to: appPublicRoutes.nostr, to: appPublicRoutes.nostr,
title: 'Login', title: 'Nostr Login',
sx: { padding: '10px' }, sx: { padding: '10px' },
label: <img src={nostrImage} width="25" alt="nostr logo" height="25" /> label: <img src={nostrImage} width="25" alt="nostr logo" height="25" />
} }

View File

@ -8,14 +8,13 @@ import { useEffect, useRef, useState } from 'react'
import { DndProvider, useDrag, useDrop } from 'react-dnd' import { DndProvider, useDrag, useDrop } from 'react-dnd'
import { MultiBackend } from 'react-dnd-multi-backend' import { MultiBackend } from 'react-dnd-multi-backend'
import { HTML5toTouch } from 'rdndmb-html5-to-touch' import { HTML5toTouch } from 'rdndmb-html5-to-touch'
import { useSelector } from 'react-redux' import { useAppSelector } from '../../hooks/store'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { UserAvatar } from '../../components/UserAvatar' import { UserAvatar } from '../../components/UserAvatar'
import { MetadataController, NostrController } from '../../controllers' import { MetadataController, NostrController } from '../../controllers'
import { appPrivateRoutes } from '../../routes' import { appPrivateRoutes } from '../../routes'
import { State } from '../../store/rootReducer'
import { import {
CreateSignatureEventContent, CreateSignatureEventContent,
Meta, Meta,
@ -76,8 +75,6 @@ export const CreatePage = () => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [authUrl, setAuthUrl] = useState<string>()
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`) const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
const [selectedFiles, setSelectedFiles] = useState<File[]>([]) const [selectedFiles, setSelectedFiles] = useState<File[]>([])
@ -102,7 +99,7 @@ export const CreatePage = () => {
const signers = users.filter((u) => u.role === UserRole.signer) const signers = users.filter((u) => u.role === UserRole.signer)
const viewers = users.filter((u) => u.role === UserRole.viewer) const viewers = users.filter((u) => u.role === UserRole.viewer)
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
@ -185,10 +182,6 @@ export const CreatePage = () => {
} }
}) })
}, [metadata, users]) }, [metadata, users])
// Set up event listener for authentication event
nostrController.on('nsecbunker-auth', (url) => {
setAuthUrl(url)
})
useEffect(() => { useEffect(() => {
if (uploadedFiles) { if (uploadedFiles) {
@ -255,17 +248,22 @@ export const CreatePage = () => {
const input = userInput.toLowerCase() const input = userInput.toLowerCase()
if (input.startsWith('npub')) { if (input.startsWith('npub')) {
const pubkey = npubToHex(input) return handleAddNpubUser(input)
if (pubkey) {
addUser(pubkey)
setUserInput('')
} else {
setError('Provided npub is not valid. Please enter correct npub.')
}
return
} }
if (input.includes('@')) { if (input.includes('@')) {
return await handleAddNip05User(input)
}
// If the user enters the domain (w/o @) assume it's the "root" and append _@
// https://github.com/nostr-protocol/nips/blob/master/05.md#showing-just-the-domain-as-an-identifier
if (input.includes('.')) {
return await handleAddNip05User(`_@${input}`)
}
setError('Invalid input! Make sure to provide correct npub or nip05.')
async function handleAddNip05User(input: string) {
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Querying for nip05') setLoadingSpinnerDesc('Querying for nip05')
const nip05Profile = await queryNip05(input) const nip05Profile = await queryNip05(input)
@ -288,7 +286,16 @@ export const CreatePage = () => {
return return
} }
setError('Invalid input! Make sure to provide correct npub or nip05.') function handleAddNpubUser(input: string) {
const pubkey = npubToHex(input)
if (pubkey) {
addUser(pubkey)
setUserInput('')
} else {
setError('Provided npub is not valid. Please enter correct npub.')
}
return
}
} }
const handleUserRoleChange = (role: UserRole, pubkey: string) => { const handleUserRoleChange = (role: UserRole, pubkey: string) => {
@ -778,17 +785,6 @@ export const CreatePage = () => {
} }
} }
if (authUrl) {
return (
<iframe
title="Nsecbunker auth"
src={authUrl}
width="100%"
height="500px"
/>
)
}
return ( return (
<> <>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />} {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}

View File

@ -257,6 +257,7 @@ export const HomePage = () => {
.map((key) => ( .map((key) => (
<DisplaySigit <DisplaySigit
key={`sigit-${key}`} key={`sigit-${key}`}
sigitCreateId={key}
parsedMeta={parsedSigits[key]} parsedMeta={parsedSigits[key]}
meta={sigits[key]} meta={sigits[key]}
/> />

View File

@ -1,7 +1,6 @@
import { Box, Button } from '@mui/material' import { Box, Button } from '@mui/material'
import { useEffect } from 'react' import { useEffect } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router-dom' import { Outlet, useLocation } from 'react-router-dom'
import { appPublicRoutes } from '../../routes'
import { saveVisitedLink } from '../../utils' import { saveVisitedLink } from '../../utils'
import { CardComponent } from '../../components/Landing/CardComponent/CardComponent' import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
@ -20,13 +19,13 @@ import {
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack' import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack'
import { Footer } from '../../components/Footer/Footer' import { Footer } from '../../components/Footer/Footer'
import { launch as launchNostrLoginDialog } from 'nostr-login'
export const LandingPage = () => { export const LandingPage = () => {
const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const onSignInClick = async () => { const onSignInClick = async () => {
navigate(appPublicRoutes.nostr) launchNostrLoginDialog()
} }
const cards = [ const cards = [
@ -35,7 +34,7 @@ export const LandingPage = () => {
title: <>Open Source</>, title: <>Open Source</>,
description: ( description: (
<> <>
Code is MIT licenced and available at{' '} Code is AGPL licenced and available at{' '}
<a href="https://git.nostrdev.com/sigit/sigit.io"> <a href="https://git.nostrdev.com/sigit/sigit.io">
https://git.nostrdev.com/sigit/sigit.io https://git.nostrdev.com/sigit/sigit.io
</a> </a>
@ -120,9 +119,7 @@ export const LandingPage = () => {
<Container className={styles.container}> <Container className={styles.container}>
<img className={styles.logo} src="/logo.svg" alt="Logo" width={300} /> <img className={styles.logo} src="/logo.svg" alt="Logo" width={300} />
<div className={styles.titleSection}> <div className={styles.titleSection}>
<h1 className={styles.title}> <h1 className={styles.title}>Secure &amp; Private Agreements</h1>
Secure &amp; Private Document Signing
</h1>
<p className={styles.subTitle}> <p className={styles.subTitle}>
An open-source and self-hostable solution for secure document An open-source and self-hostable solution for secure document
signing and verification. signing and verification.

View File

@ -12,6 +12,11 @@ export const Login = () => {
margin="dense" margin="dense"
autoComplete="username" autoComplete="username"
disabled disabled
sx={{
input: {
cursor: 'not-allowed'
}
}}
/> />
<TextField <TextField
label="Password" label="Password"
@ -20,6 +25,11 @@ export const Login = () => {
margin="dense" margin="dense"
autoComplete="current-password" autoComplete="current-password"
disabled disabled
sx={{
input: {
cursor: 'not-allowed'
}
}}
/> />
<Button variant="contained" fullWidth disabled> <Button variant="contained" fullWidth disabled>

View File

@ -1,48 +1,43 @@
import { launch as launchNostrLoginDialog } from 'nostr-login'
import { Button, Divider, TextField } from '@mui/material' import { Button, Divider, TextField } from '@mui/material'
import { getPublicKey, nip19 } from 'nostr-tools' import { getPublicKey, nip19 } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux' import { useAppDispatch } from '../../hooks/store'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { import { AuthController } from '../../controllers'
AuthController, import { updateKeyPair, updateLoginMethod } from '../../store/actions'
MetadataController, import { LoginMethod } from '../../store/auth/types'
NostrController
} from '../../controllers'
import {
updateKeyPair,
updateLoginMethod,
updateNsecbunkerPubkey,
updateNsecbunkerRelays
} from '../../store/actions'
import { LoginMethods } from '../../store/auth/types'
import { Dispatch } from '../../store/store'
import { npubToHex, queryNip05, timeout } from '../../utils'
import { hexToBytes } from '@noble/hashes/utils' import { hexToBytes } from '@noble/hashes/utils'
import { NIP05_REGEX } from '../../constants'
import styles from './styles.module.scss' import styles from './styles.module.scss'
import { TimeoutError } from '../../types/errors/TimeoutError'
const EXTENSION_LOGIN_DELAY_SECONDS = 5
const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 55
export const Nostr = () => { export const Nostr = () => {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const dispatch: Dispatch = useDispatch() const dispatch = useAppDispatch()
const navigate = useNavigate() const navigate = useNavigate()
const authController = new AuthController() const authController = new AuthController()
const metadataController = MetadataController.getInstance()
const nostrController = NostrController.getInstance()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [isExtensionSlow, setIsExtensionSlow] = useState(false)
const [inputValue, setInputValue] = useState('') const [inputValue, setInputValue] = useState('')
const [authUrl, setAuthUrl] = useState<string>()
const navigateAfterLogin = (path: string) => {
const callbackPath = searchParams.get('callbackPath')
if (callbackPath) {
// base64 decoded path
const path = atob(callbackPath)
navigate(path)
return
}
navigate(path)
}
const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] = const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
useState(false) useState(false)
@ -63,59 +58,6 @@ export const Nostr = () => {
} }
} }
const navigateAfterLogin = (path: string) => {
const callbackPath = searchParams.get('callbackPath')
if (callbackPath) {
// base64 decoded path
const path = atob(callbackPath)
navigate(path)
return
}
navigate(path)
}
const loginWithExtension = async () => {
let waitTimeout: number | undefined
try {
// Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message
waitTimeout = window.setTimeout(() => {
setIsExtensionSlow(true)
}, EXTENSION_LOGIN_DELAY_SECONDS * 1000)
setIsLoading(true)
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
const pubkey = await nostrController.capturePublicKey()
dispatch(updateLoginMethod(LoginMethods.extension))
setLoadingSpinnerDesc('Authenticating and finding metadata')
const redirectPath = await Promise.race([
authController.authAndGetMetadataAndRelaysMap(pubkey),
timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000)
])
if (redirectPath) {
navigateAfterLogin(redirectPath)
}
} catch (error) {
if (error instanceof TimeoutError) {
// Just log the error, no toast, user has already been notified with the loading screen
console.error("Extension didn't respond in time")
} else {
toast.error('Error capturing public key from nostr extension: ' + error)
}
} finally {
// Clear the wait timeout so we don't change the state unnecessarily
window.clearTimeout(waitTimeout)
setIsLoading(false)
setLoadingSpinnerDesc('')
setIsExtensionSlow(false)
}
}
/** /**
* Login with NSEC or HEX private key * Login with NSEC or HEX private key
* @param privateKey in HEX format * @param privateKey in HEX format
@ -151,7 +93,7 @@ export const Nostr = () => {
public: publickey public: publickey
}) })
) )
dispatch(updateLoginMethod(LoginMethods.privateKey)) dispatch(updateLoginMethod(LoginMethod.privateKey))
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Authenticating and finding metadata') setLoadingSpinnerDesc('Authenticating and finding metadata')
@ -169,182 +111,10 @@ export const Nostr = () => {
setLoadingSpinnerDesc('') setLoadingSpinnerDesc('')
} }
const loginWithNsecBunker = async () => {
let relays: string[] | undefined
let pubkey: string | undefined
setIsLoading(true)
const displayError = (message: string) => {
toast.error(message)
setIsLoading(false)
setLoadingSpinnerDesc('')
}
if (inputValue.match(NIP05_REGEX)) {
const nip05Profile = await queryNip05(inputValue).catch((err) => {
toast.error('An error occurred while querying nip05 profile: ' + err)
return null
})
if (nip05Profile) {
pubkey = nip05Profile.pubkey
relays = nip05Profile.relays
}
} else if (inputValue.startsWith('npub')) {
pubkey = nip19.decode(inputValue).data as string
const metadataEvent = await metadataController
.findMetadata(pubkey)
.catch(() => {
return null
})
if (!metadataEvent) {
return displayError('metadata not found!')
}
const metadataContent =
metadataController.extractProfileMetadataContent(metadataEvent)
if (!metadataContent?.nip05) {
return displayError('nip05 not present in metadata')
}
const nip05Profile = await queryNip05(inputValue).catch((err) => {
toast.error('An error occurred while querying nip05 profile: ' + err)
return null
})
if (nip05Profile) {
if (nip05Profile.pubkey !== pubkey) {
return displayError(
'pubkey in nip05 does not match with provided npub'
)
}
relays = nip05Profile.relays
}
}
if (!relays || relays.length === 0) {
return displayError('No relay found for nsecbunker')
}
if (!pubkey) {
return displayError('pubkey not found')
}
setLoadingSpinnerDesc('Initializing nsecBunker')
await nostrController.nsecBunkerInit(relays)
setLoadingSpinnerDesc('Creating nsecbunker singer')
await nostrController
.createNsecBunkerSigner(pubkey)
.then(async (signer) => {
signer.on('authUrl', (url: string) => {
setAuthUrl(url)
})
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
dispatch(updateNsecbunkerPubkey(pubkey))
dispatch(updateNsecbunkerRelays(relays))
setLoadingSpinnerDesc('Authenticating and finding metadata')
const redirectPath = await authController
.authAndGetMetadataAndRelaysMap(pubkey!)
.catch((err) => {
toast.error('Error occurred in authentication: ' + err)
return null
})
if (redirectPath) navigateAfterLogin(redirectPath)
})
.catch((err) => {
toast.error(
'An error occurred while creating nsecbunker signer: ' + err
)
})
.finally(() => {
setIsLoading(false)
setLoadingSpinnerDesc('')
})
}
const loginWithBunkerConnectionString = async () => {
// Extract the key
const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length
const keyEndIndex = inputValue.indexOf('?relay=')
const key = inputValue.substring(keyStartIndex, keyEndIndex)
const pubkey = npubToHex(key)
if (!pubkey) {
toast.error('Invalid pubkey in bunker connection string.')
setIsLoading(false)
return
}
// Extract the relay value
const relayIndex = inputValue.indexOf('relay=')
const relay = inputValue.substring(
relayIndex + 'relay='.length,
inputValue.length
)
setIsLoading(true)
setLoadingSpinnerDesc('Initializing bunker NDK')
await nostrController.nsecBunkerInit([relay])
setLoadingSpinnerDesc('Creating remote signer')
await nostrController
.createNsecBunkerSigner(pubkey)
.then(async (signer) => {
signer.on('authUrl', (url: string) => {
setAuthUrl(url)
})
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
dispatch(updateNsecbunkerPubkey(pubkey))
dispatch(updateNsecbunkerRelays([relay]))
setLoadingSpinnerDesc('Authenticating and finding metadata')
const redirectPath = await authController
.authAndGetMetadataAndRelaysMap(pubkey!)
.catch((err) => {
toast.error('Error occurred in authentication: ' + err)
return null
})
if (redirectPath) navigateAfterLogin(redirectPath)
})
.catch((err) => {
toast.error(
'An error occurred while creating nsecbunker signer: ' + err
)
})
.finally(() => {
setIsLoading(false)
setLoadingSpinnerDesc('')
})
}
const login = () => { const login = () => {
if (inputValue.startsWith('bunker://')) {
return loginWithBunkerConnectionString()
}
if (inputValue.startsWith('nsec')) { if (inputValue.startsWith('nsec')) {
return loginWithNsec() return loginWithNsec()
} }
if (inputValue.startsWith('npub')) {
return loginWithNsecBunker()
}
if (inputValue.match(NIP05_REGEX)) {
return loginWithNsecBunker()
}
// Check if maybe hex nsec // Check if maybe hex nsec
try { try {
@ -356,64 +126,33 @@ export const Nostr = () => {
console.warn('err', err) console.warn('err', err)
} }
toast.error( toast.error('Invalid format, please use: private key (hex or nsec)')
'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.'
)
return return
} }
if (authUrl) {
return (
<iframe
title="Nsecbunker auth"
src={authUrl}
width="100%"
height="500px"
/>
)
}
return ( return (
<> <>
{isLoading && ( {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<LoadingSpinner desc={loadingSpinnerDesc}>
{isExtensionSlow && (
<>
<p>
Your nostr extension is not responding. Check these
alternatives:{' '}
<a href="https://github.com/aljazceru/awesome-nostr?tab=readme-ov-file#nip-07-browser-extensions">
https://github.com/aljazceru/awesome-nostr
</a>
</p>
<br />
<Button
fullWidth
variant="contained"
onClick={() => {
setLoadingSpinnerDesc('')
setIsLoading(false)
setIsExtensionSlow(false)
}}
>
Close
</Button>
</>
)}
</LoadingSpinner>
)}
{isNostrExtensionAvailable && ( {isNostrExtensionAvailable && (
<> <>
<label className={styles.label} htmlFor="extension-login"> <label className={styles.label} htmlFor="extension-login">
Login by using a browser extension Login by using a{' '}
<a
rel="noopener"
href="https://github.com/nostrband/nostr-login"
target="_blank"
>
nostr-login
</a>
</label> </label>
<Button <Button
id="extension-login" id="nostr-login"
onClick={loginWithExtension}
variant="contained" variant="contained"
onClick={() => {
launchNostrLoginDialog()
}}
> >
Extension Login Nostr Login
</Button> </Button>
<Divider <Divider
sx={{ sx={{
@ -424,16 +163,18 @@ export const Nostr = () => {
</Divider> </Divider>
</> </>
)} )}
<form autoComplete="off">
<TextField <TextField
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
label="nip05 login / nip46 bunker string" label="Private key (Not recommended)"
helperText="Private key (Not recommended)" type="password"
autoComplete="off"
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
fullWidth fullWidth
margin="dense" margin="dense"
/> />
</form>
<Button <Button
disabled={!inputValue} disabled={!inputValue}
onClick={login} onClick={login}

View File

@ -3,13 +3,12 @@ import EditIcon from '@mui/icons-material/Edit'
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material' import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools' import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux' import { useAppSelector } from '../../hooks/store'
import { Link, useNavigate, useParams } from 'react-router-dom' import { Link, useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { MetadataController } from '../../controllers' import { MetadataController } from '../../controllers'
import { getProfileSettingsRoute } from '../../routes' import { getProfileSettingsRoute } from '../../routes'
import { State } from '../../store/rootReducer'
import { NostrJoiningBlock, ProfileMetadata } from '../../types' import { NostrJoiningBlock, ProfileMetadata } from '../../types'
import { import {
getNostrJoiningBlockNumber, getNostrJoiningBlockNumber,
@ -33,9 +32,9 @@ export const ProfilePage = () => {
const [nostrJoiningBlock, setNostrJoiningBlock] = const [nostrJoiningBlock, setNostrJoiningBlock] =
useState<NostrJoiningBlock | null>(null) useState<NostrJoiningBlock | null>(null)
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>() const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
const metadataState = useSelector((state: State) => state.metadata) const metadataState = useAppSelector((state) => state.metadata)
const { usersPubkey } = useSelector((state: State) => state.auth) const { usersPubkey } = useAppSelector((state) => state.auth)
const userRobotImage = useSelector((state: State) => state.userRobotImage) const userRobotImage = useAppSelector((state) => state.userRobotImage)
const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)

View File

@ -24,7 +24,7 @@
} }
.container { .container {
color: black color: black;
} }
.left { .left {
@ -51,7 +51,8 @@
} }
.image-placeholder { .image-placeholder {
width: 150px; width: 100%;
height: auto;
} }
.link { .link {

View File

@ -12,6 +12,11 @@ export const Register = () => {
margin="dense" margin="dense"
autoComplete="username" autoComplete="username"
disabled disabled
sx={{
input: {
cursor: 'not-allowed'
}
}}
/> />
<TextField <TextField
label="Password" label="Password"
@ -21,6 +26,11 @@ export const Register = () => {
type="password" type="password"
autoComplete="new-password" autoComplete="new-password"
disabled disabled
sx={{
input: {
cursor: 'not-allowed'
}
}}
/> />
<TextField <TextField
label="Confirm password" label="Confirm password"
@ -30,6 +40,11 @@ export const Register = () => {
type="password" type="password"
autoComplete="new-password" autoComplete="new-password"
disabled disabled
sx={{
input: {
cursor: 'not-allowed'
}
}}
/> />
<Button variant="contained" fullWidth disabled> <Button variant="contained" fullWidth disabled>

View File

@ -2,25 +2,22 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle'
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos' import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
import CachedIcon from '@mui/icons-material/Cached' import CachedIcon from '@mui/icons-material/Cached'
import RouterIcon from '@mui/icons-material/Router' import RouterIcon from '@mui/icons-material/Router'
import { useTheme } from '@mui/material' import { ListItem, useTheme } from '@mui/material'
import List from '@mui/material/List' import List from '@mui/material/List'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemIcon from '@mui/material/ListItemIcon' import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemText from '@mui/material/ListItemText' import ListItemText from '@mui/material/ListItemText'
import ListSubheader from '@mui/material/ListSubheader' import ListSubheader from '@mui/material/ListSubheader'
import { useSelector } from 'react-redux' import { useAppSelector } from '../../hooks/store'
import { useNavigate } from 'react-router-dom' import { Link } from 'react-router-dom'
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes' import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
import { State } from '../../store/rootReducer'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { Footer } from '../../components/Footer/Footer' import { Footer } from '../../components/Footer/Footer'
import ExtensionIcon from '@mui/icons-material/Extension'
import { LoginMethod } from '../../store/auth/types'
export const SettingsPage = () => { export const SettingsPage = () => {
const theme = useTheme() const theme = useTheme()
const { usersPubkey, loginMethod } = useAppSelector((state) => state.auth)
const navigate = useNavigate()
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const listItem = (label: string, disabled = false) => { const listItem = (label: string, disabled = false) => {
return ( return (
<> <>
@ -57,43 +54,40 @@ export const SettingsPage = () => {
fontSize: '1.5rem', fontSize: '1.5rem',
borderBottom: '0.5px solid', borderBottom: '0.5px solid',
paddingBottom: 2, paddingBottom: 2,
paddingTop: 2 paddingTop: 2,
zIndex: 2
}} }}
> >
Settings Settings
</ListSubheader> </ListSubheader>
} }
> >
<ListItemButton <ListItem component={Link} to={getProfileSettingsRoute(usersPubkey!)}>
onClick={() => {
navigate(getProfileSettingsRoute(usersPubkey!))
}}
>
<ListItemIcon> <ListItemIcon>
<AccountCircleIcon /> <AccountCircleIcon />
</ListItemIcon> </ListItemIcon>
{listItem('Profile')} {listItem('Profile')}
</ListItemButton> </ListItem>
<ListItemButton <ListItem component={Link} to={appPrivateRoutes.relays}>
onClick={() => {
navigate(appPrivateRoutes.relays)
}}
>
<ListItemIcon> <ListItemIcon>
<RouterIcon /> <RouterIcon />
</ListItemIcon> </ListItemIcon>
{listItem('Relays')} {listItem('Relays')}
</ListItemButton> </ListItem>
<ListItemButton <ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
onClick={() => {
navigate(appPrivateRoutes.cacheSettings)
}}
>
<ListItemIcon> <ListItemIcon>
<CachedIcon /> <CachedIcon />
</ListItemIcon> </ListItemIcon>
{listItem('Local Cache')} {listItem('Local Cache')}
</ListItemButton> </ListItem>
{loginMethod === LoginMethod.nostrLogin && (
<ListItem component={Link} to={appPrivateRoutes.nostrLogin}>
<ListItemIcon>
<ExtensionIcon />
</ListItemIcon>
{listItem('Nostr Login')}
</ListItem>
)}
</List> </List>
</Container> </Container>
<Footer /> <Footer />

View File

@ -66,7 +66,8 @@ export const CacheSettingsPage = () => {
fontSize: '1.5rem', fontSize: '1.5rem',
borderBottom: '0.5px solid', borderBottom: '0.5px solid',
paddingBottom: 2, paddingBottom: 2,
paddingTop: 2 paddingTop: 2,
zIndex: 2
}} }}
> >
Cache Setting Cache Setting

View File

@ -0,0 +1,78 @@
import {
List,
ListItemButton,
ListItemIcon,
ListItemText,
ListSubheader,
useTheme
} from '@mui/material'
import { launch as launchNostrLoginDialog } from 'nostr-login'
import { Container } from '../../../components/Container'
import PeopleIcon from '@mui/icons-material/People'
import ImportExportIcon from '@mui/icons-material/ImportExport'
import { useAppSelector } from '../../../hooks/store'
import { NostrLoginAuthMethod } from '../../../store/auth/types'
export const NostrLoginPage = () => {
const theme = useTheme()
const nostrLoginAuthMethod = useAppSelector(
(state) => state.auth?.nostrLoginAuthMethod
)
return (
<Container>
<List
sx={{
width: '100%',
bgcolor: 'background.paper'
}}
subheader={
<ListSubheader
sx={{
fontSize: '1.5rem',
borderBottom: '0.5px solid',
paddingBottom: 2,
paddingTop: 2,
zIndex: 2
}}
>
Nostr Settings
</ListSubheader>
}
>
<ListItemButton
onClick={() => {
launchNostrLoginDialog('switch-account')
}}
>
<ListItemIcon>
<PeopleIcon />
</ListItemIcon>
<ListItemText
primary={'Nostr Login Accounts'}
sx={{
color: theme.palette.text.primary
}}
/>
</ListItemButton>
{nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
<ListItemButton
onClick={() => {
launchNostrLoginDialog('import')
}}
>
<ListItemIcon>
<ImportExportIcon />
</ListItemIcon>
<ListItemText
primary={'Import / Export Keys'}
sx={{
color: theme.palette.text.primary
}}
/>
</ListItemButton>
)}
</List>
</Container>
)
}

View File

@ -18,13 +18,13 @@ import { toast } from 'react-toastify'
import { MetadataController, NostrController } from '../../../controllers' import { MetadataController, NostrController } from '../../../controllers'
import { NostrJoiningBlock, ProfileMetadata } from '../../../types' import { NostrJoiningBlock, ProfileMetadata } from '../../../types'
import styles from './style.module.scss' import styles from './style.module.scss'
import { useDispatch, useSelector } from 'react-redux' import { useAppDispatch, useAppSelector } from '../../../hooks/store'
import { State } from '../../../store/rootReducer'
import { LoadingButton } from '@mui/lab' import { LoadingButton } from '@mui/lab'
import { Dispatch } from '../../../store/store' import { Dispatch } from '../../../store/store'
import { setMetadataEvent } from '../../../store/actions' import { setMetadataEvent } from '../../../store/actions'
import { LoadingSpinner } from '../../../components/LoadingSpinner' import { LoadingSpinner } from '../../../components/LoadingSpinner'
import { LoginMethods } from '../../../store/auth/types' import { LoginMethod, NostrLoginAuthMethod } from '../../../store/auth/types'
import { SmartToy } from '@mui/icons-material' import { SmartToy } from '@mui/icons-material'
import { import {
getNostrJoiningBlockNumber, getNostrJoiningBlockNumber,
@ -33,13 +33,15 @@ import {
} from '../../../utils' } from '../../../utils'
import { Container } from '../../../components/Container' import { Container } from '../../../components/Container'
import { Footer } from '../../../components/Footer/Footer' import { Footer } from '../../../components/Footer/Footer'
import LaunchIcon from '@mui/icons-material/Launch'
import { launch as launchNostrLoginDialog } from 'nostr-login'
export const ProfileSettingsPage = () => { export const ProfileSettingsPage = () => {
const theme = useTheme() const theme = useTheme()
const { npub } = useParams() const { npub } = useParams()
const dispatch: Dispatch = useDispatch() const dispatch: Dispatch = useAppDispatch()
const metadataController = MetadataController.getInstance() const metadataController = MetadataController.getInstance()
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
@ -49,10 +51,12 @@ export const ProfileSettingsPage = () => {
useState<NostrJoiningBlock | null>(null) useState<NostrJoiningBlock | null>(null)
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>() const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
const metadataState = useSelector((state: State) => state.metadata) const metadataState = useAppSelector((state) => state.metadata)
const keys = useSelector((state: State) => state.auth?.keyPair) const keys = useAppSelector((state) => state.auth?.keyPair)
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) const { usersPubkey, loginMethod, nostrLoginAuthMethod } = useAppSelector(
const userRobotImage = useSelector((state: State) => state.userRobotImage) (state) => state.auth
)
const userRobotImage = useAppSelector((state) => state.userRobotImage)
const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)
@ -287,7 +291,8 @@ export const ProfileSettingsPage = () => {
sx={{ sx={{
paddingBottom: 1, paddingBottom: 1,
paddingTop: 1, paddingTop: 1,
fontSize: '1.5rem' fontSize: '1.5rem',
zIndex: 2
}} }}
className={styles.subHeader} className={styles.subHeader}
> >
@ -363,7 +368,7 @@ export const ProfileSettingsPage = () => {
<> <>
{usersPubkey && {usersPubkey &&
copyItem(nip19.npubEncode(usersPubkey), 'Public Key')} copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
{loginMethod === LoginMethods.privateKey && {loginMethod === LoginMethod.privateKey &&
keys && keys &&
keys.private && keys.private &&
copyItem( copyItem(
@ -373,6 +378,33 @@ export const ProfileSettingsPage = () => {
)} )}
</> </>
)} )}
{isUsersOwnProfile && (
<>
{loginMethod === LoginMethod.nostrLogin &&
nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
<ListItem
sx={{ marginTop: 1 }}
onClick={() => {
launchNostrLoginDialog('import')
}}
>
<TextField
label="Private Key (nostr-login)"
defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••"
size="small"
className={styles.textField}
disabled
type={'password'}
InputProps={{
endAdornment: (
<LaunchIcon className={styles.copyItem} />
)
}}
/>
</ListItem>
)}
</>
)}
</div> </div>
)} )}
</List> </List>

View File

@ -6,13 +6,12 @@ import _ from 'lodash'
import { MuiFileInput } from 'mui-file-input' import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools' import { Event, verifyEvent } from 'nostr-tools'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useAppSelector } from '../../hooks/store'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers' import { NostrController } from '../../controllers'
import { appPublicRoutes } from '../../routes' import { appPublicRoutes } from '../../routes'
import { State } from '../../store/rootReducer'
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
import { import {
decryptArrayBuffer, decryptArrayBuffer,
@ -55,6 +54,7 @@ import {
} from '../../utils/file.ts' } from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
import { generateTimestamp } from '../../utils/opentimestamps.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
User_Is_Next_Signer, User_Is_Next_Signer,
@ -64,17 +64,39 @@ enum SignedStatus {
export const SignPage = () => { export const SignPage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const params = useParams()
const usersAppData = useAppSelector((state) => state.userAppData)
/** /**
* Received from `location.state`
*
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json
* arrayBuffer will be received in navigation from create page in offline mode * arrayBuffer (decryptedArrayBuffer) will be received in navigation from create page in offline mode
* meta will be received in navigation from create & home page in online mode * meta (metaInNavState) will be received in navigation from create & home page in online mode
*/ */
const { let metaInNavState = location?.state?.meta || undefined
meta: metaInNavState, const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || {
arrayBuffer: decryptedArrayBuffer, decryptedArrayBuffer: undefined,
uploadedZip uploadedZip: undefined
} = location.state || {} }
/**
* If userAppData (redux) is available, and we have the route param (sigit id)
* which is actually a `createEventId`, we will fetch a `sigit`
* based on the provided route ID and set fetched `sigit` to the `metaInNavState`
*/
if (usersAppData) {
const sigitCreateId = params.id
if (sigitCreateId) {
const sigit = usersAppData.sigits[sigitCreateId]
if (sigit) {
metaInNavState = sigit
}
}
}
const [displayInput, setDisplayInput] = useState(false) const [displayInput, setDisplayInput] = useState(false)
@ -107,9 +129,8 @@ export const SignPage = () => {
// This state variable indicates whether the logged-in user is a signer, a creator, or neither. // This state variable indicates whether the logged-in user is a signer, a creator, or neither.
const [isSignerOrCreator, setIsSignerOrCreator] = useState(false) const [isSignerOrCreator, setIsSignerOrCreator] = useState(false)
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
const [authUrl, setAuthUrl] = useState<string>()
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>( const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
[] []
@ -273,11 +294,6 @@ export const SignPage = () => {
const { keys, sender } = parsedKeysJson const { keys, sender } = parsedKeysJson
for (const key of keys) { for (const key of keys) {
// Set up event listener for authentication event
nostrController.on('nsecbunker-auth', (url) => {
setAuthUrl(url)
})
// decrypt the encryptionKey, with timeout (duration = 60 seconds) // decrypt the encryptionKey, with timeout (duration = 60 seconds)
const encryptionKey = await Promise.race([ const encryptionKey = await Promise.race([
nostrController.nip04Decrypt(sender, key), nostrController.nip04Decrypt(sender, key),
@ -290,9 +306,6 @@ export const SignPage = () => {
console.log('err :>> ', err) console.log('err :>> ', err)
return null return null
}) })
.finally(() => {
setAuthUrl(undefined) // Clear authentication URL
})
// Return if encryption failed // Return if encryption failed
if (!encryptionKey) continue if (!encryptionKey) continue
@ -524,7 +537,11 @@ export const SignPage = () => {
setIsLoading(true) setIsLoading(true)
const arrayBuffer = await decrypt(selectedFile) const arrayBuffer = await decrypt(selectedFile)
if (!arrayBuffer) return if (!arrayBuffer) {
setIsLoading(false)
toast.error('Error decrypting file')
return
}
handleDecryptedArrayBuffer(arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
} }
@ -767,14 +784,9 @@ export const SignPage = () => {
2 2
) )
const zip = new JSZip() const zip = await getZipWithFiles(meta, files)
zip.file('meta.json', stringifiedMeta) zip.file('meta.json', stringifiedMeta)
for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await file.arrayBuffer())
}
const arrayBuffer = await zip const arrayBuffer = await zip
.generateAsync({ .generateAsync({
type: 'arraybuffer', type: 'arraybuffer',
@ -800,19 +812,14 @@ export const SignPage = () => {
navigate(appPublicRoutes.verify) navigate(appPublicRoutes.verify)
} }
const handleExportSigit = async () => { const handleEncryptedExport = async () => {
if (Object.entries(files).length === 0 || !meta) return if (Object.entries(files).length === 0 || !meta) return
const zip = new JSZip()
const stringifiedMeta = JSON.stringify(meta, null, 2) const stringifiedMeta = JSON.stringify(meta, null, 2)
const zip = await getZipWithFiles(meta, files)
zip.file('meta.json', stringifiedMeta) zip.file('meta.json', stringifiedMeta)
for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await file.arrayBuffer())
}
const arrayBuffer = await zip const arrayBuffer = await zip
.generateAsync({ .generateAsync({
type: 'arraybuffer', type: 'arraybuffer',
@ -874,17 +881,6 @@ export const SignPage = () => {
} }
} }
if (authUrl) {
return (
<iframe
title="Nsecbunker auth"
src={authUrl}
width="100%"
height="500px"
/>
)
}
if (isLoading) { if (isLoading) {
return <LoadingSpinner desc={loadingSpinnerDesc} /> return <LoadingSpinner desc={loadingSpinnerDesc} />
} }
@ -950,7 +946,7 @@ export const SignPage = () => {
{signedStatus === SignedStatus.Fully_Signed && ( {signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}> <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained"> <Button onClick={handleExport} variant="contained">
Export Export Sigit
</Button> </Button>
</Box> </Box>
)} )}
@ -965,8 +961,8 @@ export const SignPage = () => {
{isSignerOrCreator && ( {isSignerOrCreator && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}> <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExportSigit} variant="contained"> <Button onClick={handleEncryptedExport} variant="contained">
Export Sigit Export Encrypted Sigit
</Button> </Button>
</Box> </Box>
)} )}

View File

@ -14,6 +14,7 @@
border-bottom: 0.5px solid; border-bottom: 0.5px solid;
padding: 8px 16px; padding: 8px 16px;
font-size: 1.5rem; font-size: 1.5rem;
z-index: 2;
} }
.filesWrapper { .filesWrapper {
@ -62,7 +63,6 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
//z-index: 200;
} }
.fixedBottomForm input[type='text'] { .fixedBottomForm input[type='text'] {

View File

@ -14,7 +14,6 @@ import {
} from '../../types' } from '../../types'
import { import {
decryptArrayBuffer, decryptArrayBuffer,
extractMarksFromSignedMeta,
getHash, getHash,
hexToNpub, hexToNpub,
unixNow, unixNow,
@ -29,15 +28,8 @@ import {
import styles from './style.module.scss' import styles from './style.module.scss'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import axios from 'axios' import axios from 'axios'
import { import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
addMarks, import { useAppSelector } from '../../hooks/store'
FONT_SIZE,
FONT_TYPE,
groupMarksByFileNamePage,
inPx
} from '../../utils/pdf.ts'
import { State } from '../../store/rootReducer.ts'
import { useSelector } from 'react-redux'
import { getLastSignersSig } from '../../utils/sign.ts' import { getLastSignersSig } from '../../utils/sign.ts'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
@ -48,7 +40,11 @@ import FileList from '../../components/FileList'
import { CurrentUserFile } from '../../types/file.ts' import { CurrentUserFile } from '../../types/file.ts'
import { Mark } from '../../types/mark.ts' import { Mark } from '../../types/mark.ts'
import React from 'react' import React from 'react'
import { convertToSigitFile, SigitFile } from '../../utils/file.ts' import {
convertToSigitFile,
getZipWithFiles,
SigitFile
} from '../../utils/file.ts'
import { FileDivider } from '../../components/FileDivider.tsx' import { FileDivider } from '../../components/FileDivider.tsx'
import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx' import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx'
import { useScale } from '../../hooks/useScale.tsx' import { useScale } from '../../hooks/useScale.tsx'
@ -164,7 +160,7 @@ const SlimPdfView = ({
export const VerifyPage = () => { export const VerifyPage = () => {
const location = useLocation() const location = useLocation()
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
@ -298,7 +294,9 @@ export const VerifyPage = () => {
return timestamp return timestamp
}) })
if (upgradedUserTimestamps.length > 0) { console.log('upgraded timestamps: ', upgradedTimestamps)
if (upgradedUserTimestamps.length === 0) {
return return
} }
@ -327,6 +325,7 @@ export const VerifyPage = () => {
updatedMeta.modifiedAt = unixNow() updatedMeta.modifiedAt = unixNow()
const updatedEvent = await updateUsersAppData(updatedMeta) const updatedEvent = await updateUsersAppData(updatedMeta)
console.log('updated event: ', updatedEvent)
if (!updatedEvent) return if (!updatedEvent) return
const userSet = new Set<`npub1${string}`>() const userSet = new Set<`npub1${string}`>()
@ -523,7 +522,7 @@ export const VerifyPage = () => {
setIsLoading(false) setIsLoading(false)
} }
const handleExport = async () => { const handleMarkedExport = async () => {
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
const usersNpub = hexToNpub(usersPubkey) const usersNpub = hexToNpub(usersPubkey)
@ -553,22 +552,9 @@ export const VerifyPage = () => {
const updatedMeta = { ...meta, exportSignature } const updatedMeta = { ...meta, exportSignature }
const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) const stringifiedMeta = JSON.stringify(updatedMeta, null, 2)
const zip = new JSZip() const zip = await getZipWithFiles(updatedMeta, files)
zip.file('meta.json', stringifiedMeta) zip.file('meta.json', stringifiedMeta)
const marks = extractMarksFromSignedMeta(updatedMeta)
const marksByPage = groupMarksByFileNamePage(marks)
for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) {
// Draw marks into PDF file and generate a brand new blob
const blob = await addMarks(file, marksByPage[fileName])
zip.file(`files/${fileName}`, blob)
} else {
zip.file(`files/${fileName}`, file)
}
}
const arrayBuffer = await zip const arrayBuffer = await zip
.generateAsync({ .generateAsync({
type: 'arraybuffer', type: 'arraybuffer',
@ -635,7 +621,7 @@ export const VerifyPage = () => {
)} )}
currentFile={currentFile} currentFile={currentFile}
setCurrentFile={setCurrentFile} setCurrentFile={setCurrentFile}
handleDownload={handleExport} handleDownload={handleMarkedExport}
downloadLabel="Download Sigit" downloadLabel="Download Sigit"
/> />
) )

View File

@ -1,13 +1,10 @@
import { Modal } from '../layouts/modal'
import { CreatePage } from '../pages/create' import { CreatePage } from '../pages/create'
import { HomePage } from '../pages/home' import { HomePage } from '../pages/home'
import { LandingPage } from '../pages/landing' import { LandingPage } from '../pages/landing'
import { Login } from '../pages/login'
import { Nostr } from '../pages/nostr'
import { ProfilePage } from '../pages/profile' import { ProfilePage } from '../pages/profile'
import { Register } from '../pages/register'
import { SettingsPage } from '../pages/settings/Settings' import { SettingsPage } from '../pages/settings/Settings'
import { CacheSettingsPage } from '../pages/settings/cache' import { CacheSettingsPage } from '../pages/settings/cache'
import { NostrLoginPage } from '../pages/settings/nostrLogin'
import { ProfileSettingsPage } from '../pages/settings/profile' import { ProfileSettingsPage } from '../pages/settings/profile'
import { RelaysPage } from '../pages/settings/relays' import { RelaysPage } from '../pages/settings/relays'
import { SignPage } from '../pages/sign' import { SignPage } from '../pages/sign'
@ -22,7 +19,8 @@ export const appPrivateRoutes = {
settings: '/settings', settings: '/settings',
profileSettings: '/settings/profile/:npub', profileSettings: '/settings/profile/:npub',
cacheSettings: '/settings/cache', cacheSettings: '/settings/cache',
relays: '/settings/relays' relays: '/settings/relays',
nostrLogin: '/settings/nostrLogin'
} }
export const appPublicRoutes = { export const appPublicRoutes = {
@ -85,29 +83,7 @@ export const publicRoutes: PublicRouteProps[] = [
{ {
path: appPublicRoutes.landingPage, path: appPublicRoutes.landingPage,
hiddenWhenLoggedIn: true, hiddenWhenLoggedIn: true,
element: <LandingPage />, element: <LandingPage />
children: [
{
element: <Modal />,
children: [
{
path: appPublicRoutes.login,
hiddenWhenLoggedIn: true,
element: <Login />
},
{
path: appPublicRoutes.register,
hiddenWhenLoggedIn: true,
element: <Register />
},
{
path: appPublicRoutes.nostr,
hiddenWhenLoggedIn: true,
element: <Nostr />
}
]
}
]
}, },
{ {
path: appPublicRoutes.profile, path: appPublicRoutes.profile,
@ -129,7 +105,7 @@ export const privateRoutes = [
element: <CreatePage /> element: <CreatePage />
}, },
{ {
path: appPrivateRoutes.sign, path: `${appPrivateRoutes.sign}/:id?`,
element: <SignPage /> element: <SignPage />
}, },
{ {
@ -147,5 +123,9 @@ export const privateRoutes = [
{ {
path: appPrivateRoutes.relays, path: appPrivateRoutes.relays,
element: <RelaysPage /> element: <RelaysPage />
},
{
path: appPrivateRoutes.nostrLogin,
element: <NostrLoginPage />
} }
] ]

View File

@ -0,0 +1,81 @@
import { Event, UnsignedEvent, EventTemplate, NostrEvent } from 'nostr-tools'
import { SignedEvent } from '../../types'
import { LoginMethodStrategy } from './loginMethodStrategy'
import { WindowNostr } from 'nostr-tools/nip07'
/**
* Login Method Strategy when using nostr-login package.
*
* This class extends {@link LoginMethodStrategy base strategy} and implements all login method operations
* @see {@link LoginMethodStrategy}
*/
export class NostrLoginStrategy extends LoginMethodStrategy {
private nostr: WindowNostr
constructor() {
super()
if (!window.nostr) {
throw new Error(
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
)
}
this.nostr = window.nostr as WindowNostr
}
async nip04Encrypt(receiver: string, content: string): Promise<string> {
if (!this.nostr.nip04) {
throw new Error(
`Your nostr extension does not support nip04 encryption & decryption`
)
}
const encrypted = await this.nostr.nip04.encrypt(receiver, content)
return encrypted
}
async nip04Decrypt(sender: string, content: string): Promise<string> {
if (!this.nostr.nip04) {
throw new Error(
`Your nostr extension does not support nip04 encryption & decryption`
)
}
const decrypted = await this.nostr.nip04.decrypt(sender, content)
return decrypted
}
async nip44Encrypt(receiver: string, content: string): Promise<string> {
if (!this.nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Encrypt the content using NIP-44 provided by the nostr extension.
const encrypted = await this.nostr.nip44.encrypt(receiver, content)
return encrypted as string
}
async nip44Decrypt(sender: string, content: string): Promise<string> {
if (!this.nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Decrypt the content using NIP-44 provided by the nostr extension.
const decrypted = await this.nostr.nip44.decrypt(sender, content)
return decrypted as string
}
async signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
return (await this.nostr
.signEvent(event as NostrEvent)
.catch((err: unknown) => {
console.log('Error while signing event: ', err)
throw err
})) as Event
}
}

View File

@ -0,0 +1,124 @@
import {
UnsignedEvent,
EventTemplate,
nip19,
nip44,
finalizeEvent,
nip04
} from 'nostr-tools'
import { SignedEvent } from '../../types'
import store from '../../store/store'
import { LoginMethod } from '../../store/auth/types'
import { LoginMethodStrategy } from './loginMethodStrategy'
import { verifySignedEvent } from '../../utils/nostr'
/**
* Login Method Strategy when using dev private key login.
*
* This class extends {@link LoginMethodStrategy base strategy} and implements all login method operations
* @see {@link LoginMethodStrategy}
*/
export class PrivateKeyStrategy extends LoginMethodStrategy {
async nip04Encrypt(receiver: string, content: string): Promise<string> {
const keys = store.getState().auth.keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const encrypted = await nip04.encrypt(privateKey, receiver, content)
return encrypted
}
async nip04Decrypt(sender: string, content: string): Promise<string> {
const keys = store.getState().auth.keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const decrypted = await nip04.decrypt(privateKey, sender, content)
return decrypted
}
async nip44Encrypt(receiver: string, content: string): Promise<string> {
const keys = store.getState().auth.keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
receiver
)
// Encrypt the content using the generated conversation key.
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
return encrypted
}
async nip44Decrypt(sender: string, content: string): Promise<string> {
const keys = store.getState().auth.keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
sender
)
// Decrypt the content using the generated conversation key.
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
return decrypted
}
async signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
const keys = store.getState().auth.keyPair
if (!keys) {
return Promise.reject(
`Login method is ${LoginMethod.privateKey}, but keys are not found`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const signedEvent = finalizeEvent(event, privateKey)
verifySignedEvent(signedEvent)
return Promise.resolve(signedEvent)
}
}

View File

@ -0,0 +1,50 @@
import { UnsignedEvent, EventTemplate } from 'nostr-tools'
import { SignedEvent } from '../../types'
import {
LoginMethodStrategy,
LoginMethodOperations
} from './loginMethodStrategy'
import { LoginMethod } from '../../store/auth/types'
import { NostrLoginStrategy } from './NostrLoginStrategy'
import { PrivateKeyStrategy } from './PrivateKeyStrategy'
/**
* This class is a context provider and helper class. This MUST be instantiated and used as an entry point for any of the {@link LoginMethodOperations LoginMethodOperations}
* @constructor Takes {@link LoginMethod LoginMethod} as an argument and sets the correct strategy
*
* @see {@link LoginMethod}
* @see {@link LoginMethodOperations}
*/
export class LoginMethodContext implements LoginMethodOperations {
private strategy: LoginMethodStrategy
constructor(loginMethod?: LoginMethod) {
switch (loginMethod) {
case LoginMethod.nostrLogin:
this.strategy = new NostrLoginStrategy()
break
case LoginMethod.privateKey:
this.strategy = new PrivateKeyStrategy()
break
default:
this.strategy = new LoginMethodStrategy()
break
}
}
nip04Encrypt(receiver: string, content: string): Promise<string> {
return this.strategy.nip04Encrypt(receiver, content)
}
nip04Decrypt(sender: string, content: string): Promise<string> {
return this.strategy.nip04Decrypt(sender, content)
}
nip44Encrypt(receiver: string, content: string): Promise<string> {
return this.strategy.nip44Encrypt(receiver, content)
}
nip44Decrypt(sender: string, content: string): Promise<string> {
return this.strategy.nip44Decrypt(sender, content)
}
signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
return this.strategy.signEvent(event)
}
}

View File

@ -0,0 +1,38 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { EventTemplate, UnsignedEvent } from 'nostr-tools'
import { SignedEvent } from '../../types/nostr'
/**
* This interface holds all operations that are dependant on the login method and is used as the basis for the login strategies.
*/
export interface LoginMethodOperations {
nip04Encrypt(receiver: string, content: string): Promise<string>
nip04Decrypt(sender: string, content: string): Promise<string>
nip44Encrypt(receiver: string, content: string): Promise<string>
nip44Decrypt(sender: string, content: string): Promise<string>
signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent>
}
/**
* This is the fallback class that provides base implementation for the {@link LoginMethodOperations login method operations} . Only used to throw errors in case when the LoginMethod is missing (login context not set).
* @see {@link LoginMethodOperations}
*/
export class LoginMethodStrategy implements LoginMethodOperations {
async nip04Encrypt(_receiver: string, _content: string): Promise<string> {
throw new Error('Login method strategy is undefined')
}
async nip04Decrypt(_sender: string, _content: string): Promise<string> {
throw new Error('Login method strategy is undefined')
}
async nip44Encrypt(_receiver: string, _content: string): Promise<string> {
throw new Error('Login method strategy is undefined')
}
async nip44Decrypt(_sender: string, _content: string): Promise<string> {
throw new Error('Login method strategy is undefined')
}
async signEvent(_event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
return Promise.reject(
`We could not sign the event, none of the signing methods are available`
)
}
}

View File

@ -4,9 +4,8 @@ export const USER_LOGOUT = 'USER_LOGOUT'
export const SET_AUTH_STATE = 'SET_AUTH_STATE' 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_NOSTR_LOGIN_AUTH_METHOD = 'UPDATE_NOSTR_LOGIN_AUTH_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_RELAYS = 'UPDATE_NSECBUNKER_RELAYS'
export const SET_METADATA_EVENT = 'SET_METADATA_EVENT' export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'

View File

@ -2,12 +2,12 @@ import * as ActionTypes from '../actionTypes'
import { import {
AuthState, AuthState,
Keys, Keys,
LoginMethods, LoginMethod,
SetAuthState, SetAuthState,
UpdateKeyPair, UpdateKeyPair,
UpdateLoginMethod, UpdateLoginMethod,
UpdateNsecBunkerPubkey, NostrLoginAuthMethod,
UpdateNsecBunkerRelays UpdateNostrLoginAuthMethod
} from './types' } from './types'
export const setAuthState = (payload: AuthState): SetAuthState => ({ export const setAuthState = (payload: AuthState): SetAuthState => ({
@ -16,27 +16,20 @@ export const setAuthState = (payload: AuthState): SetAuthState => ({
}) })
export const updateLoginMethod = ( export const updateLoginMethod = (
payload: LoginMethods | undefined payload: LoginMethod | undefined
): UpdateLoginMethod => ({ ): UpdateLoginMethod => ({
type: ActionTypes.UPDATE_LOGIN_METHOD, type: ActionTypes.UPDATE_LOGIN_METHOD,
payload payload
}) })
export const updateNostrLoginAuthMethod = (
payload: NostrLoginAuthMethod | undefined
): UpdateNostrLoginAuthMethod => ({
type: ActionTypes.UPDATE_NOSTR_LOGIN_AUTH_METHOD,
payload
})
export const updateKeyPair = (payload: Keys | undefined): UpdateKeyPair => ({ export const updateKeyPair = (payload: Keys | undefined): UpdateKeyPair => ({
type: ActionTypes.UPDATE_KEYPAIR, type: ActionTypes.UPDATE_KEYPAIR,
payload payload
}) })
export const updateNsecbunkerPubkey = (
payload: string | undefined
): UpdateNsecBunkerPubkey => ({
type: ActionTypes.UPDATE_NSECBUNKER_PUBKEY,
payload
})
export const updateNsecbunkerRelays = (
payload: string[] | undefined
): UpdateNsecBunkerRelays => ({
type: ActionTypes.UPDATE_NSECBUNKER_RELAYS,
payload
})

View File

@ -8,16 +8,15 @@ const initialState: AuthState = {
const reducer = ( const reducer = (
state = initialState, state = initialState,
action: AuthDispatchTypes action: AuthDispatchTypes
): AuthState | null => { ): AuthState => {
switch (action.type) { switch (action.type) {
case ActionTypes.SET_AUTH_STATE: { case ActionTypes.SET_AUTH_STATE: {
const { loginMethod, keyPair, nsecBunkerPubkey, nsecBunkerRelays } = state const { loginMethod, nostrLoginAuthMethod, keyPair } = state
return { return {
loginMethod, loginMethod,
nostrLoginAuthMethod,
keyPair, keyPair,
nsecBunkerPubkey,
nsecBunkerRelays,
...action.payload ...action.payload
} }
} }
@ -30,6 +29,15 @@ const reducer = (
} }
} }
case ActionTypes.UPDATE_NOSTR_LOGIN_AUTH_METHOD: {
const { payload } = action
return {
...state,
nostrLoginAuthMethod: payload
}
}
case ActionTypes.UPDATE_KEYPAIR: { case ActionTypes.UPDATE_KEYPAIR: {
const { payload } = action const { payload } = action
@ -39,26 +47,8 @@ const reducer = (
} }
} }
case ActionTypes.UPDATE_NSECBUNKER_PUBKEY: {
const { payload } = action
return {
...state,
nsecBunkerPubkey: payload
}
}
case ActionTypes.UPDATE_NSECBUNKER_RELAYS: {
const { payload } = action
return {
...state,
nsecBunkerRelays: payload
}
}
case ActionTypes.RESTORE_STATE: case ActionTypes.RESTORE_STATE:
return action.payload.auth return action.payload.auth || initialState
default: default:
return state return state

View File

@ -1,10 +1,17 @@
import * as ActionTypes from '../actionTypes' import * as ActionTypes from '../actionTypes'
import { RestoreState, UserLogout } from '../actions' import { RestoreState, UserLogout } from '../actions'
export enum LoginMethods { export enum NostrLoginAuthMethod {
extension = 'extension', Connect = 'connect',
ReadOnly = 'readOnly',
Extension = 'extension',
Local = 'local',
OTP = 'otp'
}
export enum LoginMethod {
nostrLogin = 'nostrLogin',
privateKey = 'privateKey', privateKey = 'privateKey',
nsecBunker = 'nsecBunker',
register = 'register' register = 'register'
} }
@ -16,10 +23,21 @@ export interface Keys {
export interface AuthState { export interface AuthState {
loggedIn: boolean loggedIn: boolean
usersPubkey?: string usersPubkey?: string
loginMethod?: LoginMethods /**
* sigit login {@link LoginMethod methods }
* @see {@link LoginMethod}
*/
loginMethod?: LoginMethod
/**
* nostr-login package specific {@link NostrLoginAuthMethod method }
* @see {@link NostrLoginAuthMethod}
*/
nostrLoginAuthMethod?: NostrLoginAuthMethod
/**
* {@link Keys keyPair} for user auth (usually only public is available)
* @see {@link Keys}
*/
keyPair?: Keys keyPair?: Keys
nsecBunkerPubkey?: string
nsecBunkerRelays?: string[]
} }
export interface SetAuthState { export interface SetAuthState {
@ -29,7 +47,12 @@ export interface SetAuthState {
export interface UpdateLoginMethod { export interface UpdateLoginMethod {
type: typeof ActionTypes.UPDATE_LOGIN_METHOD type: typeof ActionTypes.UPDATE_LOGIN_METHOD
payload: LoginMethods | undefined payload: LoginMethod | undefined
}
export interface UpdateNostrLoginAuthMethod {
type: typeof ActionTypes.UPDATE_NOSTR_LOGIN_AUTH_METHOD
payload: NostrLoginAuthMethod | undefined
} }
export interface UpdateKeyPair { export interface UpdateKeyPair {
@ -37,21 +60,10 @@ export interface UpdateKeyPair {
payload: Keys | undefined payload: Keys | undefined
} }
export interface UpdateNsecBunkerPubkey {
type: typeof ActionTypes.UPDATE_NSECBUNKER_PUBKEY
payload: string | undefined
}
export interface UpdateNsecBunkerRelays {
type: typeof ActionTypes.UPDATE_NSECBUNKER_RELAYS
payload: string[] | undefined
}
export type AuthDispatchTypes = export type AuthDispatchTypes =
| RestoreState | RestoreState
| SetAuthState | SetAuthState
| UpdateLoginMethod | UpdateLoginMethod
| UpdateNostrLoginAuthMethod
| UpdateKeyPair | UpdateKeyPair
| UpdateNsecBunkerPubkey
| UpdateNsecBunkerRelays
| UserLogout | UserLogout

View File

@ -15,7 +15,7 @@ const reducer = (
} }
case ActionTypes.RESTORE_STATE: case ActionTypes.RESTORE_STATE:
return action.payload.metadata || null return action.payload.metadata || initialState
default: default:
return state return state

View File

@ -10,7 +10,7 @@ const initialState: RelaysState = {
const reducer = ( const reducer = (
state = initialState, state = initialState,
action: RelaysDispatchTypes action: RelaysDispatchTypes
): RelaysState | null => { ): RelaysState => {
switch (action.type) { switch (action.type) {
case ActionTypes.SET_RELAY_MAP: case ActionTypes.SET_RELAY_MAP:
return { ...state, map: action.payload, mapUpdated: Date.now() } return { ...state, map: action.payload, mapUpdated: Date.now() }
@ -25,7 +25,7 @@ const reducer = (
} }
case ActionTypes.RESTORE_STATE: case ActionTypes.RESTORE_STATE:
return action.payload.relays return action.payload.relays || initialState
default: default:
return state return state

View File

@ -12,7 +12,7 @@ const reducer = (
return action.payload return action.payload
case ActionTypes.RESTORE_STATE: case ActionTypes.RESTORE_STATE:
return action.payload.userRobotImage return action.payload.userRobotImage || initialState
default: default:
return state return state

View File

@ -60,10 +60,24 @@ export interface OpenTimestampUpgradeVerifyResponse {
} }
export interface UserAppData { export interface UserAppData {
sigits: { [key: string]: Meta } // key will be id of create signature /**
processedGiftWraps: string[] // an array of ids of processed gift wrapped events * Key will be id of create signature
keyPair?: Keys // this key pair is used for blossom requests authentication */
blossomUrls: string[] // array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom sigits: { [key: string]: Meta }
/**
* An array of ids of processed gift wrapped events
*/
processedGiftWraps: string[]
/**
* Generated ephemeral key pair (https://docs.sigit.io/#/technical?id=storing-app-data).
* This {@link Keys key pair} is used for blossom requests authentication.
* @see {@link Keys}
*/
keyPair?: Keys
/**
* Array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom.
*/
blossomUrls: string[]
} }
export interface DocSignatureEvent extends Event { export interface DocSignatureEvent extends Event {

View File

@ -1,11 +1,4 @@
import { MarkType } from '../types/drawing.ts'
export const EMPTY: string = '' export const EMPTY: string = ''
export const MARK_TYPE_TRANSLATION: { [key: string]: string } = {
[MarkType.FULLNAME.valueOf()]: 'Full Name'
}
export const SIGN: string = 'Sign'
export const NEXT: string = 'Next'
export const ARRAY_BUFFER = 'arraybuffer' export const ARRAY_BUFFER = 'arraybuffer'
export const DEFLATE = 'DEFLATE' export const DEFLATE = 'DEFLATE'

View File

@ -19,14 +19,14 @@ export const getZipWithFiles = async (
const marksByFileNamePage = groupMarksByFileNamePage(marks) const marksByFileNamePage = groupMarksByFileNamePage(marks)
for (const [fileName, file] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) { // Handle PDF Files, add marks
// Handle PDF Files if (file.isPdf && fileName in marksByFileNamePage) {
const blob = await addMarks(file, marksByFileNamePage[fileName]) const blob = await addMarks(file, marksByFileNamePage[fileName])
zip.file(`files/${fileName}`, blob) zip.file(`marked/${fileName}`, blob)
} else {
// Handle other files
zip.file(`files/${fileName}`, file)
} }
// Save original files
zip.file(`files/${fileName}`, file)
} }
return zip return zip

View File

@ -26,14 +26,6 @@ export const clearState = () => {
localStorage.removeItem('state') localStorage.removeItem('state')
} }
export const saveNsecBunkerDelegatedKey = (privateKey: string) => {
localStorage.setItem('nsecbunker-delegated-key', privateKey)
}
export const getNsecBunkerDelegatedKey = () => {
return localStorage.getItem('nsecbunker-delegated-key')
}
export const saveVisitedLink = (pathname: string, search: string) => { export const saveVisitedLink = (pathname: string, search: string) => {
localStorage.setItem( localStorage.setItem(
'visitedLink', 'visitedLink',
@ -69,3 +61,8 @@ export const getAuthToken = () => {
export const clearAuthToken = () => { export const clearAuthToken = () => {
localStorage.removeItem('authToken') localStorage.removeItem('authToken')
} }
export const clear = () => {
clearAuthToken()
clearState()
}

View File

@ -153,6 +153,11 @@ const findOtherUserMarks = (marks: Mark[], pubkey: string): Mark[] => {
} }
export const DEFAULT_TOOLBOX: DrawTool[] = [ export const DEFAULT_TOOLBOX: DrawTool[] = [
{
identifier: MarkType.TEXT,
icon: faT,
label: 'Text'
},
{ {
identifier: MarkType.FULLNAME, identifier: MarkType.FULLNAME,
icon: faIdCard, icon: faIdCard,
@ -177,11 +182,6 @@ export const DEFAULT_TOOLBOX: DrawTool[] = [
label: 'Date Time', label: 'Date Time',
isComingSoon: true isComingSoon: true
}, },
{
identifier: MarkType.TEXT,
icon: faT,
label: 'Text'
},
{ {
identifier: MarkType.NUMBER, identifier: MarkType.NUMBER,
icon: fa1, icon: fa1,

View File

@ -10,7 +10,6 @@ import {
} from 'nostr-tools' } from 'nostr-tools'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
import { AuthState } from '../store/auth/types'
import store from '../store/store' import store from '../store/store'
import { CreateSignatureEventContent, Meta } from '../types' import { CreateSignatureEventContent, Meta } from '../types'
import { hexToNpub, unixNow } from './nostr' import { hexToNpub, unixNow } from './nostr'
@ -232,7 +231,7 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
const zipUrl = createSignatureContent.zipUrl const zipUrl = createSignatureContent.zipUrl
// Retrieve the user's public key from the state // Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = store.getState().auth.usersPubkey!
const usersNpub = hexToNpub(usersPubkey) const usersNpub = hexToNpub(usersPubkey)
// Return null if the metadata does not contain keys // Return null if the metadata does not contain keys

View File

@ -27,8 +27,7 @@ import {
updateProcessedGiftWraps, updateProcessedGiftWraps,
updateUserAppData as updateUserAppDataAction updateUserAppData as updateUserAppDataAction
} from '../store/actions' } from '../store/actions'
import { AuthState, Keys } from '../store/auth/types' import { Keys } from '../store/auth/types'
import { RelaysState } from '../store/relays/types'
import store from '../store/store' import store from '../store/store'
import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types' import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
import { getDefaultRelayMap } from './relays' import { getDefaultRelayMap } from './relays'
@ -41,7 +40,7 @@ import { SIGIT_BLOSSOM } from './const.ts'
* Generates a `d` tag for userAppData * Generates a `d` tag for userAppData
*/ */
const getDTagForUserAppData = async (): Promise<string | null> => { const getDTagForUserAppData = async (): Promise<string | null> => {
const isLoggedIn = store.getState().auth?.loggedIn const isLoggedIn = store.getState().auth.loggedIn
const pubkey = store.getState().auth?.usersPubkey const pubkey = store.getState().auth?.usersPubkey
if (!isLoggedIn || !pubkey) { if (!isLoggedIn || !pubkey) {
@ -360,7 +359,7 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
const relays: string[] = [] const relays: string[] = []
// Retrieve the user's public key and relay map from the Redux store // Retrieve the user's public key and relay map from the Redux store
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = store.getState().auth.usersPubkey!
const relayMap = store.getState().relays?.map const relayMap = store.getState().relays?.map
// Check if relayMap is undefined in the Redux store // Check if relayMap is undefined in the Redux store
@ -426,6 +425,7 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
// Handle case where the encrypted content is an empty object // Handle case where the encrypted content is an empty object
if (encryptedContent === '{}') { if (encryptedContent === '{}') {
// Generate ephemeral key pair
const secret = generateSecretKey() const secret = generateSecretKey()
const pubKey = getPublicKey(secret) const pubKey = getPublicKey(secret)
@ -570,7 +570,7 @@ export const updateUsersAppData = async (meta: Meta) => {
}) })
} }
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = store.getState().auth.usersPubkey!
// encrypt content for storing in kind 30078 event // encrypt content for storing in kind 30078 event
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
@ -617,8 +617,7 @@ export const updateUsersAppData = async (meta: Meta) => {
if (!signedEvent) return null if (!signedEvent) return null
const relayMap = const relayMap = store.getState().relays.map || getDefaultRelayMap()
(store.getState().relays as RelaysState).map || getDefaultRelayMap()
const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write) const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)
const publishResult = await Promise.race([ const publishResult = await Promise.race([
@ -876,8 +875,11 @@ export const subscribeForSigits = async (pubkey: string) => {
} }
const processReceivedEvent = async (event: Event, difficulty: number = 5) => { const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
const processedEvents = (store.getState().userAppData as UserAppData) const processedEvents = store.getState().userAppData?.processedGiftWraps
.processedGiftWraps
// Abort processing if userAppData is undefined
if (!processedEvents) return
if (processedEvents.includes(event.id)) return if (processedEvents.includes(event.id)) return
store.dispatch(updateProcessedGiftWraps([...processedEvents, event.id])) store.dispatch(updateProcessedGiftWraps([...processedEvents, event.id]))
@ -928,7 +930,7 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
*/ */
export const sendNotification = async (receiver: string, meta: Meta) => { export const sendNotification = async (receiver: string, meta: Meta) => {
// Retrieve the user's public key from the state // Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = store.getState().auth.usersPubkey!
// Create an unsigned event object with the provided metadata // Create an unsigned event object with the provided metadata
const unsignedEvent: UnsignedEvent = { const unsignedEvent: UnsignedEvent = {