diff --git a/frontend/deno.lock b/frontend/deno.lock index a72040c..c3a9dc8 100644 --- a/frontend/deno.lock +++ b/frontend/deno.lock @@ -5,20 +5,20 @@ "npm:@atcute/crypto@^2.3.0": "2.3.0", "npm:@atcute/did-plc@~0.3.1": "0.3.1", "npm:@atcute/multibase@^1.1.6": "1.1.6", - "npm:@noble/secp256k1@^2.1.0": "2.3.0", - "npm:@sveltejs/vite-plugin-svelte@5": "5.1.1_svelte@5.45.10__acorn@8.15.0_vite@6.4.1__picomatch@4.0.3", - "npm:@testing-library/jest-dom@^6.6.3": "6.9.1", - "npm:@testing-library/svelte@^5.2.6": "5.2.9_svelte@5.45.10__acorn@8.15.0_vite@6.4.1__picomatch@4.0.3_vitest@2.1.9__jsdom@25.0.1__vite@5.4.21_jsdom@25.0.1", - "npm:@testing-library/user-event@^14.5.2": "14.6.1_@testing-library+dom@10.4.1", + "npm:@noble/secp256k1@3": "3.0.0", + "npm:@sveltejs/vite-plugin-svelte@^6.2.1": "6.2.1_svelte@5.46.1__acorn@8.15.0_vite@7.3.0__picomatch@4.0.3", + "npm:@testing-library/jest-dom@^6.9.1": "6.9.1", + "npm:@testing-library/svelte@^5.3.1": "5.3.1_svelte@5.46.1__acorn@8.15.0_vite@7.3.0__picomatch@4.0.3_vitest@4.0.16__jsdom@25.0.1__vite@7.3.0___picomatch@4.0.3_jsdom@25.0.1", + "npm:@testing-library/user-event@^14.6.1": "14.6.1_@testing-library+dom@10.4.1", "npm:jsdom@^25.0.1": "25.0.1", - "npm:multiformats@^13.3.1": "13.4.2", - "npm:svelte-check@*": "4.3.5_svelte@5.45.10__acorn@8.15.0_typescript@5.9.3", - "npm:svelte-i18n@^4.0.1": "4.0.1_svelte@5.45.10__acorn@8.15.0", - "npm:svelte@5": "5.45.10_acorn@8.15.0", - "npm:vite@*": "6.4.1_picomatch@4.0.3", - "npm:vite@6": "6.4.1_picomatch@4.0.3", - "npm:vitest@*": "2.1.9_jsdom@25.0.1_vite@5.4.21", - "npm:vitest@^2.1.8": "2.1.9_jsdom@25.0.1_vite@5.4.21" + "npm:multiformats@^13.4.2": "13.4.2", + "npm:svelte-i18n@^4.0.1": "4.0.1_svelte@5.46.1__acorn@8.15.0", + "npm:svelte@^5.46.1": "5.46.1_acorn@8.15.0", + "npm:vite@*": "7.3.0_picomatch@4.0.3", + "npm:vite@^7.3.0": "7.3.0_picomatch@4.0.3", + "npm:vitest@*": "4.0.16_jsdom@25.0.1_vite@7.3.0__picomatch@4.0.3", + "npm:vitest@^4.0.16": "4.0.16_jsdom@25.0.1_vite@7.3.0__picomatch@4.0.3", + "npm:zod@^4.3.5": "4.3.5" }, "npm": { "@adobe/css-tools@4.4.4": { @@ -54,7 +54,7 @@ "dependencies": [ "@atcute/multibase", "@atcute/uint8array", - "@noble/secp256k1@3.0.0" + "@noble/secp256k1" ] }, "@atcute/did-plc@0.3.1": { @@ -96,8 +96,8 @@ "@atcute/uint8array@1.0.6": { "integrity": "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==" }, - "@atcute/util-fetch@1.0.4": { - "integrity": "sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==", + "@atcute/util-fetch@1.0.5": { + "integrity": "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==", "dependencies": [ "@badrap/valita" ] @@ -158,13 +158,8 @@ "os": ["aix"], "cpu": ["ppc64"] }, - "@esbuild/aix-ppc64@0.21.5": { - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "os": ["aix"], - "cpu": ["ppc64"] - }, - "@esbuild/aix-ppc64@0.25.12": { - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "@esbuild/aix-ppc64@0.27.2": { + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "os": ["aix"], "cpu": ["ppc64"] }, @@ -173,13 +168,8 @@ "os": ["android"], "cpu": ["arm64"] }, - "@esbuild/android-arm64@0.21.5": { - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "os": ["android"], - "cpu": ["arm64"] - }, - "@esbuild/android-arm64@0.25.12": { - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "@esbuild/android-arm64@0.27.2": { + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "os": ["android"], "cpu": ["arm64"] }, @@ -188,13 +178,8 @@ "os": ["android"], "cpu": ["arm"] }, - "@esbuild/android-arm@0.21.5": { - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "os": ["android"], - "cpu": ["arm"] - }, - "@esbuild/android-arm@0.25.12": { - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "@esbuild/android-arm@0.27.2": { + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "os": ["android"], "cpu": ["arm"] }, @@ -203,13 +188,8 @@ "os": ["android"], "cpu": ["x64"] }, - "@esbuild/android-x64@0.21.5": { - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "os": ["android"], - "cpu": ["x64"] - }, - "@esbuild/android-x64@0.25.12": { - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "@esbuild/android-x64@0.27.2": { + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "os": ["android"], "cpu": ["x64"] }, @@ -218,13 +198,8 @@ "os": ["darwin"], "cpu": ["arm64"] }, - "@esbuild/darwin-arm64@0.21.5": { - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@esbuild/darwin-arm64@0.25.12": { - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "@esbuild/darwin-arm64@0.27.2": { + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "os": ["darwin"], "cpu": ["arm64"] }, @@ -233,13 +208,8 @@ "os": ["darwin"], "cpu": ["x64"] }, - "@esbuild/darwin-x64@0.21.5": { - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@esbuild/darwin-x64@0.25.12": { - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "@esbuild/darwin-x64@0.27.2": { + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "os": ["darwin"], "cpu": ["x64"] }, @@ -248,13 +218,8 @@ "os": ["freebsd"], "cpu": ["arm64"] }, - "@esbuild/freebsd-arm64@0.21.5": { - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, - "@esbuild/freebsd-arm64@0.25.12": { - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "@esbuild/freebsd-arm64@0.27.2": { + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "os": ["freebsd"], "cpu": ["arm64"] }, @@ -263,13 +228,8 @@ "os": ["freebsd"], "cpu": ["x64"] }, - "@esbuild/freebsd-x64@0.21.5": { - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "os": ["freebsd"], - "cpu": ["x64"] - }, - "@esbuild/freebsd-x64@0.25.12": { - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "@esbuild/freebsd-x64@0.27.2": { + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "os": ["freebsd"], "cpu": ["x64"] }, @@ -278,13 +238,8 @@ "os": ["linux"], "cpu": ["arm64"] }, - "@esbuild/linux-arm64@0.21.5": { - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@esbuild/linux-arm64@0.25.12": { - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "@esbuild/linux-arm64@0.27.2": { + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "os": ["linux"], "cpu": ["arm64"] }, @@ -293,13 +248,8 @@ "os": ["linux"], "cpu": ["arm"] }, - "@esbuild/linux-arm@0.21.5": { - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@esbuild/linux-arm@0.25.12": { - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "@esbuild/linux-arm@0.27.2": { + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "os": ["linux"], "cpu": ["arm"] }, @@ -308,13 +258,8 @@ "os": ["linux"], "cpu": ["ia32"] }, - "@esbuild/linux-ia32@0.21.5": { - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "os": ["linux"], - "cpu": ["ia32"] - }, - "@esbuild/linux-ia32@0.25.12": { - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "@esbuild/linux-ia32@0.27.2": { + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "os": ["linux"], "cpu": ["ia32"] }, @@ -323,13 +268,8 @@ "os": ["linux"], "cpu": ["loong64"] }, - "@esbuild/linux-loong64@0.21.5": { - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "os": ["linux"], - "cpu": ["loong64"] - }, - "@esbuild/linux-loong64@0.25.12": { - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "@esbuild/linux-loong64@0.27.2": { + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "os": ["linux"], "cpu": ["loong64"] }, @@ -338,13 +278,8 @@ "os": ["linux"], "cpu": ["mips64el"] }, - "@esbuild/linux-mips64el@0.21.5": { - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "os": ["linux"], - "cpu": ["mips64el"] - }, - "@esbuild/linux-mips64el@0.25.12": { - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "@esbuild/linux-mips64el@0.27.2": { + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "os": ["linux"], "cpu": ["mips64el"] }, @@ -353,13 +288,8 @@ "os": ["linux"], "cpu": ["ppc64"] }, - "@esbuild/linux-ppc64@0.21.5": { - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@esbuild/linux-ppc64@0.25.12": { - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "@esbuild/linux-ppc64@0.27.2": { + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "os": ["linux"], "cpu": ["ppc64"] }, @@ -368,13 +298,8 @@ "os": ["linux"], "cpu": ["riscv64"] }, - "@esbuild/linux-riscv64@0.21.5": { - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@esbuild/linux-riscv64@0.25.12": { - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "@esbuild/linux-riscv64@0.27.2": { + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "os": ["linux"], "cpu": ["riscv64"] }, @@ -383,13 +308,8 @@ "os": ["linux"], "cpu": ["s390x"] }, - "@esbuild/linux-s390x@0.21.5": { - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@esbuild/linux-s390x@0.25.12": { - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "@esbuild/linux-s390x@0.27.2": { + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "os": ["linux"], "cpu": ["s390x"] }, @@ -398,18 +318,13 @@ "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/linux-x64@0.21.5": { - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "@esbuild/linux-x64@0.27.2": { + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/linux-x64@0.25.12": { - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@esbuild/netbsd-arm64@0.25.12": { - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "@esbuild/netbsd-arm64@0.27.2": { + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "os": ["netbsd"], "cpu": ["arm64"] }, @@ -418,18 +333,13 @@ "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/netbsd-x64@0.21.5": { - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "@esbuild/netbsd-x64@0.27.2": { + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/netbsd-x64@0.25.12": { - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "os": ["netbsd"], - "cpu": ["x64"] - }, - "@esbuild/openbsd-arm64@0.25.12": { - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "@esbuild/openbsd-arm64@0.27.2": { + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "os": ["openbsd"], "cpu": ["arm64"] }, @@ -438,18 +348,13 @@ "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-x64@0.21.5": { - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "@esbuild/openbsd-x64@0.27.2": { + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-x64@0.25.12": { - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "os": ["openbsd"], - "cpu": ["x64"] - }, - "@esbuild/openharmony-arm64@0.25.12": { - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "@esbuild/openharmony-arm64@0.27.2": { + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "os": ["openharmony"], "cpu": ["arm64"] }, @@ -458,13 +363,8 @@ "os": ["sunos"], "cpu": ["x64"] }, - "@esbuild/sunos-x64@0.21.5": { - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "os": ["sunos"], - "cpu": ["x64"] - }, - "@esbuild/sunos-x64@0.25.12": { - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "@esbuild/sunos-x64@0.27.2": { + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "os": ["sunos"], "cpu": ["x64"] }, @@ -473,13 +373,8 @@ "os": ["win32"], "cpu": ["arm64"] }, - "@esbuild/win32-arm64@0.21.5": { - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@esbuild/win32-arm64@0.25.12": { - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "@esbuild/win32-arm64@0.27.2": { + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "os": ["win32"], "cpu": ["arm64"] }, @@ -488,13 +383,8 @@ "os": ["win32"], "cpu": ["ia32"] }, - "@esbuild/win32-ia32@0.21.5": { - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@esbuild/win32-ia32@0.25.12": { - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "@esbuild/win32-ia32@0.27.2": { + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "os": ["win32"], "cpu": ["ia32"] }, @@ -503,13 +393,8 @@ "os": ["win32"], "cpu": ["x64"] }, - "@esbuild/win32-x64@0.21.5": { - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "os": ["win32"], - "cpu": ["x64"] - }, - "@esbuild/win32-x64@0.25.12": { - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "@esbuild/win32-x64@0.27.2": { + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "os": ["win32"], "cpu": ["x64"] }, @@ -576,119 +461,116 @@ "@jridgewell/sourcemap-codec" ] }, - "@noble/secp256k1@2.3.0": { - "integrity": "sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==" - }, "@noble/secp256k1@3.0.0": { "integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==" }, - "@rollup/rollup-android-arm-eabi@4.53.3": { - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "@rollup/rollup-android-arm-eabi@4.54.0": { + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", "os": ["android"], "cpu": ["arm"] }, - "@rollup/rollup-android-arm64@4.53.3": { - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "@rollup/rollup-android-arm64@4.54.0": { + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", "os": ["android"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-arm64@4.53.3": { - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "@rollup/rollup-darwin-arm64@4.54.0": { + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-x64@4.53.3": { - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "@rollup/rollup-darwin-x64@4.54.0": { + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", "os": ["darwin"], "cpu": ["x64"] }, - "@rollup/rollup-freebsd-arm64@4.53.3": { - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "@rollup/rollup-freebsd-arm64@4.54.0": { + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@rollup/rollup-freebsd-x64@4.53.3": { - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "@rollup/rollup-freebsd-x64@4.54.0": { + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rollup/rollup-linux-arm-gnueabihf@4.53.3": { - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "@rollup/rollup-linux-arm-gnueabihf@4.54.0": { + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm-musleabihf@4.53.3": { - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "@rollup/rollup-linux-arm-musleabihf@4.54.0": { + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm64-gnu@4.53.3": { - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "@rollup/rollup-linux-arm64-gnu@4.54.0": { + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-arm64-musl@4.53.3": { - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "@rollup/rollup-linux-arm64-musl@4.54.0": { + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-loong64-gnu@4.53.3": { - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "@rollup/rollup-linux-loong64-gnu@4.54.0": { + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", "os": ["linux"], "cpu": ["loong64"] }, - "@rollup/rollup-linux-ppc64-gnu@4.53.3": { - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "@rollup/rollup-linux-ppc64-gnu@4.54.0": { + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rollup/rollup-linux-riscv64-gnu@4.53.3": { - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "@rollup/rollup-linux-riscv64-gnu@4.54.0": { + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-riscv64-musl@4.53.3": { - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "@rollup/rollup-linux-riscv64-musl@4.54.0": { + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-s390x-gnu@4.53.3": { - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "@rollup/rollup-linux-s390x-gnu@4.54.0": { + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", "os": ["linux"], "cpu": ["s390x"] }, - "@rollup/rollup-linux-x64-gnu@4.53.3": { - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "@rollup/rollup-linux-x64-gnu@4.54.0": { + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-linux-x64-musl@4.53.3": { - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "@rollup/rollup-linux-x64-musl@4.54.0": { + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-openharmony-arm64@4.53.3": { - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "@rollup/rollup-openharmony-arm64@4.54.0": { + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-arm64-msvc@4.53.3": { - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "@rollup/rollup-win32-arm64-msvc@4.54.0": { + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", "os": ["win32"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-ia32-msvc@4.53.3": { - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "@rollup/rollup-win32-ia32-msvc@4.54.0": { + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", "os": ["win32"], "cpu": ["ia32"] }, - "@rollup/rollup-win32-x64-gnu@4.53.3": { - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "@rollup/rollup-win32-x64-gnu@4.54.0": { + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", "os": ["win32"], "cpu": ["x64"] }, - "@rollup/rollup-win32-x64-msvc@4.53.3": { - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "@rollup/rollup-win32-x64-msvc@4.54.0": { + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", "os": ["win32"], "cpu": ["x64"] }, @@ -701,25 +583,24 @@ "acorn" ] }, - "@sveltejs/vite-plugin-svelte-inspector@4.0.1_@sveltejs+vite-plugin-svelte@5.1.1__svelte@5.45.10___acorn@8.15.0__vite@6.4.1___picomatch@4.0.3_svelte@5.45.10__acorn@8.15.0_vite@6.4.1__picomatch@4.0.3": { - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "@sveltejs/vite-plugin-svelte-inspector@5.0.1_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.1___acorn@8.15.0__vite@7.3.0___picomatch@4.0.3_svelte@5.46.1__acorn@8.15.0_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", "dependencies": [ "@sveltejs/vite-plugin-svelte", "debug", "svelte", - "vite@6.4.1_picomatch@4.0.3" + "vite" ] }, - "@sveltejs/vite-plugin-svelte@5.1.1_svelte@5.45.10__acorn@8.15.0_vite@6.4.1__picomatch@4.0.3": { - "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "@sveltejs/vite-plugin-svelte@6.2.1_svelte@5.46.1__acorn@8.15.0_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dependencies": [ "@sveltejs/vite-plugin-svelte-inspector", "debug", "deepmerge", - "kleur", "magic-string", "svelte", - "vite@6.4.1_picomatch@4.0.3", + "vite", "vitefu" ] }, @@ -747,16 +628,23 @@ "redent" ] }, - "@testing-library/svelte@5.2.9_svelte@5.45.10__acorn@8.15.0_vite@6.4.1__picomatch@4.0.3_vitest@2.1.9__jsdom@25.0.1__vite@5.4.21_jsdom@25.0.1": { - "integrity": "sha512-p0Lg/vL1iEsEasXKSipvW9nBCtItQGhYvxL8OZ4w7/IDdC+LGoSJw4mMS5bndVFON/gWryitEhMr29AlO4FvBg==", + "@testing-library/svelte-core@1.0.0_svelte@5.46.1__acorn@8.15.0": { + "integrity": "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==", + "dependencies": [ + "svelte" + ] + }, + "@testing-library/svelte@5.3.1_svelte@5.46.1__acorn@8.15.0_vite@7.3.0__picomatch@4.0.3_vitest@4.0.16__jsdom@25.0.1__vite@7.3.0___picomatch@4.0.3_jsdom@25.0.1": { + "integrity": "sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w==", "dependencies": [ "@testing-library/dom", + "@testing-library/svelte-core", "svelte", - "vite@6.4.1_picomatch@4.0.3", + "vite", "vitest" ], "optionalPeers": [ - "vite@6.4.1_picomatch@4.0.3", + "vite", "vitest" ] }, @@ -769,62 +657,70 @@ "@types/aria-query@5.0.4": { "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" }, + "@types/chai@5.2.3": { + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dependencies": [ + "@types/deep-eql", + "assertion-error" + ] + }, + "@types/deep-eql@4.0.2": { + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==" + }, "@types/estree@1.0.8": { "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, - "@vitest/expect@2.1.9": { - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "@vitest/expect@4.0.16": { + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", "dependencies": [ + "@standard-schema/spec", + "@types/chai", "@vitest/spy", "@vitest/utils", "chai", "tinyrainbow" ] }, - "@vitest/mocker@2.1.9_vite@5.4.21": { - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "@vitest/mocker@4.0.16_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", "dependencies": [ "@vitest/spy", "estree-walker@3.0.3", "magic-string", - "vite@5.4.21" + "vite" ], "optionalPeers": [ - "vite@5.4.21" + "vite" ] }, - "@vitest/pretty-format@2.1.9": { - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "@vitest/pretty-format@4.0.16": { + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", "dependencies": [ "tinyrainbow" ] }, - "@vitest/runner@2.1.9": { - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "@vitest/runner@4.0.16": { + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", "dependencies": [ "@vitest/utils", "pathe" ] }, - "@vitest/snapshot@2.1.9": { - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "@vitest/snapshot@4.0.16": { + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", "dependencies": [ "@vitest/pretty-format", "magic-string", "pathe" ] }, - "@vitest/spy@2.1.9": { - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", - "dependencies": [ - "tinyspy" - ] + "@vitest/spy@4.0.16": { + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==" }, - "@vitest/utils@2.1.9": { - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "@vitest/utils@4.0.16": { + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", "dependencies": [ "@vitest/pretty-format", - "loupe", "tinyrainbow" ] }, @@ -859,9 +755,6 @@ "axobject-query@4.1.0": { "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" }, - "cac@6.7.14": { - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" - }, "call-bind-apply-helpers@1.0.2": { "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": [ @@ -869,24 +762,8 @@ "function-bind" ] }, - "chai@5.3.3": { - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dependencies": [ - "assertion-error", - "check-error", - "deep-eql", - "loupe", - "pathval" - ] - }, - "check-error@2.1.1": { - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==" - }, - "chokidar@4.0.3": { - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dependencies": [ - "readdirp" - ] + "chai@6.2.2": { + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==" }, "cli-color@2.0.4": { "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", @@ -940,9 +817,6 @@ "decimal.js@10.6.0": { "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" }, - "deep-eql@5.0.2": { - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==" - }, "deepmerge@4.3.1": { "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, @@ -1060,65 +934,35 @@ "scripts": true, "bin": true }, - "esbuild@0.21.5": { - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "esbuild@0.27.2": { + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "optionalDependencies": [ - "@esbuild/aix-ppc64@0.21.5", - "@esbuild/android-arm@0.21.5", - "@esbuild/android-arm64@0.21.5", - "@esbuild/android-x64@0.21.5", - "@esbuild/darwin-arm64@0.21.5", - "@esbuild/darwin-x64@0.21.5", - "@esbuild/freebsd-arm64@0.21.5", - "@esbuild/freebsd-x64@0.21.5", - "@esbuild/linux-arm@0.21.5", - "@esbuild/linux-arm64@0.21.5", - "@esbuild/linux-ia32@0.21.5", - "@esbuild/linux-loong64@0.21.5", - "@esbuild/linux-mips64el@0.21.5", - "@esbuild/linux-ppc64@0.21.5", - "@esbuild/linux-riscv64@0.21.5", - "@esbuild/linux-s390x@0.21.5", - "@esbuild/linux-x64@0.21.5", - "@esbuild/netbsd-x64@0.21.5", - "@esbuild/openbsd-x64@0.21.5", - "@esbuild/sunos-x64@0.21.5", - "@esbuild/win32-arm64@0.21.5", - "@esbuild/win32-ia32@0.21.5", - "@esbuild/win32-x64@0.21.5" - ], - "scripts": true, - "bin": true - }, - "esbuild@0.25.12": { - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "optionalDependencies": [ - "@esbuild/aix-ppc64@0.25.12", - "@esbuild/android-arm@0.25.12", - "@esbuild/android-arm64@0.25.12", - "@esbuild/android-x64@0.25.12", - "@esbuild/darwin-arm64@0.25.12", - "@esbuild/darwin-x64@0.25.12", - "@esbuild/freebsd-arm64@0.25.12", - "@esbuild/freebsd-x64@0.25.12", - "@esbuild/linux-arm@0.25.12", - "@esbuild/linux-arm64@0.25.12", - "@esbuild/linux-ia32@0.25.12", - "@esbuild/linux-loong64@0.25.12", - "@esbuild/linux-mips64el@0.25.12", - "@esbuild/linux-ppc64@0.25.12", - "@esbuild/linux-riscv64@0.25.12", - "@esbuild/linux-s390x@0.25.12", - "@esbuild/linux-x64@0.25.12", + "@esbuild/aix-ppc64@0.27.2", + "@esbuild/android-arm@0.27.2", + "@esbuild/android-arm64@0.27.2", + "@esbuild/android-x64@0.27.2", + "@esbuild/darwin-arm64@0.27.2", + "@esbuild/darwin-x64@0.27.2", + "@esbuild/freebsd-arm64@0.27.2", + "@esbuild/freebsd-x64@0.27.2", + "@esbuild/linux-arm@0.27.2", + "@esbuild/linux-arm64@0.27.2", + "@esbuild/linux-ia32@0.27.2", + "@esbuild/linux-loong64@0.27.2", + "@esbuild/linux-mips64el@0.27.2", + "@esbuild/linux-ppc64@0.27.2", + "@esbuild/linux-riscv64@0.27.2", + "@esbuild/linux-s390x@0.27.2", + "@esbuild/linux-x64@0.27.2", "@esbuild/netbsd-arm64", - "@esbuild/netbsd-x64@0.25.12", + "@esbuild/netbsd-x64@0.27.2", "@esbuild/openbsd-arm64", - "@esbuild/openbsd-x64@0.25.12", + "@esbuild/openbsd-x64@0.27.2", "@esbuild/openharmony-arm64", - "@esbuild/sunos-x64@0.25.12", - "@esbuild/win32-arm64@0.25.12", - "@esbuild/win32-ia32@0.25.12", - "@esbuild/win32-x64@0.25.12" + "@esbuild/sunos-x64@0.27.2", + "@esbuild/win32-arm64@0.27.2", + "@esbuild/win32-ia32@0.27.2", + "@esbuild/win32-x64@0.27.2" ], "scripts": true, "bin": true @@ -1318,15 +1162,9 @@ "xml-name-validator" ] }, - "kleur@4.1.5": { - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" - }, "locate-character@3.0.0": { "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, - "loupe@3.2.1": { - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==" - }, "lru-cache@10.4.3": { "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, @@ -1393,17 +1231,17 @@ "nwsapi@2.2.23": { "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==" }, + "obug@2.1.1": { + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==" + }, "parse5@7.3.0": { "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dependencies": [ "entities" ] }, - "pathe@1.1.2": { - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" - }, - "pathval@2.0.1": { - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==" + "pathe@2.0.3": { + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" }, "picocolors@1.1.1": { "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" @@ -1433,9 +1271,6 @@ "react-is@17.0.2": { "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "readdirp@4.1.2": { - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" - }, "redent@3.0.0": { "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dependencies": [ @@ -1443,8 +1278,8 @@ "strip-indent" ] }, - "rollup@4.53.3": { - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "rollup@4.54.0": { + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dependencies": [ "@types/estree" ], @@ -1514,20 +1349,7 @@ "min-indent" ] }, - "svelte-check@4.3.5_svelte@5.45.10__acorn@8.15.0_typescript@5.9.3": { - "integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==", - "dependencies": [ - "@jridgewell/trace-mapping", - "chokidar", - "fdir", - "picocolors", - "sade", - "svelte", - "typescript" - ], - "bin": true - }, - "svelte-i18n@4.0.1_svelte@5.45.10__acorn@8.15.0": { + "svelte-i18n@4.0.1_svelte@5.46.1__acorn@8.15.0": { "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==", "dependencies": [ "cli-color", @@ -1541,8 +1363,8 @@ ], "bin": true }, - "svelte@5.45.10_acorn@8.15.0": { - "integrity": "sha512-GiWXq6akkEN3zVDMQ1BVlRolmks5JkEdzD/67mvXOz6drRfuddT5JwsGZjMGSnsTRv/PjAXX8fqBcOr2g2qc/Q==", + "svelte@5.46.1_acorn@8.15.0": { + "integrity": "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==", "dependencies": [ "@jridgewell/remapping", "@jridgewell/sourcemap-codec", @@ -1581,8 +1403,8 @@ "tinybench@2.9.0": { "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==" }, - "tinyexec@0.3.2": { - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" + "tinyexec@1.0.2": { + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==" }, "tinyglobby@0.2.15_picomatch@4.0.3": { "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", @@ -1591,14 +1413,8 @@ "picomatch" ] }, - "tinypool@1.1.1": { - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==" - }, - "tinyrainbow@1.2.0": { - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==" - }, - "tinyspy@3.0.2": { - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==" + "tinyrainbow@3.0.3": { + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==" }, "tldts-core@6.1.86": { "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==" @@ -1628,40 +1444,13 @@ "type@2.7.3": { "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" }, - "typescript@5.9.3": { - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "bin": true + "unicode-segmenter@0.14.5": { + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==" }, - "unicode-segmenter@0.14.4": { - "integrity": "sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==" - }, - "vite-node@2.1.9": { - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "vite@7.3.0_picomatch@4.0.3": { + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dependencies": [ - "cac", - "debug", - "es-module-lexer", - "pathe", - "vite@5.4.21" - ], - "bin": true - }, - "vite@5.4.21": { - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dependencies": [ - "esbuild@0.21.5", - "postcss", - "rollup" - ], - "optionalDependencies": [ - "fsevents" - ], - "bin": true - }, - "vite@6.4.1_picomatch@4.0.3": { - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dependencies": [ - "esbuild@0.25.12", + "esbuild@0.27.2", "fdir", "picomatch", "postcss", @@ -1673,17 +1462,17 @@ ], "bin": true }, - "vitefu@1.1.1_vite@6.4.1__picomatch@4.0.3": { + "vitefu@1.1.1_vite@7.3.0__picomatch@4.0.3": { "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dependencies": [ - "vite@6.4.1_picomatch@4.0.3" + "vite" ], "optionalPeers": [ - "vite@6.4.1_picomatch@4.0.3" + "vite" ] }, - "vitest@2.1.9_jsdom@25.0.1_vite@5.4.21": { - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "vitest@4.0.16_jsdom@25.0.1_vite@7.3.0__picomatch@4.0.3": { + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dependencies": [ "@vitest/expect", "@vitest/mocker", @@ -1692,19 +1481,19 @@ "@vitest/snapshot", "@vitest/spy", "@vitest/utils", - "chai", - "debug", + "es-module-lexer", "expect-type", "jsdom", "magic-string", + "obug", "pathe", + "picomatch", "std-env", "tinybench", "tinyexec", - "tinypool", + "tinyglobby", "tinyrainbow", - "vite@5.4.21", - "vite-node", + "vite", "why-is-node-running" ], "optionalPeers": [ @@ -1725,7 +1514,8 @@ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dependencies": [ "iconv-lite" - ] + ], + "deprecated": true }, "whatwg-mimetype@4.0.0": { "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==" @@ -1756,6 +1546,9 @@ }, "zimmerframe@1.1.4": { "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==" + }, + "zod@4.3.5": { + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==" } }, "workspace": { @@ -1765,17 +1558,18 @@ "npm:@atcute/crypto@^2.3.0", "npm:@atcute/did-plc@~0.3.1", "npm:@atcute/multibase@^1.1.6", - "npm:@noble/secp256k1@^2.1.0", - "npm:@sveltejs/vite-plugin-svelte@5", - "npm:@testing-library/jest-dom@^6.6.3", - "npm:@testing-library/svelte@^5.2.6", - "npm:@testing-library/user-event@^14.5.2", + "npm:@noble/secp256k1@3", + "npm:@sveltejs/vite-plugin-svelte@^6.2.1", + "npm:@testing-library/jest-dom@^6.9.1", + "npm:@testing-library/svelte@^5.3.1", + "npm:@testing-library/user-event@^14.6.1", "npm:jsdom@^25.0.1", - "npm:multiformats@^13.3.1", + "npm:multiformats@^13.4.2", "npm:svelte-i18n@^4.0.1", - "npm:svelte@5", - "npm:vite@6", - "npm:vitest@^2.1.8" + "npm:svelte@^5.46.1", + "npm:vite@^7.3.0", + "npm:vitest@^4.0.16", + "npm:zod@^4.3.5" ] } } diff --git a/frontend/package.json b/frontend/package.json index 875a16e..d0aa605 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,18 +16,19 @@ "@atcute/crypto": "^2.3.0", "@atcute/did-plc": "^0.3.1", "@atcute/multibase": "^1.1.6", - "@noble/secp256k1": "^2.1.0", - "multiformats": "^13.3.1", - "svelte-i18n": "^4.0.1" + "@noble/secp256k1": "^3.0.0", + "multiformats": "^13.4.2", + "svelte-i18n": "^4.0.1", + "zod": "^4.3.5" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/svelte": "^5.2.6", - "@testing-library/user-event": "^14.5.2", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/svelte": "^5.3.1", + "@testing-library/user-event": "^14.6.1", "jsdom": "^25.0.1", - "svelte": "^5.0.0", - "vite": "^6.0.0", - "vitest": "^2.1.8" + "svelte": "^5.46.1", + "vite": "^7.3.0", + "vitest": "^4.0.16" } } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 5bac107..1adaa5a 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -4,6 +4,7 @@ import { initServerConfig } from './lib/serverConfig.svelte' import { initI18n } from './lib/i18n' import { isLoading as i18nLoading } from 'svelte-i18n' + import Toast from './components/Toast.svelte' import Login from './routes/Login.svelte' import Register from './routes/Register.svelte' import RegisterPasskey from './routes/RegisterPasskey.svelte' @@ -36,7 +37,7 @@ import DidDocumentEditor from './routes/DidDocumentEditor.svelte' initI18n() - const auth = getAuthState() + const auth = $derived(getAuthState()) let oauthCallbackPending = $state(hasOAuthCallback()) @@ -59,10 +60,10 @@ }) $effect(() => { - if (auth.loading) return + if (auth.kind === 'loading') return const path = getCurrentPath() if (path === '/') { - if (auth.session) { + if (auth.kind === 'authenticated') { navigate('/dashboard', true) } else { navigate('/login', true) @@ -142,14 +143,13 @@
- {#if auth.loading || $i18nLoading || oauthCallbackPending} -
-

Loading...

-
+ {#if auth.kind === 'loading' || $i18nLoading || oauthCallbackPending} +
{:else} {/if}
+ diff --git a/frontend/src/components/ReauthModal.svelte b/frontend/src/components/ReauthModal.svelte index 56e91aa..e0a27ae 100644 --- a/frontend/src/components/ReauthModal.svelte +++ b/frontend/src/components/ReauthModal.svelte @@ -2,6 +2,12 @@ import { getAuthState, getValidToken } from '../lib/auth.svelte' import { api, ApiError } from '../lib/api' import { _ } from '../lib/i18n' + import type { Session } from '../lib/types/api' + import { + prepareRequestOptions, + serializeAssertionResponse, + type WebAuthnRequestOptionsResponse, + } from '../lib/webauthn' interface Props { show: boolean @@ -12,7 +18,13 @@ let { show = $bindable(), availableMethods = ['password'], onSuccess, onCancel }: Props = $props() - const auth = getAuthState() + const auth = $derived(getAuthState()) + + function getSession(): Session | null { + return auth.kind === 'authenticated' ? auth.session : null + } + + const session = $derived(getSession()) let activeMethod = $state<'password' | 'totp' | 'passkey'>('password') let password = $state('') let totpCode = $state('') @@ -37,40 +49,9 @@ } }) - function arrayBufferToBase64Url(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer) - let binary = '' - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]) - } - return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') - } - - function base64UrlToArrayBuffer(base64url: string): ArrayBuffer { - const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/') - const padded = base64 + '='.repeat((4 - base64.length % 4) % 4) - const binary = atob(padded) - const bytes = new Uint8Array(binary.length) - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i) - } - return bytes.buffer - } - - function prepareAuthOptions(options: any): PublicKeyCredentialRequestOptions { - return { - ...options.publicKey, - challenge: base64UrlToArrayBuffer(options.publicKey.challenge), - allowCredentials: options.publicKey.allowCredentials?.map((cred: any) => ({ - ...cred, - id: base64UrlToArrayBuffer(cred.id) - })) || [] - } - } - async function handlePasswordSubmit(e: Event) { e.preventDefault() - if (!auth.session || !password) return + if (!session || !password) return loading = true error = '' try { @@ -91,7 +72,7 @@ async function handleTotpSubmit(e: Event) { e.preventDefault() - if (!auth.session || !totpCode) return + if (!session || !totpCode) return loading = true error = '' try { @@ -111,7 +92,7 @@ } async function handlePasskeyAuth() { - if (!auth.session) return + if (!session) return if (!window.PublicKeyCredential) { error = 'Passkeys are not supported in this browser' return @@ -125,7 +106,7 @@ return } const { options } = await api.reauthPasskeyStart(token) - const publicKeyOptions = prepareAuthOptions(options) + const publicKeyOptions = prepareRequestOptions(options as WebAuthnRequestOptionsResponse) const credential = await navigator.credentials.get({ publicKey: publicKeyOptions }) @@ -133,19 +114,7 @@ error = 'Passkey authentication was cancelled' return } - const pkCredential = credential as PublicKeyCredential - const response = pkCredential.response as AuthenticatorAssertionResponse - const credentialResponse = { - id: pkCredential.id, - type: pkCredential.type, - rawId: arrayBufferToBase64Url(pkCredential.rawId), - response: { - clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON), - authenticatorData: arrayBufferToBase64Url(response.authenticatorData), - signature: arrayBufferToBase64Url(response.signature), - userHandle: response.userHandle ? arrayBufferToBase64Url(response.userHandle) : null, - }, - } + const credentialResponse = serializeAssertionResponse(credential as PublicKeyCredential) await api.reauthPasskeyFinish(token, credentialResponse) show = false onSuccess() diff --git a/frontend/src/components/Skeleton.svelte b/frontend/src/components/Skeleton.svelte new file mode 100644 index 0000000..4fbb16e --- /dev/null +++ b/frontend/src/components/Skeleton.svelte @@ -0,0 +1,77 @@ + + +{#if variant === 'card'} +
+
+
+
+
+ {#each Array(lines) as _} +
+ {/each} +
+
+{:else if variant === 'circle'} +
+{:else} + {#each Array(lines) as _, i} +
+ {/each} +{/if} + + diff --git a/frontend/src/components/Toast.svelte b/frontend/src/components/Toast.svelte new file mode 100644 index 0000000..ab77c00 --- /dev/null +++ b/frontend/src/components/Toast.svelte @@ -0,0 +1,188 @@ + + +{#if toasts.length > 0} +
+ {#each toasts as toast (toast.id)} + + {/each} +
+{/if} + + diff --git a/frontend/src/lib/api-validated.ts b/frontend/src/lib/api-validated.ts new file mode 100644 index 0000000..827eff8 --- /dev/null +++ b/frontend/src/lib/api-validated.ts @@ -0,0 +1,345 @@ +import { z } from 'zod' +import { ok, err, type Result } from './types/result' +import { ApiError } from './api' +import type { AccessToken, RefreshToken, Did, Handle, Nsid, Rkey } from './types/branded' +import { + sessionSchema, + serverDescriptionSchema, + appPasswordSchema, + createdAppPasswordSchema, + listSessionsResponseSchema, + totpStatusSchema, + totpSecretSchema, + enableTotpResponseSchema, + listPasskeysResponseSchema, + listTrustedDevicesResponseSchema, + reauthStatusSchema, + notificationPrefsSchema, + didDocumentSchema, + repoDescriptionSchema, + listRecordsResponseSchema, + recordResponseSchema, + createRecordResponseSchema, + serverStatsSchema, + serverConfigSchema, + passwordStatusSchema, + successResponseSchema, + legacyLoginPreferenceSchema, + accountInfoSchema, + searchAccountsResponseSchema, + listBackupsResponseSchema, + createBackupResponseSchema, + type ValidatedSession, + type ValidatedServerDescription, + type ValidatedListSessionsResponse, + type ValidatedTotpStatus, + type ValidatedTotpSecret, + type ValidatedEnableTotpResponse, + type ValidatedListPasskeysResponse, + type ValidatedListTrustedDevicesResponse, + type ValidatedReauthStatus, + type ValidatedNotificationPrefs, + type ValidatedDidDocument, + type ValidatedRepoDescription, + type ValidatedListRecordsResponse, + type ValidatedRecordResponse, + type ValidatedCreateRecordResponse, + type ValidatedServerStats, + type ValidatedServerConfig, + type ValidatedPasswordStatus, + type ValidatedSuccessResponse, + type ValidatedLegacyLoginPreference, + type ValidatedAccountInfo, + type ValidatedSearchAccountsResponse, + type ValidatedListBackupsResponse, + type ValidatedCreateBackupResponse, + type ValidatedCreatedAppPassword, + type ValidatedAppPassword, +} from './types/schemas' + +const API_BASE = '/xrpc' + +interface XrpcOptions { + method?: 'GET' | 'POST' + params?: Record + body?: unknown + token?: string +} + +class ValidationError extends Error { + constructor( + public issues: z.ZodIssue[], + message: string = 'API response validation failed' + ) { + super(message) + this.name = 'ValidationError' + } +} + +async function xrpcValidated( + method: string, + schema: z.ZodType, + options?: XrpcOptions +): Promise> { + const { method: httpMethod = 'GET', params, body, token } = options ?? {} + let url = `${API_BASE}/${method}` + if (params) { + const searchParams = new URLSearchParams(params) + url += `?${searchParams}` + } + const headers: Record = {} + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + if (body) { + headers['Content-Type'] = 'application/json' + } + + try { + const res = await fetch(url, { + method: httpMethod, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + + if (!res.ok) { + const errData = await res.json().catch(() => ({ + error: 'Unknown', + message: res.statusText, + })) + return err(new ApiError(res.status, errData.error, errData.message)) + } + + const data = await res.json() + const parsed = schema.safeParse(data) + + if (!parsed.success) { + return err(new ValidationError(parsed.error.issues)) + } + + return ok(parsed.data) + } catch (e) { + if (e instanceof ApiError || e instanceof ValidationError) { + return err(e) + } + return err(new ApiError(0, 'Unknown', e instanceof Error ? e.message : String(e))) + } +} + +export const validatedApi = { + getSession(token: AccessToken): Promise> { + return xrpcValidated('com.atproto.server.getSession', sessionSchema, { token }) + }, + + refreshSession(refreshJwt: RefreshToken): Promise> { + return xrpcValidated('com.atproto.server.refreshSession', sessionSchema, { + method: 'POST', + token: refreshJwt, + }) + }, + + createSession( + identifier: string, + password: string + ): Promise> { + return xrpcValidated('com.atproto.server.createSession', sessionSchema, { + method: 'POST', + body: { identifier, password }, + }) + }, + + describeServer(): Promise> { + return xrpcValidated('com.atproto.server.describeServer', serverDescriptionSchema) + }, + + listAppPasswords( + token: AccessToken + ): Promise> { + return xrpcValidated( + 'com.atproto.server.listAppPasswords', + z.object({ passwords: z.array(appPasswordSchema) }), + { token } + ) + }, + + createAppPassword( + token: AccessToken, + name: string, + scopes?: string + ): Promise> { + return xrpcValidated('com.atproto.server.createAppPassword', createdAppPasswordSchema, { + method: 'POST', + token, + body: { name, scopes }, + }) + }, + + listSessions(token: AccessToken): Promise> { + return xrpcValidated('_account.listSessions', listSessionsResponseSchema, { token }) + }, + + getTotpStatus(token: AccessToken): Promise> { + return xrpcValidated('com.atproto.server.getTotpStatus', totpStatusSchema, { token }) + }, + + createTotpSecret(token: AccessToken): Promise> { + return xrpcValidated('com.atproto.server.createTotpSecret', totpSecretSchema, { + method: 'POST', + token, + }) + }, + + enableTotp( + token: AccessToken, + code: string + ): Promise> { + return xrpcValidated('com.atproto.server.enableTotp', enableTotpResponseSchema, { + method: 'POST', + token, + body: { code }, + }) + }, + + listPasskeys(token: AccessToken): Promise> { + return xrpcValidated('com.atproto.server.listPasskeys', listPasskeysResponseSchema, { token }) + }, + + listTrustedDevices( + token: AccessToken + ): Promise> { + return xrpcValidated('_account.listTrustedDevices', listTrustedDevicesResponseSchema, { token }) + }, + + getReauthStatus(token: AccessToken): Promise> { + return xrpcValidated('_account.getReauthStatus', reauthStatusSchema, { token }) + }, + + getNotificationPrefs( + token: AccessToken + ): Promise> { + return xrpcValidated('_account.getNotificationPrefs', notificationPrefsSchema, { token }) + }, + + getDidDocument(token: AccessToken): Promise> { + return xrpcValidated('_account.getDidDocument', didDocumentSchema, { token }) + }, + + describeRepo( + token: AccessToken, + repo: Did + ): Promise> { + return xrpcValidated('com.atproto.repo.describeRepo', repoDescriptionSchema, { + token, + params: { repo }, + }) + }, + + listRecords( + token: AccessToken, + repo: Did, + collection: Nsid, + options?: { limit?: number; cursor?: string; reverse?: boolean } + ): Promise> { + const params: Record = { repo, collection } + if (options?.limit) params.limit = String(options.limit) + if (options?.cursor) params.cursor = options.cursor + if (options?.reverse) params.reverse = 'true' + return xrpcValidated('com.atproto.repo.listRecords', listRecordsResponseSchema, { + token, + params, + }) + }, + + getRecord( + token: AccessToken, + repo: Did, + collection: Nsid, + rkey: Rkey + ): Promise> { + return xrpcValidated('com.atproto.repo.getRecord', recordResponseSchema, { + token, + params: { repo, collection, rkey }, + }) + }, + + createRecord( + token: AccessToken, + repo: Did, + collection: Nsid, + record: unknown, + rkey?: Rkey + ): Promise> { + return xrpcValidated('com.atproto.repo.createRecord', createRecordResponseSchema, { + method: 'POST', + token, + body: { repo, collection, record, rkey }, + }) + }, + + getServerStats(token: AccessToken): Promise> { + return xrpcValidated('_admin.getServerStats', serverStatsSchema, { token }) + }, + + getServerConfig(): Promise> { + return xrpcValidated('_server.getConfig', serverConfigSchema) + }, + + getPasswordStatus(token: AccessToken): Promise> { + return xrpcValidated('_account.getPasswordStatus', passwordStatusSchema, { token }) + }, + + changePassword( + token: AccessToken, + currentPassword: string, + newPassword: string + ): Promise> { + return xrpcValidated('_account.changePassword', successResponseSchema, { + method: 'POST', + token, + body: { currentPassword, newPassword }, + }) + }, + + getLegacyLoginPreference( + token: AccessToken + ): Promise> { + return xrpcValidated('_account.getLegacyLoginPreference', legacyLoginPreferenceSchema, { token }) + }, + + getAccountInfo( + token: AccessToken, + did: Did + ): Promise> { + return xrpcValidated('com.atproto.admin.getAccountInfo', accountInfoSchema, { + token, + params: { did }, + }) + }, + + searchAccounts( + token: AccessToken, + options?: { handle?: string; cursor?: string; limit?: number } + ): Promise> { + const params: Record = {} + if (options?.handle) params.handle = options.handle + if (options?.cursor) params.cursor = options.cursor + if (options?.limit) params.limit = String(options.limit) + return xrpcValidated('com.atproto.admin.searchAccounts', searchAccountsResponseSchema, { + token, + params, + }) + }, + + listBackups(token: AccessToken): Promise> { + return xrpcValidated('_backup.listBackups', listBackupsResponseSchema, { token }) + }, + + createBackup(token: AccessToken): Promise> { + return xrpcValidated('_backup.createBackup', createBackupResponseSchema, { + method: 'POST', + token, + }) + }, +} + +export { ValidationError } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 3cf10c8..339ba8f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,172 +1,207 @@ -const API_BASE = "/xrpc"; +import { ok, err, type Result } from './types/result' +import type { + Did, + Handle, + AccessToken, + RefreshToken, + Cid, + Rkey, + AtUri, + Nsid, + ISODateString, + EmailAddress, + InviteCode as InviteCodeBrand, +} from './types/branded' +import { + unsafeAsDid, + unsafeAsHandle, + unsafeAsAccessToken, + unsafeAsRefreshToken, + unsafeAsCid, + unsafeAsISODate, + unsafeAsEmail, + unsafeAsInviteCode, +} from './types/branded' +import type { + Session, + DidDocument, + AppPassword, + CreatedAppPassword, + InviteCodeInfo, + ServerDescription, + NotificationPrefs, + NotificationHistoryResponse, + ServerStats, + ServerConfig, + UploadBlobResponse, + ListSessionsResponse, + SearchAccountsResponse, + GetInviteCodesResponse, + AccountInfo, + RepoDescription, + ListRecordsResponse, + RecordResponse, + CreateRecordResponse, + TotpStatus, + TotpSecret, + EnableTotpResponse, + RegenerateBackupCodesResponse, + ListPasskeysResponse, + StartPasskeyRegistrationResponse, + FinishPasskeyRegistrationResponse, + ListTrustedDevicesResponse, + ReauthStatus, + ReauthResponse, + ReauthPasskeyStartResponse, + ReserveSigningKeyResponse, + RecommendedDidCredentials, + PasskeyAccountCreateResponse, + CompletePasskeySetupResponse, + VerifyTokenResponse, + ListBackupsResponse, + CreateBackupResponse, + SetBackupEnabledResponse, + EmailUpdateResponse, + LegacyLoginPreference, + UpdateLegacyLoginResponse, + UpdateLocaleResponse, + PasswordStatus, + SuccessResponse, + CheckEmailVerifiedResponse, + VerifyMigrationEmailResponse, + ResendMigrationVerificationResponse, + ListReposResponse, + VerificationChannel, + DidType, + ApiErrorCode, + VerificationMethod as VerificationMethodType, + CreateAccountParams, + CreateAccountResult, + ConfirmSignupResult, +} from './types/api' + +const API_BASE = '/xrpc' export class ApiError extends Error { - public did?: string; - public reauthMethods?: string[]; + public did?: Did + public reauthMethods?: string[] constructor( public status: number, - public error: string, + public error: ApiErrorCode, message: string, did?: string, reauthMethods?: string[], ) { - super(message); - this.name = "ApiError"; - this.did = did; - this.reauthMethods = reauthMethods; + super(message) + this.name = 'ApiError' + this.did = did ? unsafeAsDid(did) : undefined + this.reauthMethods = reauthMethods } } -let tokenRefreshCallback: (() => Promise) | null = null; +let tokenRefreshCallback: (() => Promise) | null = null export function setTokenRefreshCallback( callback: () => Promise, ) { - tokenRefreshCallback = callback; + tokenRefreshCallback = callback } -async function xrpc(method: string, options?: { - method?: "GET" | "POST"; - params?: Record; - body?: unknown; - token?: string; - skipRetry?: boolean; -}): Promise { - const { method: httpMethod = "GET", params, body, token, skipRetry } = - options ?? {}; - let url = `${API_BASE}/${method}`; +interface XrpcOptions { + method?: 'GET' | 'POST' + params?: Record + body?: unknown + token?: string + skipRetry?: boolean +} + +async function xrpc(method: string, options?: XrpcOptions): Promise { + const { method: httpMethod = 'GET', params, body, token, skipRetry } = + options ?? {} + let url = `${API_BASE}/${method}` if (params) { - const searchParams = new URLSearchParams(params); - url += `?${searchParams}`; + const searchParams = new URLSearchParams(params) + url += `?${searchParams}` } - const headers: Record = {}; + const headers: Record = {} if (token) { - headers["Authorization"] = `Bearer ${token}`; + headers['Authorization'] = `Bearer ${token}` } if (body) { - headers["Content-Type"] = "application/json"; + headers['Content-Type'] = 'application/json' } const res = await fetch(url, { method: httpMethod, headers, body: body ? JSON.stringify(body) : undefined, - }); + }) if (!res.ok) { - const err = await res.json().catch(() => ({ - error: "Unknown", + const errData = await res.json().catch(() => ({ + error: 'Unknown', message: res.statusText, - })); + })) if ( res.status === 401 && - (err.error === "AuthenticationFailed" || err.error === "ExpiredToken") && + (errData.error === 'AuthenticationFailed' || errData.error === 'ExpiredToken') && token && tokenRefreshCallback && !skipRetry ) { - const newToken = await tokenRefreshCallback(); + const newToken = await tokenRefreshCallback() if (newToken && newToken !== token) { - return xrpc(method, { ...options, token: newToken, skipRetry: true }); + return xrpc(method, { ...options, token: newToken, skipRetry: true }) } } throw new ApiError( res.status, - err.error, - err.message, - err.did, - err.reauthMethods, - ); + errData.error as ApiErrorCode, + errData.message, + errData.did, + errData.reauthMethods, + ) } - return res.json(); + return res.json() } -export interface Session { - did: string; - handle: string; - email?: string; - emailConfirmed?: boolean; - preferredChannel?: string; - preferredChannelVerified?: boolean; - isAdmin?: boolean; - active?: boolean; - status?: "active" | "deactivated" | "migrated"; - migratedToPds?: string; - migratedAt?: string; - accessJwt: string; - refreshJwt: string; +async function xrpcResult( + method: string, + options?: XrpcOptions +): Promise> { + try { + const value = await xrpc(method, options) + return ok(value) + } catch (e) { + if (e instanceof ApiError) { + return err(e) + } + return err(new ApiError(0, 'Unknown', e instanceof Error ? e.message : String(e))) + } } export interface VerificationMethod { - id: string; - type: string; - publicKeyMultibase: string; + id: string + type: string + publicKeyMultibase: string } -export interface DidDocument { - "@context": string[]; - id: string; - alsoKnownAs: string[]; - verificationMethod: Array<{ - id: string; - type: string; - controller: string; - publicKeyMultibase: string; - }>; - service: Array<{ - id: string; - type: string; - serviceEndpoint: string; - }>; -} +export type { Session, DidDocument, AppPassword, InviteCodeInfo as InviteCode } +export type { VerificationChannel, DidType, CreateAccountParams, CreateAccountResult, ConfirmSignupResult } -export interface AppPassword { - name: string; - createdAt: string; - scopes?: string; - createdByController?: string; -} - -export interface InviteCode { - code: string; - available: number; - disabled: boolean; - forAccount: string; - createdBy: string; - createdAt: string; - uses: { usedBy: string; usedByHandle?: string; usedAt: string }[]; -} - -export type VerificationChannel = "email" | "discord" | "telegram" | "signal"; - -export type DidType = "plc" | "web" | "web-external"; - -export interface CreateAccountParams { - handle: string; - email: string; - password: string; - inviteCode?: string; - didType?: DidType; - did?: string; - signingKey?: string; - verificationChannel?: VerificationChannel; - discordId?: string; - telegramUsername?: string; - signalNumber?: string; -} - -export interface CreateAccountResult { - handle: string; - did: string; - verificationRequired: boolean; - verificationChannel: string; -} - -export interface ConfirmSignupResult { - accessJwt: string; - refreshJwt: string; - handle: string; - did: string; - email?: string; - emailConfirmed?: boolean; - preferredChannel?: string; - preferredChannelVerified?: boolean; +function castSession(raw: unknown): Session { + const s = raw as Record + return { + did: unsafeAsDid(s.did as string), + handle: unsafeAsHandle(s.handle as string), + email: s.email ? unsafeAsEmail(s.email as string) : undefined, + emailConfirmed: s.emailConfirmed as boolean | undefined, + preferredChannel: s.preferredChannel as VerificationChannel | undefined, + preferredChannelVerified: s.preferredChannelVerified as boolean | undefined, + isAdmin: s.isAdmin as boolean | undefined, + active: s.active as boolean | undefined, + status: s.status as Session['status'], + migratedToPds: s.migratedToPds as string | undefined, + migratedAt: s.migratedAt ? unsafeAsISODate(s.migratedAt as string) : undefined, + accessJwt: unsafeAsAccessToken(s.accessJwt as string), + refreshJwt: unsafeAsRefreshToken(s.refreshJwt as string), + } } export const api = { @@ -174,15 +209,15 @@ export const api = { params: CreateAccountParams, byodToken?: string, ): Promise { - const url = `${API_BASE}/com.atproto.server.createAccount`; + const url = `${API_BASE}/com.atproto.server.createAccount` const headers: Record = { - "Content-Type": "application/json", - }; + 'Content-Type': 'application/json', + } if (byodToken) { - headers["Authorization"] = `Bearer ${byodToken}`; + headers['Authorization'] = `Bearer ${byodToken}` } const response = await fetch(url, { - method: "POST", + method: 'POST', headers, body: JSON.stringify({ handle: params.handle, @@ -197,30 +232,30 @@ export const api = { telegramUsername: params.telegramUsername, signalNumber: params.signalNumber, }), - }); - const data = await response.json(); + }) + const data = await response.json() if (!response.ok) { - throw new ApiError(response.status, data.error, data.message); + throw new ApiError(response.status, data.error, data.message) } - return data; + return data }, async createAccountWithServiceAuth( serviceAuthToken: string, params: { - did: string; - handle: string; - email: string; - password: string; - inviteCode?: string; + did: Did + handle: Handle + email: EmailAddress + password: string + inviteCode?: string }, ): Promise { - const url = `${API_BASE}/com.atproto.server.createAccount`; + const url = `${API_BASE}/com.atproto.server.createAccount` const response = await fetch(url, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${serviceAuthToken}`, + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${serviceAuthToken}`, }, body: JSON.stringify({ did: params.did, @@ -229,1023 +264,1387 @@ export const api = { password: params.password, inviteCode: params.inviteCode, }), - }); - const data = await response.json(); + }) + const data = await response.json() if (!response.ok) { - throw new ApiError(response.status, data.error, data.message); + throw new ApiError(response.status, data.error, data.message) } - return data; + return castSession(data) }, confirmSignup( - did: string, + did: Did, verificationCode: string, ): Promise { - return xrpc("com.atproto.server.confirmSignup", { - method: "POST", + return xrpc('com.atproto.server.confirmSignup', { + method: 'POST', body: { did, verificationCode }, - }); + }) }, - resendVerification(did: string): Promise<{ success: boolean }> { - return xrpc("com.atproto.server.resendVerification", { - method: "POST", + resendVerification(did: Did): Promise<{ success: boolean }> { + return xrpc('com.atproto.server.resendVerification', { + method: 'POST', body: { did }, - }); + }) }, - createSession(identifier: string, password: string): Promise { - return xrpc("com.atproto.server.createSession", { - method: "POST", + async createSession(identifier: string, password: string): Promise { + const raw = await xrpc('com.atproto.server.createSession', { + method: 'POST', body: { identifier, password }, - }); + }) + return castSession(raw) }, checkEmailVerified(identifier: string): Promise<{ verified: boolean }> { - return xrpc("_checkEmailVerified", { - method: "POST", + return xrpc('_checkEmailVerified', { + method: 'POST', body: { identifier }, - }); + }) }, - getSession(token: string): Promise { - return xrpc("com.atproto.server.getSession", { token }); + async getSession(token: AccessToken): Promise { + const raw = await xrpc('com.atproto.server.getSession', { token }) + return castSession(raw) }, - refreshSession(refreshJwt: string): Promise { - return xrpc("com.atproto.server.refreshSession", { - method: "POST", + async refreshSession(refreshJwt: RefreshToken): Promise { + const raw = await xrpc('com.atproto.server.refreshSession', { + method: 'POST', token: refreshJwt, - }); + }) + return castSession(raw) }, - async deleteSession(token: string): Promise { - await xrpc("com.atproto.server.deleteSession", { - method: "POST", + async deleteSession(token: AccessToken): Promise { + await xrpc('com.atproto.server.deleteSession', { + method: 'POST', token, - }); + }) }, - listAppPasswords(token: string): Promise<{ passwords: AppPassword[] }> { - return xrpc("com.atproto.server.listAppPasswords", { token }); + listAppPasswords(token: AccessToken): Promise<{ passwords: AppPassword[] }> { + return xrpc('com.atproto.server.listAppPasswords', { token }) }, createAppPassword( - token: string, + token: AccessToken, name: string, scopes?: string, - ): Promise< - { name: string; password: string; createdAt: string; scopes?: string } - > { - return xrpc("com.atproto.server.createAppPassword", { - method: "POST", + ): Promise { + return xrpc('com.atproto.server.createAppPassword', { + method: 'POST', token, body: { name, scopes }, - }); + }) }, - async revokeAppPassword(token: string, name: string): Promise { - await xrpc("com.atproto.server.revokeAppPassword", { - method: "POST", + async revokeAppPassword(token: AccessToken, name: string): Promise { + await xrpc('com.atproto.server.revokeAppPassword', { + method: 'POST', token, body: { name }, - }); + }) }, - getAccountInviteCodes(token: string): Promise<{ codes: InviteCode[] }> { - return xrpc("com.atproto.server.getAccountInviteCodes", { token }); + getAccountInviteCodes(token: AccessToken): Promise<{ codes: InviteCodeInfo[] }> { + return xrpc('com.atproto.server.getAccountInviteCodes', { token }) }, createInviteCode( - token: string, + token: AccessToken, useCount: number = 1, ): Promise<{ code: string }> { - return xrpc("com.atproto.server.createInviteCode", { - method: "POST", + return xrpc('com.atproto.server.createInviteCode', { + method: 'POST', token, body: { useCount }, - }); + }) }, - async requestPasswordReset(email: string): Promise { - await xrpc("com.atproto.server.requestPasswordReset", { - method: "POST", + async requestPasswordReset(email: EmailAddress): Promise { + await xrpc('com.atproto.server.requestPasswordReset', { + method: 'POST', body: { email }, - }); + }) }, async resetPassword(token: string, password: string): Promise { - await xrpc("com.atproto.server.resetPassword", { - method: "POST", + await xrpc('com.atproto.server.resetPassword', { + method: 'POST', body: { token, password }, - }); + }) }, - requestEmailUpdate( - token: string, - ): Promise<{ tokenRequired: boolean }> { - return xrpc("com.atproto.server.requestEmailUpdate", { - method: "POST", + requestEmailUpdate(token: AccessToken): Promise { + return xrpc('com.atproto.server.requestEmailUpdate', { + method: 'POST', token, - }); + }) }, async updateEmail( - token: string, + token: AccessToken, email: string, emailToken?: string, ): Promise { - await xrpc("com.atproto.server.updateEmail", { - method: "POST", + await xrpc('com.atproto.server.updateEmail', { + method: 'POST', token, body: { email, token: emailToken }, - }); + }) }, - async updateHandle(token: string, handle: string): Promise { - await xrpc("com.atproto.identity.updateHandle", { - method: "POST", + async updateHandle(token: AccessToken, handle: Handle): Promise { + await xrpc('com.atproto.identity.updateHandle', { + method: 'POST', token, body: { handle }, - }); + }) }, - async requestAccountDelete(token: string): Promise { - await xrpc("com.atproto.server.requestAccountDelete", { - method: "POST", + async requestAccountDelete(token: AccessToken): Promise { + await xrpc('com.atproto.server.requestAccountDelete', { + method: 'POST', token, - }); + }) }, async deleteAccount( - did: string, + did: Did, password: string, deleteToken: string, ): Promise { - await xrpc("com.atproto.server.deleteAccount", { - method: "POST", + await xrpc('com.atproto.server.deleteAccount', { + method: 'POST', body: { did, password, token: deleteToken }, - }); + }) }, - describeServer(): Promise<{ - availableUserDomains: string[]; - inviteCodeRequired: boolean; - links?: { privacyPolicy?: string; termsOfService?: string }; - version?: string; - availableCommsChannels?: string[]; - selfHostedDidWebEnabled?: boolean; - }> { - return xrpc("com.atproto.server.describeServer"); + describeServer(): Promise { + return xrpc('com.atproto.server.describeServer') }, - listRepos(limit?: number): Promise<{ - repos: Array<{ did: string; head: string; rev: string }>; - cursor?: string; - }> { - const params: Record = {}; - if (limit) params.limit = String(limit); - return xrpc("com.atproto.sync.listRepos", { params }); + listRepos(limit?: number): Promise { + const params: Record = {} + if (limit) params.limit = String(limit) + return xrpc('com.atproto.sync.listRepos', { params }) }, - getNotificationPrefs(token: string): Promise<{ - preferredChannel: string; - email: string; - discordId: string | null; - discordVerified: boolean; - telegramUsername: string | null; - telegramVerified: boolean; - signalNumber: string | null; - signalVerified: boolean; - }> { - return xrpc("_account.getNotificationPrefs", { token }); + getNotificationPrefs(token: AccessToken): Promise { + return xrpc('_account.getNotificationPrefs', { token }) }, - updateNotificationPrefs(token: string, prefs: { - preferredChannel?: string; - discordId?: string; - telegramUsername?: string; - signalNumber?: string; - }): Promise<{ success: boolean }> { - return xrpc("_account.updateNotificationPrefs", { - method: "POST", + updateNotificationPrefs(token: AccessToken, prefs: { + preferredChannel?: string + discordId?: string + telegramUsername?: string + signalNumber?: string + }): Promise { + return xrpc('_account.updateNotificationPrefs', { + method: 'POST', token, body: prefs, - }); + }) }, confirmChannelVerification( - token: string, + token: AccessToken, channel: string, identifier: string, code: string, - ): Promise<{ success: boolean }> { - return xrpc("_account.confirmChannelVerification", { - method: "POST", + ): Promise { + return xrpc('_account.confirmChannelVerification', { + method: 'POST', token, body: { channel, identifier, code }, - }); + }) }, - getNotificationHistory(token: string): Promise<{ - notifications: Array<{ - createdAt: string; - channel: string; - notificationType: string; - status: string; - subject: string | null; - body: string; - }>; - }> { - return xrpc("_account.getNotificationHistory", { token }); + getNotificationHistory(token: AccessToken): Promise { + return xrpc('_account.getNotificationHistory', { token }) }, - getServerStats(token: string): Promise<{ - userCount: number; - repoCount: number; - recordCount: number; - blobStorageBytes: number; - }> { - return xrpc("_admin.getServerStats", { token }); + getServerStats(token: AccessToken): Promise { + return xrpc('_admin.getServerStats', { token }) }, - getServerConfig(): Promise<{ - serverName: string; - primaryColor: string | null; - primaryColorDark: string | null; - secondaryColor: string | null; - secondaryColorDark: string | null; - logoCid: string | null; - }> { - return xrpc("_server.getConfig"); + getServerConfig(): Promise { + return xrpc('_server.getConfig') }, updateServerConfig( - token: string, + token: AccessToken, config: { - serverName?: string; - primaryColor?: string; - primaryColorDark?: string; - secondaryColor?: string; - secondaryColorDark?: string; - logoCid?: string; + serverName?: string + primaryColor?: string + primaryColorDark?: string + secondaryColor?: string + secondaryColorDark?: string + logoCid?: string }, - ): Promise<{ success: boolean }> { - return xrpc("_admin.updateServerConfig", { - method: "POST", + ): Promise { + return xrpc('_admin.updateServerConfig', { + method: 'POST', token, body: config, - }); + }) }, - async uploadBlob( - token: string, - file: File, - ): Promise< - { - blob: { - $type: string; - ref: { $link: string }; - mimeType: string; - size: number; - }; - } - > { - const res = await fetch("/xrpc/com.atproto.repo.uploadBlob", { - method: "POST", + async uploadBlob(token: AccessToken, file: File): Promise { + const res = await fetch('/xrpc/com.atproto.repo.uploadBlob', { + method: 'POST', headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": file.type, + 'Authorization': `Bearer ${token}`, + 'Content-Type': file.type, }, body: file, - }); + }) if (!res.ok) { - const err = await res.json().catch(() => ({ - error: "Unknown", + const errData = await res.json().catch(() => ({ + error: 'Unknown', message: res.statusText, - })); - throw new ApiError(res.status, err.error, err.message); + })) + throw new ApiError(res.status, errData.error, errData.message) } - return res.json(); + return res.json() }, async changePassword( - token: string, + token: AccessToken, currentPassword: string, newPassword: string, ): Promise { - await xrpc("_account.changePassword", { - method: "POST", + await xrpc('_account.changePassword', { + method: 'POST', token, body: { currentPassword, newPassword }, - }); + }) }, - removePassword(token: string): Promise<{ success: boolean }> { - return xrpc("_account.removePassword", { - method: "POST", + removePassword(token: AccessToken): Promise { + return xrpc('_account.removePassword', { + method: 'POST', token, - }); + }) }, - getPasswordStatus(token: string): Promise<{ hasPassword: boolean }> { - return xrpc("_account.getPasswordStatus", { token }); + getPasswordStatus(token: AccessToken): Promise { + return xrpc('_account.getPasswordStatus', { token }) }, - getLegacyLoginPreference( - token: string, - ): Promise<{ allowLegacyLogin: boolean; hasMfa: boolean }> { - return xrpc("_account.getLegacyLoginPreference", { token }); + getLegacyLoginPreference(token: AccessToken): Promise { + return xrpc('_account.getLegacyLoginPreference', { token }) }, updateLegacyLoginPreference( - token: string, + token: AccessToken, allowLegacyLogin: boolean, - ): Promise<{ allowLegacyLogin: boolean }> { - return xrpc("_account.updateLegacyLoginPreference", { - method: "POST", + ): Promise { + return xrpc('_account.updateLegacyLoginPreference', { + method: 'POST', token, body: { allowLegacyLogin }, - }); + }) }, - updateLocale( - token: string, - preferredLocale: string, - ): Promise<{ preferredLocale: string }> { - return xrpc("_account.updateLocale", { - method: "POST", + updateLocale(token: AccessToken, preferredLocale: string): Promise { + return xrpc('_account.updateLocale', { + method: 'POST', token, body: { preferredLocale }, - }); + }) }, - listSessions(token: string): Promise<{ - sessions: Array<{ - id: string; - sessionType: string; - clientName: string | null; - createdAt: string; - expiresAt: string; - isCurrent: boolean; - }>; - }> { - return xrpc("_account.listSessions", { token }); + listSessions(token: AccessToken): Promise { + return xrpc('_account.listSessions', { token }) }, - async revokeSession(token: string, sessionId: string): Promise { - await xrpc("_account.revokeSession", { - method: "POST", + async revokeSession(token: AccessToken, sessionId: string): Promise { + await xrpc('_account.revokeSession', { + method: 'POST', token, body: { sessionId }, - }); + }) }, - revokeAllSessions(token: string): Promise<{ revokedCount: number }> { - return xrpc("_account.revokeAllSessions", { - method: "POST", + revokeAllSessions(token: AccessToken): Promise<{ revokedCount: number }> { + return xrpc('_account.revokeAllSessions', { + method: 'POST', token, - }); + }) }, - searchAccounts(token: string, options?: { - handle?: string; - cursor?: string; - limit?: number; - }): Promise<{ - cursor?: string; - accounts: Array<{ - did: string; - handle: string; - email?: string; - indexedAt: string; - emailConfirmedAt?: string; - deactivatedAt?: string; - }>; - }> { - const params: Record = {}; - if (options?.handle) params.handle = options.handle; - if (options?.cursor) params.cursor = options.cursor; - if (options?.limit) params.limit = String(options.limit); - return xrpc("com.atproto.admin.searchAccounts", { token, params }); + searchAccounts(token: AccessToken, options?: { + handle?: string + cursor?: string + limit?: number + }): Promise { + const params: Record = {} + if (options?.handle) params.handle = options.handle + if (options?.cursor) params.cursor = options.cursor + if (options?.limit) params.limit = String(options.limit) + return xrpc('com.atproto.admin.searchAccounts', { token, params }) }, - getInviteCodes(token: string, options?: { - sort?: "recent" | "usage"; - cursor?: string; - limit?: number; - }): Promise<{ - cursor?: string; - codes: Array<{ - code: string; - available: number; - disabled: boolean; - forAccount: string; - createdBy: string; - createdAt: string; - uses: Array<{ usedBy: string; usedAt: string }>; - }>; - }> { - const params: Record = {}; - if (options?.sort) params.sort = options.sort; - if (options?.cursor) params.cursor = options.cursor; - if (options?.limit) params.limit = String(options.limit); - return xrpc("com.atproto.admin.getInviteCodes", { token, params }); + getInviteCodes(token: AccessToken, options?: { + sort?: 'recent' | 'usage' + cursor?: string + limit?: number + }): Promise { + const params: Record = {} + if (options?.sort) params.sort = options.sort + if (options?.cursor) params.cursor = options.cursor + if (options?.limit) params.limit = String(options.limit) + return xrpc('com.atproto.admin.getInviteCodes', { token, params }) }, async disableInviteCodes( - token: string, + token: AccessToken, codes?: string[], accounts?: string[], ): Promise { - await xrpc("com.atproto.admin.disableInviteCodes", { - method: "POST", + await xrpc('com.atproto.admin.disableInviteCodes', { + method: 'POST', token, body: { codes, accounts }, - }); + }) }, - getAccountInfo(token: string, did: string): Promise<{ - did: string; - handle: string; - email?: string; - indexedAt: string; - emailConfirmedAt?: string; - invitesDisabled?: boolean; - deactivatedAt?: string; - }> { - return xrpc("com.atproto.admin.getAccountInfo", { token, params: { did } }); + getAccountInfo(token: AccessToken, did: Did): Promise { + return xrpc('com.atproto.admin.getAccountInfo', { token, params: { did } }) }, - async disableAccountInvites(token: string, account: string): Promise { - await xrpc("com.atproto.admin.disableAccountInvites", { - method: "POST", + async disableAccountInvites(token: AccessToken, account: Did): Promise { + await xrpc('com.atproto.admin.disableAccountInvites', { + method: 'POST', token, body: { account }, - }); + }) }, - async enableAccountInvites(token: string, account: string): Promise { - await xrpc("com.atproto.admin.enableAccountInvites", { - method: "POST", + async enableAccountInvites(token: AccessToken, account: Did): Promise { + await xrpc('com.atproto.admin.enableAccountInvites', { + method: 'POST', token, body: { account }, - }); + }) }, - async adminDeleteAccount(token: string, did: string): Promise { - await xrpc("com.atproto.admin.deleteAccount", { - method: "POST", + async adminDeleteAccount(token: AccessToken, did: Did): Promise { + await xrpc('com.atproto.admin.deleteAccount', { + method: 'POST', token, body: { did }, - }); + }) }, - describeRepo(token: string, repo: string): Promise<{ - handle: string; - did: string; - didDoc: unknown; - collections: string[]; - handleIsCorrect: boolean; - }> { - return xrpc("com.atproto.repo.describeRepo", { + describeRepo(token: AccessToken, repo: Did): Promise { + return xrpc('com.atproto.repo.describeRepo', { token, params: { repo }, - }); + }) }, - listRecords(token: string, repo: string, collection: string, options?: { - limit?: number; - cursor?: string; - reverse?: boolean; - }): Promise<{ - records: Array<{ uri: string; cid: string; value: unknown }>; - cursor?: string; - }> { - const params: Record = { repo, collection }; - if (options?.limit) params.limit = String(options.limit); - if (options?.cursor) params.cursor = options.cursor; - if (options?.reverse) params.reverse = "true"; - return xrpc("com.atproto.repo.listRecords", { token, params }); + listRecords(token: AccessToken, repo: Did, collection: Nsid, options?: { + limit?: number + cursor?: string + reverse?: boolean + }): Promise { + const params: Record = { repo, collection } + if (options?.limit) params.limit = String(options.limit) + if (options?.cursor) params.cursor = options.cursor + if (options?.reverse) params.reverse = 'true' + return xrpc('com.atproto.repo.listRecords', { token, params }) }, getRecord( - token: string, - repo: string, - collection: string, - rkey: string, - ): Promise<{ - uri: string; - cid: string; - value: unknown; - }> { - return xrpc("com.atproto.repo.getRecord", { + token: AccessToken, + repo: Did, + collection: Nsid, + rkey: Rkey, + ): Promise { + return xrpc('com.atproto.repo.getRecord', { token, params: { repo, collection, rkey }, - }); + }) }, createRecord( - token: string, - repo: string, - collection: string, + token: AccessToken, + repo: Did, + collection: Nsid, record: unknown, - rkey?: string, - ): Promise<{ - uri: string; - cid: string; - }> { - return xrpc("com.atproto.repo.createRecord", { - method: "POST", + rkey?: Rkey, + ): Promise { + return xrpc('com.atproto.repo.createRecord', { + method: 'POST', token, body: { repo, collection, record, rkey }, - }); + }) }, putRecord( - token: string, - repo: string, - collection: string, - rkey: string, + token: AccessToken, + repo: Did, + collection: Nsid, + rkey: Rkey, record: unknown, - ): Promise<{ - uri: string; - cid: string; - }> { - return xrpc("com.atproto.repo.putRecord", { - method: "POST", + ): Promise { + return xrpc('com.atproto.repo.putRecord', { + method: 'POST', token, body: { repo, collection, rkey, record }, - }); + }) }, async deleteRecord( - token: string, - repo: string, - collection: string, - rkey: string, + token: AccessToken, + repo: Did, + collection: Nsid, + rkey: Rkey, ): Promise { - await xrpc("com.atproto.repo.deleteRecord", { - method: "POST", + await xrpc('com.atproto.repo.deleteRecord', { + method: 'POST', token, body: { repo, collection, rkey }, - }); + }) }, - getTotpStatus( - token: string, - ): Promise<{ enabled: boolean; hasBackupCodes: boolean }> { - return xrpc("com.atproto.server.getTotpStatus", { token }); + getTotpStatus(token: AccessToken): Promise { + return xrpc('com.atproto.server.getTotpStatus', { token }) }, - createTotpSecret( - token: string, - ): Promise<{ uri: string; qrBase64: string }> { - return xrpc("com.atproto.server.createTotpSecret", { - method: "POST", + createTotpSecret(token: AccessToken): Promise { + return xrpc('com.atproto.server.createTotpSecret', { + method: 'POST', token, - }); + }) }, - enableTotp( - token: string, - code: string, - ): Promise<{ success: boolean; backupCodes: string[] }> { - return xrpc("com.atproto.server.enableTotp", { - method: "POST", + enableTotp(token: AccessToken, code: string): Promise { + return xrpc('com.atproto.server.enableTotp', { + method: 'POST', token, body: { code }, - }); + }) }, disableTotp( - token: string, + token: AccessToken, password: string, code: string, - ): Promise<{ success: boolean }> { - return xrpc("com.atproto.server.disableTotp", { - method: "POST", + ): Promise { + return xrpc('com.atproto.server.disableTotp', { + method: 'POST', token, body: { password, code }, - }); + }) }, regenerateBackupCodes( - token: string, + token: AccessToken, password: string, code: string, - ): Promise<{ backupCodes: string[] }> { - return xrpc("com.atproto.server.regenerateBackupCodes", { - method: "POST", + ): Promise { + return xrpc('com.atproto.server.regenerateBackupCodes', { + method: 'POST', token, body: { password, code }, - }); + }) }, startPasskeyRegistration( - token: string, + token: AccessToken, friendlyName?: string, - ): Promise<{ options: unknown }> { - return xrpc("com.atproto.server.startPasskeyRegistration", { - method: "POST", + ): Promise { + return xrpc('com.atproto.server.startPasskeyRegistration', { + method: 'POST', token, body: { friendlyName }, - }); + }) }, finishPasskeyRegistration( - token: string, + token: AccessToken, credential: unknown, friendlyName?: string, - ): Promise<{ id: string; credentialId: string }> { - return xrpc("com.atproto.server.finishPasskeyRegistration", { - method: "POST", + ): Promise { + return xrpc('com.atproto.server.finishPasskeyRegistration', { + method: 'POST', token, body: { credential, friendlyName }, - }); + }) }, - listPasskeys(token: string): Promise<{ - passkeys: Array<{ - id: string; - credentialId: string; - friendlyName: string | null; - createdAt: string; - lastUsed: string | null; - }>; - }> { - return xrpc("com.atproto.server.listPasskeys", { token }); + listPasskeys(token: AccessToken): Promise { + return xrpc('com.atproto.server.listPasskeys', { token }) }, - async deletePasskey(token: string, id: string): Promise { - await xrpc("com.atproto.server.deletePasskey", { - method: "POST", + async deletePasskey(token: AccessToken, id: string): Promise { + await xrpc('com.atproto.server.deletePasskey', { + method: 'POST', token, body: { id }, - }); + }) }, async updatePasskey( - token: string, + token: AccessToken, id: string, friendlyName: string, ): Promise { - await xrpc("com.atproto.server.updatePasskey", { - method: "POST", + await xrpc('com.atproto.server.updatePasskey', { + method: 'POST', token, body: { id, friendlyName }, - }); + }) }, - listTrustedDevices(token: string): Promise<{ - devices: Array<{ - id: string; - userAgent: string | null; - friendlyName: string | null; - trustedAt: string | null; - trustedUntil: string | null; - lastSeenAt: string; - }>; - }> { - return xrpc("_account.listTrustedDevices", { token }); + listTrustedDevices(token: AccessToken): Promise { + return xrpc('_account.listTrustedDevices', { token }) }, - revokeTrustedDevice( - token: string, - deviceId: string, - ): Promise<{ success: boolean }> { - return xrpc("_account.revokeTrustedDevice", { - method: "POST", + revokeTrustedDevice(token: AccessToken, deviceId: string): Promise { + return xrpc('_account.revokeTrustedDevice', { + method: 'POST', token, body: { deviceId }, - }); + }) }, updateTrustedDevice( - token: string, + token: AccessToken, deviceId: string, friendlyName: string, - ): Promise<{ success: boolean }> { - return xrpc("_account.updateTrustedDevice", { - method: "POST", + ): Promise { + return xrpc('_account.updateTrustedDevice', { + method: 'POST', token, body: { deviceId, friendlyName }, - }); + }) }, - getReauthStatus(token: string): Promise<{ - requiresReauth: boolean; - lastReauthAt: string | null; - availableMethods: string[]; - }> { - return xrpc("_account.getReauthStatus", { token }); + getReauthStatus(token: AccessToken): Promise { + return xrpc('_account.getReauthStatus', { token }) }, - reauthPassword( - token: string, - password: string, - ): Promise<{ success: boolean; reauthAt: string }> { - return xrpc("_account.reauthPassword", { - method: "POST", + reauthPassword(token: AccessToken, password: string): Promise { + return xrpc('_account.reauthPassword', { + method: 'POST', token, body: { password }, - }); + }) }, - reauthTotp( - token: string, - code: string, - ): Promise<{ success: boolean; reauthAt: string }> { - return xrpc("_account.reauthTotp", { - method: "POST", + reauthTotp(token: AccessToken, code: string): Promise { + return xrpc('_account.reauthTotp', { + method: 'POST', token, body: { code }, - }); + }) }, - reauthPasskeyStart(token: string): Promise<{ options: unknown }> { - return xrpc("_account.reauthPasskeyStart", { - method: "POST", + reauthPasskeyStart(token: AccessToken): Promise { + return xrpc('_account.reauthPasskeyStart', { + method: 'POST', token, - }); + }) }, - reauthPasskeyFinish( - token: string, - credential: unknown, - ): Promise<{ success: boolean; reauthAt: string }> { - return xrpc("_account.reauthPasskeyFinish", { - method: "POST", + reauthPasskeyFinish(token: AccessToken, credential: unknown): Promise { + return xrpc('_account.reauthPasskeyFinish', { + method: 'POST', token, body: { credential }, - }); + }) }, - reserveSigningKey(did?: string): Promise<{ signingKey: string }> { - return xrpc("com.atproto.server.reserveSigningKey", { - method: "POST", + reserveSigningKey(did?: Did): Promise { + return xrpc('com.atproto.server.reserveSigningKey', { + method: 'POST', body: { did }, - }); + }) }, - getRecommendedDidCredentials(token: string): Promise<{ - rotationKeys?: string[]; - alsoKnownAs?: string[]; - verificationMethods?: { atproto?: string }; - services?: { atproto_pds?: { type: string; endpoint: string } }; - }> { - return xrpc("com.atproto.identity.getRecommendedDidCredentials", { token }); + getRecommendedDidCredentials(token: AccessToken): Promise { + return xrpc('com.atproto.identity.getRecommendedDidCredentials', { token }) }, - async activateAccount(token: string): Promise { - await xrpc("com.atproto.server.activateAccount", { - method: "POST", + async activateAccount(token: AccessToken): Promise { + await xrpc('com.atproto.server.activateAccount', { + method: 'POST', token, - }); + }) }, async createPasskeyAccount(params: { - handle: string; - email?: string; - inviteCode?: string; - didType?: DidType; - did?: string; - signingKey?: string; - verificationChannel?: VerificationChannel; - discordId?: string; - telegramUsername?: string; - signalNumber?: string; - }, byodToken?: string): Promise<{ - did: string; - handle: string; - setupToken: string; - setupExpiresAt: string; - }> { - const url = `${API_BASE}/_account.createPasskeyAccount`; + handle: Handle + email?: EmailAddress + inviteCode?: string + didType?: DidType + did?: Did + signingKey?: string + verificationChannel?: VerificationChannel + discordId?: string + telegramUsername?: string + signalNumber?: string + }, byodToken?: string): Promise { + const url = `${API_BASE}/_account.createPasskeyAccount` const headers: Record = { - "Content-Type": "application/json", - }; + 'Content-Type': 'application/json', + } if (byodToken) { - headers["Authorization"] = `Bearer ${byodToken}`; + headers['Authorization'] = `Bearer ${byodToken}` } const res = await fetch(url, { - method: "POST", + method: 'POST', headers, body: JSON.stringify(params), - }); + }) if (!res.ok) { - const err = await res.json().catch(() => ({ - error: "Unknown", + const errData = await res.json().catch(() => ({ + error: 'Unknown', message: res.statusText, - })); - throw new ApiError(res.status, err.error, err.message); + })) + throw new ApiError(res.status, errData.error, errData.message) } - return res.json(); + return res.json() }, startPasskeyRegistrationForSetup( - did: string, + did: Did, setupToken: string, friendlyName?: string, - ): Promise<{ options: unknown }> { - return xrpc("_account.startPasskeyRegistrationForSetup", { - method: "POST", + ): Promise { + return xrpc('_account.startPasskeyRegistrationForSetup', { + method: 'POST', body: { did, setupToken, friendlyName }, - }); + }) }, completePasskeySetup( - did: string, + did: Did, setupToken: string, passkeyCredential: unknown, passkeyFriendlyName?: string, - ): Promise<{ - did: string; - handle: string; - appPassword: string; - appPasswordName: string; - }> { - return xrpc("_account.completePasskeySetup", { - method: "POST", + ): Promise { + return xrpc('_account.completePasskeySetup', { + method: 'POST', body: { did, setupToken, passkeyCredential, passkeyFriendlyName }, - }); + }) }, - requestPasskeyRecovery(email: string): Promise<{ success: boolean }> { - return xrpc("_account.requestPasskeyRecovery", { - method: "POST", + requestPasskeyRecovery(email: EmailAddress): Promise { + return xrpc('_account.requestPasskeyRecovery', { + method: 'POST', body: { email }, - }); + }) }, recoverPasskeyAccount( - did: string, + did: Did, recoveryToken: string, newPassword: string, - ): Promise<{ success: boolean }> { - return xrpc("_account.recoverPasskeyAccount", { - method: "POST", + ): Promise { + return xrpc('_account.recoverPasskeyAccount', { + method: 'POST', body: { did, recoveryToken, newPassword }, - }); + }) }, - verifyMigrationEmail( - token: string, - email: string, - ): Promise<{ success: boolean; did: string }> { - return xrpc("com.atproto.server.verifyMigrationEmail", { - method: "POST", + verifyMigrationEmail(token: string, email: EmailAddress): Promise { + return xrpc('com.atproto.server.verifyMigrationEmail', { + method: 'POST', body: { token, email }, - }); + }) }, - resendMigrationVerification(email: string): Promise<{ sent: boolean }> { - return xrpc("com.atproto.server.resendMigrationVerification", { - method: "POST", + resendMigrationVerification(email: EmailAddress): Promise { + return xrpc('com.atproto.server.resendMigrationVerification', { + method: 'POST', body: { email }, - }); + }) }, verifyToken( token: string, identifier: string, - accessToken?: string, - ): Promise<{ - success: boolean; - did: string; - purpose: string; - channel: string; - }> { - return xrpc("_account.verifyToken", { - method: "POST", + accessToken?: AccessToken, + ): Promise { + return xrpc('_account.verifyToken', { + method: 'POST', body: { token, identifier }, token: accessToken, - }); + }) }, - getDidDocument(token: string): Promise { - return xrpc("_account.getDidDocument", { token }); + getDidDocument(token: AccessToken): Promise { + return xrpc('_account.getDidDocument', { token }) }, updateDidDocument( - token: string, + token: AccessToken, params: { - verificationMethods?: VerificationMethod[]; - alsoKnownAs?: string[]; - serviceEndpoint?: string; + verificationMethods?: VerificationMethod[] + alsoKnownAs?: string[] + serviceEndpoint?: string }, - ): Promise<{ success: boolean }> { - return xrpc("_account.updateDidDocument", { - method: "POST", + ): Promise { + return xrpc('_account.updateDidDocument', { + method: 'POST', token, body: params, - }); + }) }, - async deactivateAccount( - token: string, - deleteAfter?: string, - ): Promise { - await xrpc("com.atproto.server.deactivateAccount", { - method: "POST", + async deactivateAccount(token: AccessToken, deleteAfter?: string): Promise { + await xrpc('com.atproto.server.deactivateAccount', { + method: 'POST', token, body: { deleteAfter }, - }); + }) }, - async getRepo(token: string, did: string): Promise { - const url = `${API_BASE}/com.atproto.sync.getRepo?did=${ - encodeURIComponent(did) - }`; + async getRepo(token: AccessToken, did: Did): Promise { + const url = `${API_BASE}/com.atproto.sync.getRepo?did=${encodeURIComponent(did)}` const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, - }); + }) if (!res.ok) { - const err = await res.json().catch(() => ({ - error: "Unknown", + const errData = await res.json().catch(() => ({ + error: 'Unknown', message: res.statusText, - })); - throw new ApiError(res.status, err.error, err.message); + })) + throw new ApiError(res.status, errData.error, errData.message) } - return res.arrayBuffer(); + return res.arrayBuffer() }, - listBackups(token: string): Promise<{ - backups: Array<{ - id: string; - repoRev: string; - repoRootCid: string; - blockCount: number; - sizeBytes: number; - createdAt: string; - }>; - backupEnabled: boolean; - }> { - return xrpc("_backup.listBackups", { token }); + listBackups(token: AccessToken): Promise { + return xrpc('_backup.listBackups', { token }) }, - async getBackup(token: string, id: string): Promise { - const url = `${API_BASE}/_backup.getBackup?id=${encodeURIComponent(id)}`; + async getBackup(token: AccessToken, id: string): Promise { + const url = `${API_BASE}/_backup.getBackup?id=${encodeURIComponent(id)}` const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, - }); + }) if (!res.ok) { - const err = await res.json().catch(() => ({ - error: "Unknown", + const errData = await res.json().catch(() => ({ + error: 'Unknown', message: res.statusText, - })); - throw new ApiError(res.status, err.error, err.message); + })) + throw new ApiError(res.status, errData.error, errData.message) } - return res.blob(); + return res.blob() }, - createBackup(token: string): Promise<{ - id: string; - repoRev: string; - sizeBytes: number; - blockCount: number; - }> { - return xrpc("_backup.createBackup", { - method: "POST", + createBackup(token: AccessToken): Promise { + return xrpc('_backup.createBackup', { + method: 'POST', token, - }); + }) }, - async deleteBackup(token: string, id: string): Promise { - await xrpc("_backup.deleteBackup", { - method: "POST", + async deleteBackup(token: AccessToken, id: string): Promise { + await xrpc('_backup.deleteBackup', { + method: 'POST', token, params: { id }, - }); + }) }, - setBackupEnabled( - token: string, - enabled: boolean, - ): Promise<{ enabled: boolean }> { - return xrpc("_backup.setEnabled", { - method: "POST", + setBackupEnabled(token: AccessToken, enabled: boolean): Promise { + return xrpc('_backup.setEnabled', { + method: 'POST', token, body: { enabled }, - }); + }) }, - async importRepo(token: string, car: Uint8Array): Promise { - const url = `${API_BASE}/com.atproto.repo.importRepo`; + async importRepo(token: AccessToken, car: Uint8Array): Promise { + const url = `${API_BASE}/com.atproto.repo.importRepo` const res = await fetch(url, { - method: "POST", + method: 'POST', headers: { Authorization: `Bearer ${token}`, - "Content-Type": "application/vnd.ipld.car", + 'Content-Type': 'application/vnd.ipld.car', }, body: car, - }); + }) if (!res.ok) { - const err = await res.json().catch(() => ({ - error: "Unknown", + const errData = await res.json().catch(() => ({ + error: 'Unknown', message: res.statusText, - })); - throw new ApiError(res.status, err.error, err.message); + })) + throw new ApiError(res.status, errData.error, errData.message) } }, -}; +} + +export const typedApi = { + createSession( + identifier: string, + password: string + ): Promise> { + return xrpcResult('com.atproto.server.createSession', { + method: 'POST', + body: { identifier, password }, + }).then(r => r.ok ? ok(castSession(r.value)) : r) + }, + + getSession(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.getSession', { token }) + .then(r => r.ok ? ok(castSession(r.value)) : r) + }, + + refreshSession(refreshJwt: RefreshToken): Promise> { + return xrpcResult('com.atproto.server.refreshSession', { + method: 'POST', + token: refreshJwt, + }).then(r => r.ok ? ok(castSession(r.value)) : r) + }, + + describeServer(): Promise> { + return xrpcResult('com.atproto.server.describeServer') + }, + + listAppPasswords(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.listAppPasswords', { token }) + }, + + createAppPassword( + token: AccessToken, + name: string, + scopes?: string + ): Promise> { + return xrpcResult('com.atproto.server.createAppPassword', { + method: 'POST', + token, + body: { name, scopes }, + }) + }, + + revokeAppPassword(token: AccessToken, name: string): Promise> { + return xrpcResult('com.atproto.server.revokeAppPassword', { + method: 'POST', + token, + body: { name }, + }) + }, + + listSessions(token: AccessToken): Promise> { + return xrpcResult('_account.listSessions', { token }) + }, + + revokeSession(token: AccessToken, sessionId: string): Promise> { + return xrpcResult('_account.revokeSession', { + method: 'POST', + token, + body: { sessionId }, + }) + }, + + getTotpStatus(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.getTotpStatus', { token }) + }, + + createTotpSecret(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.createTotpSecret', { + method: 'POST', + token, + }) + }, + + enableTotp(token: AccessToken, code: string): Promise> { + return xrpcResult('com.atproto.server.enableTotp', { + method: 'POST', + token, + body: { code }, + }) + }, + + disableTotp( + token: AccessToken, + password: string, + code: string + ): Promise> { + return xrpcResult('com.atproto.server.disableTotp', { + method: 'POST', + token, + body: { password, code }, + }) + }, + + listPasskeys(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.listPasskeys', { token }) + }, + + deletePasskey(token: AccessToken, id: string): Promise> { + return xrpcResult('com.atproto.server.deletePasskey', { + method: 'POST', + token, + body: { id }, + }) + }, + + listTrustedDevices(token: AccessToken): Promise> { + return xrpcResult('_account.listTrustedDevices', { token }) + }, + + getReauthStatus(token: AccessToken): Promise> { + return xrpcResult('_account.getReauthStatus', { token }) + }, + + getNotificationPrefs(token: AccessToken): Promise> { + return xrpcResult('_account.getNotificationPrefs', { token }) + }, + + updateHandle(token: AccessToken, handle: Handle): Promise> { + return xrpcResult('com.atproto.identity.updateHandle', { + method: 'POST', + token, + body: { handle }, + }) + }, + + describeRepo(token: AccessToken, repo: Did): Promise> { + return xrpcResult('com.atproto.repo.describeRepo', { + token, + params: { repo }, + }) + }, + + listRecords( + token: AccessToken, + repo: Did, + collection: Nsid, + options?: { limit?: number; cursor?: string; reverse?: boolean } + ): Promise> { + const params: Record = { repo, collection } + if (options?.limit) params.limit = String(options.limit) + if (options?.cursor) params.cursor = options.cursor + if (options?.reverse) params.reverse = 'true' + return xrpcResult('com.atproto.repo.listRecords', { token, params }) + }, + + getRecord( + token: AccessToken, + repo: Did, + collection: Nsid, + rkey: Rkey + ): Promise> { + return xrpcResult('com.atproto.repo.getRecord', { + token, + params: { repo, collection, rkey }, + }) + }, + + deleteRecord( + token: AccessToken, + repo: Did, + collection: Nsid, + rkey: Rkey + ): Promise> { + return xrpcResult('com.atproto.repo.deleteRecord', { + method: 'POST', + token, + body: { repo, collection, rkey }, + }) + }, + + searchAccounts( + token: AccessToken, + options?: { handle?: string; cursor?: string; limit?: number } + ): Promise> { + const params: Record = {} + if (options?.handle) params.handle = options.handle + if (options?.cursor) params.cursor = options.cursor + if (options?.limit) params.limit = String(options.limit) + return xrpcResult('com.atproto.admin.searchAccounts', { token, params }) + }, + + getAccountInfo(token: AccessToken, did: Did): Promise> { + return xrpcResult('com.atproto.admin.getAccountInfo', { token, params: { did } }) + }, + + getServerStats(token: AccessToken): Promise> { + return xrpcResult('_admin.getServerStats', { token }) + }, + + listBackups(token: AccessToken): Promise> { + return xrpcResult('_backup.listBackups', { token }) + }, + + createBackup(token: AccessToken): Promise> { + return xrpcResult('_backup.createBackup', { + method: 'POST', + token, + }) + }, + + getDidDocument(token: AccessToken): Promise> { + return xrpcResult('_account.getDidDocument', { token }) + }, + + deleteSession(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.deleteSession', { + method: 'POST', + token, + }) + }, + + revokeAllSessions(token: AccessToken): Promise> { + return xrpcResult('_account.revokeAllSessions', { + method: 'POST', + token, + }) + }, + + getAccountInviteCodes(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.getAccountInviteCodes', { token }) + }, + + createInviteCode(token: AccessToken, useCount: number = 1): Promise> { + return xrpcResult('com.atproto.server.createInviteCode', { + method: 'POST', + token, + body: { useCount }, + }) + }, + + changePassword( + token: AccessToken, + currentPassword: string, + newPassword: string + ): Promise> { + return xrpcResult('_account.changePassword', { + method: 'POST', + token, + body: { currentPassword, newPassword }, + }) + }, + + getPasswordStatus(token: AccessToken): Promise> { + return xrpcResult('_account.getPasswordStatus', { token }) + }, + + getServerConfig(): Promise> { + return xrpcResult('_server.getConfig') + }, + + getLegacyLoginPreference(token: AccessToken): Promise> { + return xrpcResult('_account.getLegacyLoginPreference', { token }) + }, + + updateLegacyLoginPreference( + token: AccessToken, + allowLegacyLogin: boolean + ): Promise> { + return xrpcResult('_account.updateLegacyLoginPreference', { + method: 'POST', + token, + body: { allowLegacyLogin }, + }) + }, + + getNotificationHistory(token: AccessToken): Promise> { + return xrpcResult('_account.getNotificationHistory', { token }) + }, + + updateNotificationPrefs( + token: AccessToken, + prefs: { + preferredChannel?: string + discordId?: string + telegramUsername?: string + signalNumber?: string + } + ): Promise> { + return xrpcResult('_account.updateNotificationPrefs', { + method: 'POST', + token, + body: prefs, + }) + }, + + revokeTrustedDevice(token: AccessToken, deviceId: string): Promise> { + return xrpcResult('_account.revokeTrustedDevice', { + method: 'POST', + token, + body: { deviceId }, + }) + }, + + updateTrustedDevice( + token: AccessToken, + deviceId: string, + friendlyName: string + ): Promise> { + return xrpcResult('_account.updateTrustedDevice', { + method: 'POST', + token, + body: { deviceId, friendlyName }, + }) + }, + + reauthPassword(token: AccessToken, password: string): Promise> { + return xrpcResult('_account.reauthPassword', { + method: 'POST', + token, + body: { password }, + }) + }, + + reauthTotp(token: AccessToken, code: string): Promise> { + return xrpcResult('_account.reauthTotp', { + method: 'POST', + token, + body: { code }, + }) + }, + + reauthPasskeyStart(token: AccessToken): Promise> { + return xrpcResult('_account.reauthPasskeyStart', { + method: 'POST', + token, + }) + }, + + reauthPasskeyFinish(token: AccessToken, credential: unknown): Promise> { + return xrpcResult('_account.reauthPasskeyFinish', { + method: 'POST', + token, + body: { credential }, + }) + }, + + confirmSignup(did: Did, verificationCode: string): Promise> { + return xrpcResult('com.atproto.server.confirmSignup', { + method: 'POST', + body: { did, verificationCode }, + }) + }, + + resendVerification(did: Did): Promise> { + return xrpcResult('com.atproto.server.resendVerification', { + method: 'POST', + body: { did }, + }) + }, + + requestEmailUpdate(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.requestEmailUpdate', { + method: 'POST', + token, + }) + }, + + updateEmail(token: AccessToken, email: string, emailToken?: string): Promise> { + return xrpcResult('com.atproto.server.updateEmail', { + method: 'POST', + token, + body: { email, token: emailToken }, + }) + }, + + requestAccountDelete(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.requestAccountDelete', { + method: 'POST', + token, + }) + }, + + deleteAccount(did: Did, password: string, deleteToken: string): Promise> { + return xrpcResult('com.atproto.server.deleteAccount', { + method: 'POST', + body: { did, password, token: deleteToken }, + }) + }, + + updateDidDocument( + token: AccessToken, + params: { + verificationMethods?: VerificationMethod[] + alsoKnownAs?: string[] + serviceEndpoint?: string + } + ): Promise> { + return xrpcResult('_account.updateDidDocument', { + method: 'POST', + token, + body: params, + }) + }, + + deactivateAccount(token: AccessToken, deleteAfter?: string): Promise> { + return xrpcResult('com.atproto.server.deactivateAccount', { + method: 'POST', + token, + body: { deleteAfter }, + }) + }, + + activateAccount(token: AccessToken): Promise> { + return xrpcResult('com.atproto.server.activateAccount', { + method: 'POST', + token, + }) + }, + + setBackupEnabled(token: AccessToken, enabled: boolean): Promise> { + return xrpcResult('_backup.setEnabled', { + method: 'POST', + token, + body: { enabled }, + }) + }, + + deleteBackup(token: AccessToken, id: string): Promise> { + return xrpcResult('_backup.deleteBackup', { + method: 'POST', + token, + params: { id }, + }) + }, + + createRecord( + token: AccessToken, + repo: Did, + collection: Nsid, + record: unknown, + rkey?: Rkey + ): Promise> { + return xrpcResult('com.atproto.repo.createRecord', { + method: 'POST', + token, + body: { repo, collection, record, rkey }, + }) + }, + + putRecord( + token: AccessToken, + repo: Did, + collection: Nsid, + rkey: Rkey, + record: unknown + ): Promise> { + return xrpcResult('com.atproto.repo.putRecord', { + method: 'POST', + token, + body: { repo, collection, rkey, record }, + }) + }, + + getInviteCodes( + token: AccessToken, + options?: { sort?: 'recent' | 'usage'; cursor?: string; limit?: number } + ): Promise> { + const params: Record = {} + if (options?.sort) params.sort = options.sort + if (options?.cursor) params.cursor = options.cursor + if (options?.limit) params.limit = String(options.limit) + return xrpcResult('com.atproto.admin.getInviteCodes', { token, params }) + }, + + disableAccountInvites(token: AccessToken, account: Did): Promise> { + return xrpcResult('com.atproto.admin.disableAccountInvites', { + method: 'POST', + token, + body: { account }, + }) + }, + + enableAccountInvites(token: AccessToken, account: Did): Promise> { + return xrpcResult('com.atproto.admin.enableAccountInvites', { + method: 'POST', + token, + body: { account }, + }) + }, + + adminDeleteAccount(token: AccessToken, did: Did): Promise> { + return xrpcResult('com.atproto.admin.deleteAccount', { + method: 'POST', + token, + body: { did }, + }) + }, + + startPasskeyRegistration( + token: AccessToken, + friendlyName?: string + ): Promise> { + return xrpcResult('com.atproto.server.startPasskeyRegistration', { + method: 'POST', + token, + body: { friendlyName }, + }) + }, + + finishPasskeyRegistration( + token: AccessToken, + credential: unknown, + friendlyName?: string + ): Promise> { + return xrpcResult('com.atproto.server.finishPasskeyRegistration', { + method: 'POST', + token, + body: { credential, friendlyName }, + }) + }, + + updatePasskey( + token: AccessToken, + id: string, + friendlyName: string + ): Promise> { + return xrpcResult('com.atproto.server.updatePasskey', { + method: 'POST', + token, + body: { id, friendlyName }, + }) + }, + + regenerateBackupCodes( + token: AccessToken, + password: string, + code: string + ): Promise> { + return xrpcResult('com.atproto.server.regenerateBackupCodes', { + method: 'POST', + token, + body: { password, code }, + }) + }, + + updateLocale(token: AccessToken, preferredLocale: string): Promise> { + return xrpcResult('_account.updateLocale', { + method: 'POST', + token, + body: { preferredLocale }, + }) + }, + + confirmChannelVerification( + token: AccessToken, + channel: string, + identifier: string, + code: string + ): Promise> { + return xrpcResult('_account.confirmChannelVerification', { + method: 'POST', + token, + body: { channel, identifier, code }, + }) + }, + + removePassword(token: AccessToken): Promise> { + return xrpcResult('_account.removePassword', { + method: 'POST', + token, + }) + }, +} diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts index 2f1f4ae..455e75d 100644 --- a/frontend/src/lib/auth.svelte.ts +++ b/frontend/src/lib/auth.svelte.ts @@ -1,11 +1,23 @@ import { api, ApiError, + typedApi, type CreateAccountParams, type CreateAccountResult, - type Session, - setTokenRefreshCallback, } from "./api"; +import type { Session } from "./types/api"; +import { + type Did, + type Handle, + type AccessToken, + type RefreshToken, + unsafeAsDid, + unsafeAsHandle, + unsafeAsAccessToken, + unsafeAsRefreshToken, +} from "./types/branded"; +import { type Result, ok, err, isOk, isErr, map } from "./types/result"; +import { assertNever } from "./types/exhaustive"; import { checkForOAuthCallback, clearOAuthCallbackParams, @@ -15,39 +27,184 @@ import { } from "./oauth"; import { setLocale, type SupportedLocale } from "./i18n"; -function applyLocaleFromSession( - sessionInfo: { preferredLocale?: string | null }, -) { +const STORAGE_KEY = "tranquil_pds_session"; +const ACCOUNTS_KEY = "tranquil_pds_accounts"; + +export interface SavedAccount { + readonly did: Did; + readonly handle: Handle; + readonly accessJwt: AccessToken; + readonly refreshJwt: RefreshToken; +} + +export type AuthError = + | { readonly type: "network"; readonly message: string } + | { readonly type: "unauthorized"; readonly message: string } + | { readonly type: "validation"; readonly message: string } + | { readonly type: "oauth"; readonly message: string } + | { readonly type: "unknown"; readonly message: string }; + +function toAuthError(e: unknown): AuthError { + if (e instanceof ApiError) { + if (e.status === 401) { + return { type: "unauthorized", message: e.message }; + } + return { type: "validation", message: e.message }; + } + if (e instanceof Error) { + if (e.message.includes("network") || e.message.includes("fetch")) { + return { type: "network", message: e.message }; + } + return { type: "unknown", message: e.message }; + } + return { type: "unknown", message: "An unknown error occurred" }; +} + +type AuthStateKind = "unauthenticated" | "loading" | "authenticated" | "error"; + +export type AuthState = + | { + readonly kind: "unauthenticated"; + readonly savedAccounts: readonly SavedAccount[]; + } + | { + readonly kind: "loading"; + readonly savedAccounts: readonly SavedAccount[]; + readonly previousSession: Session | null; + } + | { + readonly kind: "authenticated"; + readonly session: Session; + readonly savedAccounts: readonly SavedAccount[]; + } + | { + readonly kind: "error"; + readonly error: AuthError; + readonly savedAccounts: readonly SavedAccount[]; + }; + +function createUnauthenticated( + savedAccounts: readonly SavedAccount[], +): AuthState { + return { kind: "unauthenticated", savedAccounts }; +} + +function createLoading( + savedAccounts: readonly SavedAccount[], + previousSession: Session | null = null, +): AuthState { + return { kind: "loading", savedAccounts, previousSession }; +} + +function createAuthenticated( + session: Session, + savedAccounts: readonly SavedAccount[], +): AuthState { + return { kind: "authenticated", session, savedAccounts }; +} + +function createError( + error: AuthError, + savedAccounts: readonly SavedAccount[], +): AuthState { + return { kind: "error", error, savedAccounts }; +} + +const state = $state<{ current: AuthState }>({ + current: createLoading([]), +}); + +function applyLocaleFromSession(sessionInfo: { + preferredLocale?: string | null; +}): void { if (sessionInfo.preferredLocale) { setLocale(sessionInfo.preferredLocale as SupportedLocale); } } -const STORAGE_KEY = "tranquil_pds_session"; -const ACCOUNTS_KEY = "tranquil_pds_accounts"; - -export interface SavedAccount { - did: string; - handle: string; - accessJwt: string; - refreshJwt: string; +function sessionToSavedAccount(session: Session): SavedAccount { + return { + did: unsafeAsDid(session.did), + handle: unsafeAsHandle(session.handle), + accessJwt: unsafeAsAccessToken(session.accessJwt), + refreshJwt: unsafeAsRefreshToken(session.refreshJwt), + }; } -interface AuthState { - session: Session | null; - loading: boolean; - error: string | null; - savedAccounts: SavedAccount[]; +interface StoredSession { + readonly did: string; + readonly handle: string; + readonly accessJwt: string; + readonly refreshJwt: string; + readonly email?: string; + readonly emailConfirmed?: boolean; + readonly preferredChannel?: string; + readonly preferredChannelVerified?: boolean; + readonly preferredLocale?: string | null; } -const state = $state({ - session: null, - loading: true, - error: null, - savedAccounts: [], -}); +function parseStoredSession(json: string): Result { + try { + const parsed = JSON.parse(json); + if ( + typeof parsed === "object" && + parsed !== null && + typeof parsed.did === "string" && + typeof parsed.handle === "string" && + typeof parsed.accessJwt === "string" && + typeof parsed.refreshJwt === "string" + ) { + return ok(parsed as StoredSession); + } + return err(new Error("Invalid session format")); + } catch (e) { + return err(e instanceof Error ? e : new Error("Failed to parse session")); + } +} -function saveSession(session: Session | null) { +function parseStoredAccounts(json: string): Result { + try { + const parsed = JSON.parse(json); + if (!Array.isArray(parsed)) { + return err(new Error("Invalid accounts format")); + } + const accounts: SavedAccount[] = parsed + .filter( + (a): a is { did: string; handle: string; accessJwt: string; refreshJwt: string } => + typeof a === "object" && + a !== null && + typeof a.did === "string" && + typeof a.handle === "string" && + typeof a.accessJwt === "string" && + typeof a.refreshJwt === "string", + ) + .map((a) => ({ + did: unsafeAsDid(a.did), + handle: unsafeAsHandle(a.handle), + accessJwt: unsafeAsAccessToken(a.accessJwt), + refreshJwt: unsafeAsRefreshToken(a.refreshJwt), + })); + return ok(accounts); + } catch (e) { + return err(e instanceof Error ? e : new Error("Failed to parse accounts")); + } +} + +function loadSessionFromStorage(): StoredSession | null { + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) return null; + const result = parseStoredSession(stored); + return isOk(result) ? result.value : null; +} + +function loadSavedAccountsFromStorage(): readonly SavedAccount[] { + const stored = localStorage.getItem(ACCOUNTS_KEY); + if (!stored) return []; + const result = parseStoredAccounts(stored); + return isOk(result) ? result.value : []; +} + +function persistSession(session: Session | null): void { if (session) { localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); } else { @@ -55,82 +212,85 @@ function saveSession(session: Session | null) { } } -function loadSession(): Session | null { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - return JSON.parse(stored); - } catch { - return null; - } - } - return null; -} - -function loadSavedAccounts(): SavedAccount[] { - const stored = localStorage.getItem(ACCOUNTS_KEY); - if (stored) { - try { - return JSON.parse(stored); - } catch { - return []; - } - } - return []; -} - -function saveSavedAccounts(accounts: SavedAccount[]) { +function persistSavedAccounts(accounts: readonly SavedAccount[]): void { localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(accounts)); } -function addOrUpdateSavedAccount(session: Session) { - const accounts = loadSavedAccounts(); - const existing = accounts.findIndex((a) => a.did === session.did); - const savedAccount: SavedAccount = { - did: session.did, - handle: session.handle, - accessJwt: session.accessJwt, - refreshJwt: session.refreshJwt, - }; - if (existing >= 0) { - accounts[existing] = savedAccount; - } else { - accounts.push(savedAccount); - } - saveSavedAccounts(accounts); - state.savedAccounts = accounts; +function updateSavedAccounts( + accounts: readonly SavedAccount[], + session: Session, +): readonly SavedAccount[] { + const newAccount = sessionToSavedAccount(session); + const filtered = accounts.filter((a) => a.did !== newAccount.did); + return [...filtered, newAccount]; } -function removeSavedAccount(did: string) { - const accounts = loadSavedAccounts().filter((a) => a.did !== did); - saveSavedAccounts(accounts); - state.savedAccounts = accounts; +function removeSavedAccountByDid( + accounts: readonly SavedAccount[], + did: Did, +): readonly SavedAccount[] { + return accounts.filter((a) => a.did !== did); +} + +function findSavedAccount( + accounts: readonly SavedAccount[], + did: Did, +): SavedAccount | undefined { + return accounts.find((a) => a.did === did); +} + +function getSavedAccounts(): readonly SavedAccount[] { + return state.current.savedAccounts; +} + +function setState(newState: AuthState): void { + state.current = newState; +} + +function setAuthenticated(session: Session): void { + const accounts = updateSavedAccounts(getSavedAccounts(), session); + persistSession(session); + persistSavedAccounts(accounts); + setState(createAuthenticated(session, accounts)); +} + +function setUnauthenticated(): void { + persistSession(null); + setState(createUnauthenticated(getSavedAccounts())); +} + +function setError(error: AuthError): void { + setState(createError(error, getSavedAccounts())); +} + +function setLoading(previousSession: Session | null = null): void { + setState(createLoading(getSavedAccounts(), previousSession)); } async function tryRefreshToken(): Promise { - if (!state.session) return null; + if (state.current.kind !== "authenticated") return null; + const currentSession = state.current.session; try { - const tokens = await refreshOAuthToken(state.session.refreshJwt); + const tokens = await refreshOAuthToken(currentSession.refreshJwt); const sessionInfo = await api.getSession(tokens.access_token); const session: Session = { ...sessionInfo, accessJwt: tokens.access_token, - refreshJwt: tokens.refresh_token || state.session.refreshJwt, + refreshJwt: tokens.refresh_token || currentSession.refreshJwt, }; - state.session = session; - saveSession(session); - addOrUpdateSavedAccount(session); + setAuthenticated(session); return session.accessJwt; } catch { return null; } } +import { setTokenRefreshCallback } from "./api"; + export async function initAuth(): Promise<{ oauthLoginCompleted: boolean }> { setTokenRefreshCallback(tryRefreshToken); - state.loading = true; - state.error = null; - state.savedAccounts = loadSavedAccounts(); + const savedAccounts = loadSavedAccountsFromStorage(); + setState(createLoading(savedAccounts)); const oauthCallback = checkForOAuthCallback(); if (oauthCallback) { @@ -146,29 +306,25 @@ export async function initAuth(): Promise<{ oauthLoginCompleted: boolean }> { accessJwt: tokens.access_token, refreshJwt: tokens.refresh_token || "", }; - state.session = session; - saveSession(session); - addOrUpdateSavedAccount(session); + setAuthenticated(session); applyLocaleFromSession(sessionInfo); - state.loading = false; return { oauthLoginCompleted: true }; } catch (e) { - state.error = e instanceof Error ? e.message : "OAuth login failed"; - state.loading = false; + setError({ type: "oauth", message: e instanceof Error ? e.message : "OAuth login failed" }); return { oauthLoginCompleted: false }; } } - const stored = loadSession(); + const stored = loadSessionFromStorage(); if (stored) { try { const sessionInfo = await api.getSession(stored.accessJwt); - state.session = { + const session: Session = { ...sessionInfo, accessJwt: stored.accessJwt, refreshJwt: stored.refreshJwt, }; - addOrUpdateSavedAccount(state.session); + setAuthenticated(session); applyLocaleFromSession(sessionInfo); } catch (e) { if (e instanceof ApiError && e.status === 401) { @@ -180,85 +336,72 @@ export async function initAuth(): Promise<{ oauthLoginCompleted: boolean }> { accessJwt: tokens.access_token, refreshJwt: tokens.refresh_token || stored.refreshJwt, }; - state.session = session; - saveSession(session); - addOrUpdateSavedAccount(session); + setAuthenticated(session); applyLocaleFromSession(sessionInfo); } catch (refreshError) { console.error("Token refresh failed during init:", refreshError); - saveSession(null); - state.session = null; + setUnauthenticated(); } } else { console.error("Non-401 error during getSession:", e); - saveSession(null); - state.session = null; + setUnauthenticated(); } } + } else { + setState(createUnauthenticated(savedAccounts)); } - state.loading = false; + return { oauthLoginCompleted: false }; } export async function login( identifier: string, password: string, -): Promise { - state.loading = true; - state.error = null; - try { - const session = await api.createSession(identifier, password); - state.session = session; - saveSession(session); - addOrUpdateSavedAccount(session); - } catch (e) { - if (e instanceof ApiError) { - state.error = e.message; - } else { - state.error = "Login failed"; - } - throw e; - } finally { - state.loading = false; +): Promise> { + const currentState = state.current; + const previousSession = + currentState.kind === "authenticated" ? currentState.session : null; + setLoading(previousSession); + + const result = await typedApi.createSession(identifier, password); + if (isErr(result)) { + const error = toAuthError(result.error); + setError(error); + return err(error); } + + setAuthenticated(result.value); + return ok(result.value); } -export async function loginWithOAuth(): Promise { - state.loading = true; - state.error = null; +export async function loginWithOAuth(): Promise> { + setLoading(); try { await startOAuthLogin(); + return ok(undefined); } catch (e) { - state.loading = false; - state.error = e instanceof Error - ? e.message - : "Failed to start OAuth login"; - throw e; + const error = toAuthError(e); + setError(error); + return err(error); } } export async function register( params: CreateAccountParams, -): Promise { +): Promise> { try { const result = await api.createAccount(params); - return result; + return ok(result); } catch (e) { - if (e instanceof ApiError) { - state.error = e.message; - } else { - state.error = "Registration failed"; - } - throw e; + return err(toAuthError(e)); } } export async function confirmSignup( did: string, verificationCode: string, -): Promise { - state.loading = true; - state.error = null; +): Promise> { + setLoading(); try { const result = await api.confirmSignup(did, verificationCode); const session: Session = { @@ -271,160 +414,170 @@ export async function confirmSignup( preferredChannel: result.preferredChannel, preferredChannelVerified: result.preferredChannelVerified, }; - state.session = session; - saveSession(session); - addOrUpdateSavedAccount(session); + setAuthenticated(session); + return ok(session); } catch (e) { - if (e instanceof ApiError) { - state.error = e.message; - } else { - state.error = "Verification failed"; - } - throw e; - } finally { - state.loading = false; + const error = toAuthError(e); + setError(error); + return err(error); } } -export async function resendVerification(did: string): Promise { +export async function resendVerification( + did: string, +): Promise> { try { await api.resendVerification(did); + return ok(undefined); } catch (e) { - if (e instanceof ApiError) { - throw e; - } - throw new Error("Failed to resend verification code"); + return err(toAuthError(e)); } } -export function setSession( - session: { - did: string; - handle: string; - accessJwt: string; - refreshJwt: string; - }, -): void { +export function setSession(session: { + did: string; + handle: string; + accessJwt: string; + refreshJwt: string; +}): void { const newSession: Session = { did: session.did, handle: session.handle, accessJwt: session.accessJwt, refreshJwt: session.refreshJwt, }; - state.session = newSession; - saveSession(newSession); - addOrUpdateSavedAccount(newSession); + setAuthenticated(newSession); } -export async function logout(): Promise { - if (state.session) { - const did = state.session.did; - const refreshToken = state.session.refreshJwt; +export async function logout(): Promise> { + if (state.current.kind === "authenticated") { + const { session } = state.current; + const did = unsafeAsDid(session.did); try { await fetch("/oauth/revoke", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ token: refreshToken }), + body: new URLSearchParams({ token: session.refreshJwt }), }); } catch { - // Ignore errors on logout + // Ignore revocation errors } - removeSavedAccount(did); + const accounts = removeSavedAccountByDid(getSavedAccounts(), did); + persistSavedAccounts(accounts); + persistSession(null); + setState(createUnauthenticated(accounts)); + } else { + setUnauthenticated(); } - state.session = null; - saveSession(null); + return ok(undefined); } -export async function switchAccount(did: string): Promise { - const account = state.savedAccounts.find((a) => a.did === did); +export async function switchAccount( + did: Did, +): Promise> { + const account = findSavedAccount(getSavedAccounts(), did); if (!account) { - throw new Error("Account not found"); + return err({ type: "validation", message: "Account not found" }); } - state.loading = true; - state.error = null; + + setLoading(); + try { - const session = await api.getSession(account.accessJwt); - state.session = { - ...session, - accessJwt: account.accessJwt, - refreshJwt: account.refreshJwt, + const sessionInfo = await api.getSession(account.accessJwt as string); + const session: Session = { + ...sessionInfo, + accessJwt: account.accessJwt as string, + refreshJwt: account.refreshJwt as string, }; - saveSession(state.session); - addOrUpdateSavedAccount(state.session); + setAuthenticated(session); + return ok(session); } catch (e) { if (e instanceof ApiError && e.status === 401) { try { - const tokens = await refreshOAuthToken(account.refreshJwt); + const tokens = await refreshOAuthToken(account.refreshJwt as string); const sessionInfo = await api.getSession(tokens.access_token); const session: Session = { ...sessionInfo, accessJwt: tokens.access_token, - refreshJwt: tokens.refresh_token || account.refreshJwt, + refreshJwt: tokens.refresh_token || (account.refreshJwt as string), }; - state.session = session; - saveSession(session); - addOrUpdateSavedAccount(session); + setAuthenticated(session); + return ok(session); } catch { - removeSavedAccount(did); - state.error = "Session expired. Please log in again."; - throw new Error("Session expired"); + const accounts = removeSavedAccountByDid(getSavedAccounts(), did); + persistSavedAccounts(accounts); + const error: AuthError = { + type: "unauthorized", + message: "Session expired. Please log in again.", + }; + setState(createError(error, accounts)); + return err(error); } - } else { - state.error = "Failed to switch account"; - throw e; } - } finally { - state.loading = false; + const error = toAuthError(e); + setError(error); + return err(error); } } -export function forgetAccount(did: string): void { - removeSavedAccount(did); +export function forgetAccount(did: Did): void { + const accounts = removeSavedAccountByDid(getSavedAccounts(), did); + persistSavedAccounts(accounts); + setState({ + ...state.current, + savedAccounts: accounts, + } as AuthState); } -export function getAuthState() { - return state; +export function getAuthState(): AuthState { + return state.current; } -export async function refreshSession(): Promise { - if (!state.session) return; +export async function refreshSession(): Promise> { + if (state.current.kind !== "authenticated") { + return err({ type: "unauthorized", message: "Not authenticated" }); + } + const currentSession = state.current.session; try { - const sessionInfo = await api.getSession(state.session.accessJwt); - state.session = { + const sessionInfo = await api.getSession(currentSession.accessJwt); + const session: Session = { ...sessionInfo, - accessJwt: state.session.accessJwt, - refreshJwt: state.session.refreshJwt, + accessJwt: currentSession.accessJwt, + refreshJwt: currentSession.refreshJwt, }; - saveSession(state.session); - addOrUpdateSavedAccount(state.session); + setAuthenticated(session); + return ok(session); } catch (e) { console.error("Failed to refresh session:", e); + return err(toAuthError(e)); } } -export function getToken(): string | null { - return state.session?.accessJwt ?? null; +export function getToken(): AccessToken | null { + if (state.current.kind === "authenticated") { + return unsafeAsAccessToken(state.current.session.accessJwt); + } + return null; } -export async function getValidToken(): Promise { - if (!state.session) return null; +export async function getValidToken(): Promise { + if (state.current.kind !== "authenticated") return null; + const currentSession = state.current.session; try { - await api.getSession(state.session.accessJwt); - return state.session.accessJwt; + await api.getSession(currentSession.accessJwt); + return unsafeAsAccessToken(currentSession.accessJwt); } catch (e) { if (e instanceof ApiError && e.status === 401) { try { - const tokens = await refreshOAuthToken(state.session.refreshJwt); + const tokens = await refreshOAuthToken(currentSession.refreshJwt); const sessionInfo = await api.getSession(tokens.access_token); const session: Session = { ...sessionInfo, accessJwt: tokens.access_token, - refreshJwt: tokens.refresh_token || state.session.refreshJwt, + refreshJwt: tokens.refresh_token || currentSession.refreshJwt, }; - state.session = session; - saveSession(session); - addOrUpdateSavedAccount(session); - return session.accessJwt; + setAuthenticated(session); + return unsafeAsAccessToken(session.accessJwt); } catch { return null; } @@ -434,32 +587,68 @@ export async function getValidToken(): Promise { } export function isAuthenticated(): boolean { - return state.session !== null; + return state.current.kind === "authenticated"; } -export function _testSetState( - newState: { - session: Session | null; - loading: boolean; - error: string | null; - savedAccounts?: SavedAccount[]; - }, -) { - state.session = newState.session; - state.loading = newState.loading; - state.error = newState.error; - state.savedAccounts = newState.savedAccounts ?? []; +export function isLoading(): boolean { + return state.current.kind === "loading"; } -export function _testResetState() { - state.session = null; - state.loading = true; - state.error = null; - state.savedAccounts = []; +export function getError(): AuthError | null { + return state.current.kind === "error" ? state.current.error : null; } -export function _testReset() { +export function getSession(): Session | null { + return state.current.kind === "authenticated" ? state.current.session : null; +} + +export function matchAuthState(handlers: { + unauthenticated: (accounts: readonly SavedAccount[]) => T; + loading: (accounts: readonly SavedAccount[], previousSession: Session | null) => T; + authenticated: (session: Session, accounts: readonly SavedAccount[]) => T; + error: (error: AuthError, accounts: readonly SavedAccount[]) => T; +}): T { + const current = state.current; + switch (current.kind) { + case "unauthenticated": + return handlers.unauthenticated(current.savedAccounts); + case "loading": + return handlers.loading(current.savedAccounts, current.previousSession); + case "authenticated": + return handlers.authenticated(current.session, current.savedAccounts); + case "error": + return handlers.error(current.error, current.savedAccounts); + default: + return assertNever(current); + } +} + +export function _testSetState(newState: { + session: Session | null; + loading: boolean; + error: string | null; + savedAccounts?: SavedAccount[]; +}): void { + const accounts = newState.savedAccounts ?? []; + if (newState.loading) { + setState(createLoading(accounts, newState.session)); + } else if (newState.error) { + setState(createError({ type: "unknown", message: newState.error }, accounts)); + } else if (newState.session) { + setState(createAuthenticated(newState.session, accounts)); + } else { + setState(createUnauthenticated(accounts)); + } +} + +export function _testResetState(): void { + setState(createLoading([])); +} + +export function _testReset(): void { _testResetState(); localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(ACCOUNTS_KEY); } + +export { type Session }; diff --git a/frontend/src/lib/crypto.ts b/frontend/src/lib/crypto.ts index 3f2a9ec..9bc83e6 100644 --- a/frontend/src/lib/crypto.ts +++ b/frontend/src/lib/crypto.ts @@ -35,10 +35,7 @@ function base64UrlEncode(data: Uint8Array | string): string { const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data; - let binary = ""; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } + const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } diff --git a/frontend/src/lib/migration/atproto-client.ts b/frontend/src/lib/migration/atproto-client.ts index b792616..50eb16e 100644 --- a/frontend/src/lib/migration/atproto-client.ts +++ b/frontend/src/lib/migration/atproto-client.ts @@ -600,10 +600,7 @@ export async function generatePKCE(): Promise<{ export function base64UrlEncode(buffer: Uint8Array | ArrayBuffer): string { const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer; - let binary = ""; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } + const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace( /=+$/, "", @@ -614,11 +611,7 @@ export function base64UrlDecode(base64url: string): Uint8Array { const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/"); const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4); const binary = atob(padded); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes; + return Uint8Array.from(binary, (char) => char.charCodeAt(0)); } export function prepareWebAuthnCreationOptions( @@ -865,13 +858,12 @@ export async function resolvePdsUrl( ); if (dnsRes.ok) { const dnsData = await dnsRes.json(); - const txtRecords = dnsData.Answer ?? []; - for (const record of txtRecords) { - const txt = record.data?.replace(/"/g, "") ?? ""; - if (txt.startsWith("did=")) { - did = txt.slice(4); - break; - } + const txtRecords: Array<{ data?: string }> = dnsData.Answer ?? []; + const didRecord = txtRecords + .map((record) => record.data?.replace(/"/g, "") ?? "") + .find((txt) => txt.startsWith("did=")); + if (didRecord) { + did = didRecord.slice(4); } } diff --git a/frontend/src/lib/migration/blob-migration.ts b/frontend/src/lib/migration/blob-migration.ts index fc6e1d2..79375cb 100644 --- a/frontend/src/lib/migration/blob-migration.ts +++ b/frontend/src/lib/migration/blob-migration.ts @@ -36,9 +36,7 @@ export async function migrateBlobs( "blobs, cursor:", nextCursor, ); - for (const blob of blobs) { - missingBlobs.push(blob.cid); - } + missingBlobs.push(...blobs.map((blob) => blob.cid)); cursor = nextCursor; } while (cursor); diff --git a/frontend/src/lib/oauth.ts b/frontend/src/lib/oauth.ts index 52a6d6b..dcd6d5b 100644 --- a/frontend/src/lib/oauth.ts +++ b/frontend/src/lib/oauth.ts @@ -34,10 +34,7 @@ function sha256(plain: string): Promise { function base64UrlEncode(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); - let binary = ""; - for (const byte of bytes) { - binary += String.fromCharCode(byte); - } + const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace( /=+$/, "", diff --git a/frontend/src/lib/registration/VerificationStep.svelte b/frontend/src/lib/registration/VerificationStep.svelte index 84981b3..a297e35 100644 --- a/frontend/src/lib/registration/VerificationStep.svelte +++ b/frontend/src/lib/registration/VerificationStep.svelte @@ -1,5 +1,6 @@ -{#if auth.session?.isAdmin} +{#if session?.isAdmin}
{$_('common.backToDashboard')} @@ -314,9 +321,6 @@ {#if loading}

{$_('admin.loading')}

{:else} - {#if error} -
{error}
- {/if}

{$_('admin.serverConfig')}

@@ -428,12 +432,6 @@
- {#if serverConfigError} -
{serverConfigError}
- {/if} - {#if serverConfigSuccess} -
{$_('admin.configSaved')}
- {/if} @@ -476,9 +474,6 @@ {usersLoading ? $_('admin.loading') : $_('admin.searchUsers')} - {#if usersError} -
{usersError}
- {/if} {#if showUsers}
{#if users.length === 0} @@ -528,9 +523,6 @@ {invitesLoading ? $_('admin.loading') : showInvites ? $_('admin.refresh') : $_('admin.loadInviteCodes')}
- {#if invitesError} -
{invitesError}
- {/if} {#if showInvites}
{#if invites.length === 0} diff --git a/frontend/src/routes/AppPasswords.svelte b/frontend/src/routes/AppPasswords.svelte index 684ceb7..acccd7d 100644 --- a/frontend/src/routes/AppPasswords.svelte +++ b/frontend/src/routes/AppPasswords.svelte @@ -1,13 +1,26 @@
- {$_('common.backToDashboard')} + {$_('common.backToDashboard')}

{$_('appPasswords.title')}

{$_('appPasswords.description')}

- {#if error} -
{error}
- {/if} {#if createdPassword}
@@ -162,7 +170,11 @@

{$_('appPasswords.yourPasswords')}

{#if loading} -

{$_('common.loading')}

+
    + {#each Array(2) as _} +
  • + {/each} +
{:else if passwords.length === 0}

{$_('appPasswords.noPasswords')}

{:else} @@ -459,4 +471,15 @@ text-align: center; padding: var(--space-7); } + + .skeleton-item { + height: 60px; + background: var(--bg-tertiary); + animation: skeleton-pulse 1.5s ease-in-out infinite; + } + + @keyframes skeleton-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } diff --git a/frontend/src/routes/Comms.svelte b/frontend/src/routes/Comms.svelte index cae57ee..57c4afc 100644 --- a/frontend/src/routes/Comms.svelte +++ b/frontend/src/routes/Comms.svelte @@ -1,14 +1,26 @@
- {$_('common.backToDashboard')} + {$_('common.backToDashboard')}

{$_('comms.title')}

{$_('comms.description')}

{#if loading} -

{$_('common.loading')}

+
+
+
+
{:else} - {#if error} -
{error}
- {/if} - {#if success} -
{success}
- {/if} -
@@ -331,12 +330,6 @@
- {#if verificationError} -
{verificationError}
- {/if} - {#if verificationSuccess} -
{verificationSuccess}
- {/if}
@@ -364,8 +357,6 @@
{/each}
- {:else if historyError} -
{historyError}
{:else if messages.length === 0}

{$_('comms.noMessages')}

{:else} @@ -790,4 +781,22 @@ color: var(--text-muted); margin-top: var(--space-2); } + + .skeleton-sections { + display: flex; + flex-direction: column; + gap: var(--space-6); + } + + .skeleton-section { + height: 180px; + background: var(--bg-secondary); + border-radius: var(--radius-xl); + animation: skeleton-pulse 1.5s ease-in-out infinite; + } + + @keyframes skeleton-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } diff --git a/frontend/src/routes/Controllers.svelte b/frontend/src/routes/Controllers.svelte index 21da8fa..2b2355d 100644 --- a/frontend/src/routes/Controllers.svelte +++ b/frontend/src/routes/Controllers.svelte @@ -1,8 +1,10 @@ -{#if auth.session} +{#if session}

{$_('dashboard.title')}

{/if} -
{/if}
- {#if auth.session.status === 'migrated'} + {#if session.status === 'migrated'}
{$_('dashboard.migratedTitle')} -

{$_('dashboard.migratedMessage', { values: { pds: auth.session.migratedToPds || 'another PDS' } })}

+

{$_('dashboard.migratedMessage', { values: { pds: session.migratedToPds || 'another PDS' } })}

- {:else if auth.session.status === 'deactivated' || auth.session.active === false} + {:else if session.status === 'deactivated' || session.active === false}
{$_('dashboard.deactivatedTitle')}

{$_('dashboard.deactivatedMessage')}

@@ -118,43 +136,43 @@
{$_('dashboard.handle')}
- @{auth.session.handle} - {#if auth.session.isAdmin} + @{session.handle} + {#if session.isAdmin} {$_('dashboard.admin')} {/if} - {#if auth.session.status === 'migrated'} + {#if session.status === 'migrated'} {$_('dashboard.migrated')} - {:else if auth.session.status === 'deactivated' || auth.session.active === false} + {:else if session.status === 'deactivated' || session.active === false} {$_('dashboard.deactivated')} {/if}
{$_('dashboard.did')}
-
{auth.session.did}
- {#if auth.session.preferredChannel} +
{session.did}
+ {#if session.preferredChannel}
{$_('dashboard.primaryContact')}
- {#if auth.session.preferredChannel === 'email'} - {auth.session.email || $_('register.email')} - {:else if auth.session.preferredChannel === 'discord'} + {#if session.preferredChannel === 'email'} + {session.email || $_('register.email')} + {:else if session.preferredChannel === 'discord'} {$_('register.discord')} - {:else if auth.session.preferredChannel === 'telegram'} + {:else if session.preferredChannel === 'telegram'} {$_('register.telegram')} - {:else if auth.session.preferredChannel === 'signal'} + {:else if session.preferredChannel === 'signal'} {$_('register.signal')} {:else} - {auth.session.preferredChannel} + {session.preferredChannel} {/if} - {#if auth.session.preferredChannelVerified} + {#if session.preferredChannelVerified} {$_('dashboard.verified')} {:else} {$_('dashboard.unverified')} {/if}
- {:else if auth.session.email} + {:else if session.email}
{$_('register.email')}
- {auth.session.email} - {#if auth.session.emailConfirmed} + {session.email} + {#if session.emailConfirmed} {$_('dashboard.verified')} {:else} {$_('dashboard.unverified')} @@ -165,74 +183,74 @@
-{:else if auth.loading} -
{$_('common.loading')}
+{:else if loading} +
+
+ +
{/if} diff --git a/frontend/src/routes/DidDocumentEditor.svelte b/frontend/src/routes/DidDocumentEditor.svelte index e7b4cbb..0d4d5aa 100644 --- a/frontend/src/routes/DidDocumentEditor.svelte +++ b/frontend/src/routes/DidDocumentEditor.svelte @@ -1,15 +1,27 @@
- {$_('common.backToDashboard')} + {$_('common.backToDashboard')}

{$_('inviteCodes.title')}

{$_('inviteCodes.description')}

- {#if error} -
{error}
- {/if} {#if createdCode}

{$_('inviteCodes.created')}

@@ -108,7 +115,7 @@
{/if} - {#if auth.session?.isAdmin} + {#if session?.isAdmin}
@@ -172,7 +210,7 @@