dev vouch dev on at. thats about it atvouch.dev
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

add basic frontend version of the go cli

Luna 01e2acce 2eb4a093

+1301
+22
Makefile
··· 1 1 atvouch-cli: 2 2 cd ./cli && go build -o ../atvouch 3 + 4 + release: 5 + cd ./cli && GOOS=linux GOARCH=amd64 go build -o ../release/atvouch-linux-amd64 6 + cd ./cli && GOOS=linux GOARCH=arm64 go build -o ../release/atvouch-linux-arm64 7 + cd ./cli && GOOS=darwin GOARCH=amd64 go build -o ../release/atvouch-darwin-amd64 8 + cd ./cli && GOOS=darwin GOARCH=arm64 go build -o ../release/atvouch-darwin-arm64 9 + cd ./cli && GOOS=windows GOARCH=amd64 go build -o ../release/atvouch-windows-amd64.exe 10 + cd ./cli && GOOS=windows GOARCH=arm64 go build -o ../release/atvouch-windows-arm64.exe 11 + 12 + OAUTH_BASE_URL ?= https://atvouch.dev 13 + 14 + .PHONY: frontend 15 + frontend: 16 + cd ./frontend && OAUTH_BASE_URL=$(OAUTH_BASE_URL) bunx vite build 17 + 18 + .PHONY: local-frontend 19 + local-frontend: 20 + cd ./frontend && bunx vite 21 + 22 + test: 23 + cd ./cli && go test -v . 24 + 3 25 lexgen: 4 26 ~/other.git/indigo/lexgen --build-file ./atvouch_lexicons.json ./lexicons
+2
frontend/.gitignore
··· 1 + node_modules/ 2 + dist/
+290
frontend/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "atvouch-frontend", 7 + "dependencies": { 8 + "@atcute/client": "^2.0.3", 9 + "@atcute/identity-resolver": "^1.2.2", 10 + "@atcute/oauth-browser-client": "^2.0.1", 11 + "react": "^19.0.0", 12 + "react-dom": "^19.0.0", 13 + }, 14 + "devDependencies": { 15 + "@types/react": "^19.0.0", 16 + "@types/react-dom": "^19.0.0", 17 + "@vitejs/plugin-react": "^4.3.0", 18 + "typescript": "^5.7.2", 19 + "vite": "^6.0.0", 20 + }, 21 + }, 22 + }, 23 + "packages": { 24 + "@atcute/client": ["@atcute/client@2.0.9", "", {}, "sha512-QNDm9gMP6x9LY77ArwY+urQOBtQW74/onEAz42c40JxRm6Rl9K9cU4ROvNKJ+5cpVmEm1sthEWVRmDr5CSZENA=="], 25 + 26 + "@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="], 27 + 28 + "@atcute/identity-resolver": ["@atcute/identity-resolver@1.2.2", "", { "dependencies": { "@atcute/lexicons": "^1.2.6", "@atcute/util-fetch": "^1.0.5", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw=="], 29 + 30 + "@atcute/lexicons": ["@atcute/lexicons@1.2.9", "", { "dependencies": { "@atcute/uint8array": "^1.1.1", "@atcute/util-text": "^1.1.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g=="], 31 + 32 + "@atcute/multibase": ["@atcute/multibase@1.1.8", "", { "dependencies": { "@atcute/uint8array": "^1.1.1" } }, "sha512-pJgtImMZKCjqwRbu+2GzB+4xQjKBXDwdZOzeqe0u97zYKRGftpGYGvYv3+pMe2xXe+msDyu7Nv8iJp+U14otTA=="], 33 + 34 + "@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@2.0.3", "", { "dependencies": { "@atcute/client": "^4.1.1", "@atcute/identity-resolver": "^1.2.0", "@atcute/lexicons": "^1.2.5", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.6", "nanoid": "^5.1.6" } }, "sha512-rzUjwhjE4LRRKdQnCFQag/zXRZMEAB1hhBoLfnoQuHwWbmDUCL7fzwC3jRhDPp3om8XaYNDj8a/iqRip0wRqoQ=="], 35 + 36 + "@atcute/uint8array": ["@atcute/uint8array@1.1.1", "", {}, "sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g=="], 37 + 38 + "@atcute/util-fetch": ["@atcute/util-fetch@1.0.5", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig=="], 39 + 40 + "@atcute/util-text": ["@atcute/util-text@1.1.1", "", { "dependencies": { "unicode-segmenter": "^0.14.5" } }, "sha512-JH0SxzUQJAmbOBTYyhxQbkkI6M33YpjlVLEcbP5GYt43xgFArzV0FJVmEpvIj0kjsmphHB45b6IitdvxPdec9w=="], 41 + 42 + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], 43 + 44 + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], 45 + 46 + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], 47 + 48 + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], 49 + 50 + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], 51 + 52 + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], 53 + 54 + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], 55 + 56 + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], 57 + 58 + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], 59 + 60 + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 61 + 62 + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 63 + 64 + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], 65 + 66 + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], 67 + 68 + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], 69 + 70 + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], 71 + 72 + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], 73 + 74 + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], 75 + 76 + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], 77 + 78 + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], 79 + 80 + "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 81 + 82 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 83 + 84 + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 85 + 86 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 87 + 88 + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 89 + 90 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 91 + 92 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 93 + 94 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 95 + 96 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 97 + 98 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 99 + 100 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 101 + 102 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 103 + 104 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 105 + 106 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 107 + 108 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 109 + 110 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 111 + 112 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 113 + 114 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 115 + 116 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 117 + 118 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 119 + 120 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 121 + 122 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 123 + 124 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 125 + 126 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 127 + 128 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 129 + 130 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 131 + 132 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 133 + 134 + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], 135 + 136 + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], 137 + 138 + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 139 + 140 + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 141 + 142 + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 143 + 144 + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], 145 + 146 + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], 147 + 148 + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], 149 + 150 + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], 151 + 152 + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], 153 + 154 + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], 155 + 156 + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], 157 + 158 + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], 159 + 160 + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], 161 + 162 + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], 163 + 164 + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], 165 + 166 + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], 167 + 168 + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], 169 + 170 + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], 171 + 172 + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], 173 + 174 + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], 175 + 176 + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], 177 + 178 + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], 179 + 180 + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], 181 + 182 + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], 183 + 184 + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], 185 + 186 + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], 187 + 188 + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], 189 + 190 + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], 191 + 192 + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], 193 + 194 + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], 195 + 196 + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], 197 + 198 + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 199 + 200 + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], 201 + 202 + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], 203 + 204 + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], 205 + 206 + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 207 + 208 + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], 209 + 210 + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 211 + 212 + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], 213 + 214 + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], 215 + 216 + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], 217 + 218 + "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], 219 + 220 + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 221 + 222 + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 223 + 224 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 225 + 226 + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], 227 + 228 + "esbuild": ["esbuild@0.25.12", "", { "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/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 229 + 230 + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 231 + 232 + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 233 + 234 + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 235 + 236 + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 237 + 238 + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], 239 + 240 + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 241 + 242 + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 243 + 244 + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], 245 + 246 + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], 247 + 248 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 249 + 250 + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], 251 + 252 + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], 253 + 254 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 255 + 256 + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 257 + 258 + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], 259 + 260 + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], 261 + 262 + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], 263 + 264 + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], 265 + 266 + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], 267 + 268 + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 269 + 270 + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 271 + 272 + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 273 + 274 + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 275 + 276 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 277 + 278 + "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], 279 + 280 + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], 281 + 282 + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], 283 + 284 + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 285 + 286 + "@atcute/oauth-browser-client/@atcute/client": ["@atcute/client@4.2.1", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.6" } }, "sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw=="], 287 + 288 + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 289 + } 290 + }
+12
frontend/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>atvouch</title> 7 + </head> 8 + <body> 9 + <div id="root"></div> 10 + <script type="module" src="/src/main.tsx"></script> 11 + </body> 12 + </html>
+24
frontend/package.json
··· 1 + { 2 + "name": "atvouch-frontend", 3 + "version": "0.1.0", 4 + "type": "module", 5 + "scripts": { 6 + "dev": "vite", 7 + "build": "vite build", 8 + "typecheck": "tsc --noEmit" 9 + }, 10 + "dependencies": { 11 + "@atcute/client": "^2.0.3", 12 + "@atcute/identity-resolver": "^1.2.2", 13 + "@atcute/oauth-browser-client": "^2.0.1", 14 + "react": "^19.0.0", 15 + "react-dom": "^19.0.0" 16 + }, 17 + "devDependencies": { 18 + "@types/react": "^19.0.0", 19 + "@types/react-dom": "^19.0.0", 20 + "@vitejs/plugin-react": "^4.3.0", 21 + "typescript": "^5.7.2", 22 + "vite": "^6.0.0" 23 + } 24 + }
+12
frontend/public/client-metadata.json
··· 1 + { 2 + "client_id": "http://127.0.0.1:3051/client-metadata.json", 3 + "client_name": "atvouch", 4 + "client_uri": "http://127.0.0.1:3051", 5 + "redirect_uris": ["http://127.0.0.1:3051/"], 6 + "scope": "atproto repo:dev.atvouch.graph.vouch", 7 + "grant_types": ["authorization_code", "refresh_token"], 8 + "response_types": ["code"], 9 + "token_endpoint_auth_method": "none", 10 + "application_type": "web", 11 + "dpop_bound_access_tokens": true 12 + }
+198
frontend/src/App.css
··· 1 + * { 2 + box-sizing: border-box; 3 + margin: 0; 4 + padding: 0; 5 + } 6 + 7 + body { 8 + font-family: system-ui, -apple-system, sans-serif; 9 + background: #0a0a0a; 10 + color: #e0e0e0; 11 + line-height: 1.5; 12 + } 13 + 14 + .container { 15 + max-width: 900px; 16 + margin: 0 auto; 17 + padding: 2rem 1rem; 18 + } 19 + 20 + h1 { 21 + font-size: 1.5rem; 22 + margin-bottom: 1.5rem; 23 + color: #fff; 24 + } 25 + 26 + h2 { 27 + font-size: 1.1rem; 28 + margin-bottom: 0.75rem; 29 + color: #ccc; 30 + } 31 + 32 + .topbar { 33 + display: flex; 34 + align-items: center; 35 + justify-content: space-between; 36 + gap: 1rem; 37 + padding: 0.5rem 0; 38 + border-bottom: 1px solid #222; 39 + margin-bottom: 1rem; 40 + } 41 + 42 + .topbar code { 43 + font-size: 0.85rem; 44 + color: #aaa; 45 + } 46 + 47 + .layout { 48 + display: flex; 49 + gap: 1.5rem; 50 + align-items: flex-start; 51 + } 52 + 53 + .sidebar { 54 + width: 240px; 55 + flex-shrink: 0; 56 + border: 1px solid #222; 57 + border-radius: 8px; 58 + padding: 1rem; 59 + } 60 + 61 + .main-panel { 62 + flex: 1; 63 + min-width: 0; 64 + } 65 + 66 + section { 67 + margin-bottom: 1.5rem; 68 + padding: 1rem; 69 + border: 1px solid #222; 70 + border-radius: 8px; 71 + } 72 + 73 + section:last-child { 74 + margin-bottom: 0; 75 + } 76 + 77 + .muted { 78 + color: #666; 79 + font-size: 0.85rem; 80 + } 81 + 82 + .field { 83 + display: flex; 84 + gap: 0.5rem; 85 + } 86 + 87 + input[type="text"] { 88 + flex: 1; 89 + padding: 0.5rem 0.75rem; 90 + border: 1px solid #333; 91 + border-radius: 6px; 92 + background: #141414; 93 + color: #e0e0e0; 94 + font-size: 0.9rem; 95 + } 96 + 97 + input[type="text"]::placeholder { 98 + color: #666; 99 + } 100 + 101 + input[type="text"]:focus { 102 + outline: none; 103 + border-color: #555; 104 + } 105 + 106 + button { 107 + padding: 0.5rem 1rem; 108 + border: 1px solid #333; 109 + border-radius: 6px; 110 + background: #1a1a1a; 111 + color: #e0e0e0; 112 + cursor: pointer; 113 + font-size: 0.9rem; 114 + white-space: nowrap; 115 + } 116 + 117 + button:hover:not(:disabled) { 118 + background: #252525; 119 + border-color: #444; 120 + } 121 + 122 + button:disabled { 123 + opacity: 0.5; 124 + cursor: not-allowed; 125 + } 126 + 127 + pre { 128 + background: #141414; 129 + border: 1px solid #222; 130 + border-radius: 6px; 131 + padding: 0.75rem; 132 + overflow-x: auto; 133 + font-size: 0.85rem; 134 + margin-top: 0.5rem; 135 + white-space: pre-wrap; 136 + word-break: break-all; 137 + } 138 + 139 + .error { 140 + color: #f87171; 141 + background: #1c0a0a; 142 + border: 1px solid #3b1111; 143 + border-radius: 6px; 144 + padding: 0.5rem 0.75rem; 145 + margin-top: 0.5rem; 146 + font-size: 0.85rem; 147 + } 148 + 149 + .success { 150 + color: #4ade80; 151 + border-color: #113b11; 152 + } 153 + 154 + form { 155 + margin: 0; 156 + } 157 + 158 + .vouch-list { 159 + list-style: none; 160 + margin-top: 0.5rem; 161 + } 162 + 163 + .vouch-list li { 164 + display: flex; 165 + justify-content: space-between; 166 + align-items: center; 167 + padding: 0.4rem 0; 168 + border-bottom: 1px solid #1a1a1a; 169 + font-size: 0.85rem; 170 + gap: 0.5rem; 171 + } 172 + 173 + .vouch-list li:last-child { 174 + border-bottom: none; 175 + } 176 + 177 + .vouch-handle { 178 + color: #e0e0e0; 179 + overflow: hidden; 180 + text-overflow: ellipsis; 181 + white-space: nowrap; 182 + } 183 + 184 + .vouch-date { 185 + color: #666; 186 + font-size: 0.75rem; 187 + flex-shrink: 0; 188 + } 189 + 190 + @media (max-width: 640px) { 191 + .layout { 192 + flex-direction: column; 193 + } 194 + 195 + .sidebar { 196 + width: 100%; 197 + } 198 + }
+293
frontend/src/App.tsx
··· 1 + import { useState, useEffect, useCallback, type FormEvent } from "react"; 2 + import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 + import { initOAuth, startLogin, handleCallback, resumeSession, logout } from "./auth"; 4 + import { 5 + getSessionInfo, 6 + createVouch, 7 + listVouches, 8 + checkVouchPaths, 9 + type SessionInfo, 10 + type CheckResult, 11 + type VouchEntry, 12 + } from "./api"; 13 + 14 + initOAuth(); 15 + 16 + export function App() { 17 + const [agent, setAgent] = useState<OAuthUserAgent | null>(null); 18 + const [loading, setLoading] = useState(true); 19 + const [error, setError] = useState<string | null>(null); 20 + 21 + useEffect(() => { 22 + (async () => { 23 + try { 24 + const callbackAgent = await handleCallback(); 25 + if (callbackAgent) { 26 + setAgent(callbackAgent); 27 + setLoading(false); 28 + return; 29 + } 30 + const resumed = await resumeSession(); 31 + if (resumed) setAgent(resumed); 32 + } catch (err) { 33 + setError(String(err)); 34 + } 35 + setLoading(false); 36 + })(); 37 + }, []); 38 + 39 + const handleLogout = useCallback(async () => { 40 + if (!agent) return; 41 + await logout(agent); 42 + setAgent(null); 43 + }, [agent]); 44 + 45 + if (loading) return <div className="container"><p>Loading...</p></div>; 46 + 47 + return ( 48 + <div className="container"> 49 + <h1>atvouch</h1> 50 + {error && <div className="error">{error}</div>} 51 + {!agent ? ( 52 + <LoginForm /> 53 + ) : ( 54 + <Dashboard agent={agent} onLogout={handleLogout} /> 55 + )} 56 + </div> 57 + ); 58 + } 59 + 60 + function LoginForm() { 61 + const [handle, setHandle] = useState(""); 62 + const [submitting, setSubmitting] = useState(false); 63 + const [error, setError] = useState<string | null>(null); 64 + 65 + const onSubmit = async (e: FormEvent) => { 66 + e.preventDefault(); 67 + if (!handle.trim()) return; 68 + setSubmitting(true); 69 + setError(null); 70 + try { 71 + await startLogin(handle.trim()); 72 + } catch (err) { 73 + setError(String(err)); 74 + setSubmitting(false); 75 + } 76 + }; 77 + 78 + return ( 79 + <form onSubmit={onSubmit}> 80 + <h2>Login</h2> 81 + <div className="field"> 82 + <input 83 + type="text" 84 + placeholder="handle (e.g. alice.bsky.social)" 85 + value={handle} 86 + onChange={(e) => setHandle(e.target.value)} 87 + disabled={submitting} 88 + /> 89 + <button type="submit" disabled={submitting || !handle.trim()}> 90 + {submitting ? "Redirecting..." : "Login"} 91 + </button> 92 + </div> 93 + {error && <div className="error">{error}</div>} 94 + </form> 95 + ); 96 + } 97 + 98 + function Dashboard({ agent, onLogout }: { agent: OAuthUserAgent; onLogout: () => void }) { 99 + const [vouches, setVouches] = useState<VouchEntry[] | null>(null); 100 + const [vouchesLoading, setVouchesLoading] = useState(true); 101 + const [vouchesError, setVouchesError] = useState<string | null>(null); 102 + 103 + const refreshVouches = useCallback(async () => { 104 + setVouchesLoading(true); 105 + setVouchesError(null); 106 + try { 107 + setVouches(await listVouches(agent)); 108 + } catch (err) { 109 + setVouchesError(String(err)); 110 + } 111 + setVouchesLoading(false); 112 + }, [agent]); 113 + 114 + useEffect(() => { 115 + refreshVouches(); 116 + }, [refreshVouches]); 117 + 118 + return ( 119 + <div> 120 + <div className="topbar"> 121 + <span>Logged in as <code>{agent.sub}</code></span> 122 + <button onClick={onLogout}>Logout</button> 123 + </div> 124 + <div className="layout"> 125 + <aside className="sidebar"> 126 + <h2>Your vouches</h2> 127 + {vouchesError && <div className="error">{vouchesError}</div>} 128 + {vouchesLoading ? ( 129 + <p className="muted">Loading...</p> 130 + ) : vouches !== null && vouches.length === 0 ? ( 131 + <p className="muted">No vouches yet.</p> 132 + ) : ( 133 + <ul className="vouch-list"> 134 + {vouches?.map((v) => ( 135 + <li key={v.did}> 136 + <span className="vouch-handle">{v.handle ?? v.did}</span> 137 + <span className="vouch-date">{new Date(v.createdAt).toLocaleDateString()}</span> 138 + </li> 139 + ))} 140 + </ul> 141 + )} 142 + </aside> 143 + <main className="main-panel"> 144 + <MeSection agent={agent} /> 145 + <CreateVouchSection agent={agent} onVouchCreated={refreshVouches} /> 146 + <CheckVouchSection agent={agent} /> 147 + </main> 148 + </div> 149 + </div> 150 + ); 151 + } 152 + 153 + function MeSection({ agent }: { agent: OAuthUserAgent }) { 154 + const [info, setInfo] = useState<SessionInfo | null>(null); 155 + const [loading, setLoading] = useState(false); 156 + const [error, setError] = useState<string | null>(null); 157 + 158 + const fetchMe = async () => { 159 + setLoading(true); 160 + setError(null); 161 + try { 162 + setInfo(await getSessionInfo(agent)); 163 + } catch (err) { 164 + setError(String(err)); 165 + } 166 + setLoading(false); 167 + }; 168 + 169 + return ( 170 + <section> 171 + <h2>Session</h2> 172 + <button onClick={fetchMe} disabled={loading}> 173 + {loading ? "Loading..." : "Show session info"} 174 + </button> 175 + {error && <div className="error">{error}</div>} 176 + {info && ( 177 + <pre>{JSON.stringify(info, null, 2)}</pre> 178 + )} 179 + </section> 180 + ); 181 + } 182 + 183 + function CreateVouchSection({ agent, onVouchCreated }: { agent: OAuthUserAgent; onVouchCreated: () => void }) { 184 + const [handle, setHandle] = useState(""); 185 + const [submitting, setSubmitting] = useState(false); 186 + const [result, setResult] = useState<string | null>(null); 187 + const [error, setError] = useState<string | null>(null); 188 + 189 + const onSubmit = async (e: FormEvent) => { 190 + e.preventDefault(); 191 + if (!handle.trim()) return; 192 + setSubmitting(true); 193 + setError(null); 194 + setResult(null); 195 + try { 196 + const res = await createVouch(agent, handle.trim()); 197 + setResult(`Vouched for ${handle.trim()} (${res.subjectDid})\nRecord: ${res.uri}`); 198 + setHandle(""); 199 + onVouchCreated(); 200 + } catch (err) { 201 + setError(String(err)); 202 + } 203 + setSubmitting(false); 204 + }; 205 + 206 + return ( 207 + <section> 208 + <h2>Create vouch</h2> 209 + <form onSubmit={onSubmit}> 210 + <div className="field"> 211 + <input 212 + type="text" 213 + placeholder="handle to vouch for" 214 + value={handle} 215 + onChange={(e) => setHandle(e.target.value)} 216 + disabled={submitting} 217 + /> 218 + <button type="submit" disabled={submitting || !handle.trim()}> 219 + {submitting ? "Creating..." : "Vouch"} 220 + </button> 221 + </div> 222 + </form> 223 + {error && <div className="error">{error}</div>} 224 + {result && <pre className="success">{result}</pre>} 225 + </section> 226 + ); 227 + } 228 + 229 + function CheckVouchSection({ agent }: { agent: OAuthUserAgent }) { 230 + const [handle, setHandle] = useState(""); 231 + const [loading, setLoading] = useState(false); 232 + const [result, setResult] = useState<{ data: CheckResult; handle: string } | null>(null); 233 + const [error, setError] = useState<string | null>(null); 234 + 235 + const onSubmit = async (e: FormEvent) => { 236 + e.preventDefault(); 237 + const trimmed = handle.trim(); 238 + if (!trimmed) return; 239 + setLoading(true); 240 + setError(null); 241 + setResult(null); 242 + try { 243 + const data = await checkVouchPaths(agent, trimmed); 244 + setResult({ data, handle: trimmed }); 245 + } catch (err) { 246 + setError(String(err)); 247 + } 248 + setLoading(false); 249 + }; 250 + 251 + return ( 252 + <section> 253 + <h2>Check vouch paths</h2> 254 + <form onSubmit={onSubmit}> 255 + <div className="field"> 256 + <input 257 + type="text" 258 + placeholder="handle to check" 259 + value={handle} 260 + onChange={(e) => setHandle(e.target.value)} 261 + disabled={loading} 262 + /> 263 + <button type="submit" disabled={loading || !handle.trim()}> 264 + {loading ? "Checking..." : "Check"} 265 + </button> 266 + </div> 267 + </form> 268 + {error && <div className="error">{error}</div>} 269 + {result && <CheckResultDisplay result={result.data} handle={result.handle} />} 270 + </section> 271 + ); 272 + } 273 + 274 + function CheckResultDisplay({ result, handle }: { result: CheckResult; handle: string }) { 275 + if (result.directVouch) { 276 + return <pre className="success">you -&gt; {handle}</pre>; 277 + } 278 + 279 + if (result.paths.length === 0) { 280 + return <pre>No vouch routes found</pre>; 281 + } 282 + 283 + return ( 284 + <div> 285 + <p>Found {result.paths.length} vouch route(s):</p> 286 + <pre> 287 + {result.paths 288 + .map((path) => path.map((did) => result.handleMap[did] ?? did).join(" -> ")) 289 + .join("\n")} 290 + </pre> 291 + </div> 292 + ); 293 + }
+268
frontend/src/api.ts
··· 1 + import { XRPC } from "@atcute/client"; 2 + import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 + 4 + // Slingshot: handle <-> DID resolution 5 + const SLINGSHOT = "https://slingshot.microcosm.blue"; 6 + // Microcosm: reverse graph queries 7 + const MICROCOSM = "https://constellation.microcosm.blue"; 8 + 9 + export async function resolveHandle(handle: string): Promise<string> { 10 + const url = `${SLINGSHOT}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`; 11 + const resp = await fetch(url); 12 + if (!resp.ok) throw new Error(`Failed to resolve handle: ${resp.status}`); 13 + const data: { did: string } = await resp.json(); 14 + return data.did; 15 + } 16 + 17 + export async function resolveDidToHandle(did: string): Promise<string> { 18 + const url = `${SLINGSHOT}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`; 19 + const resp = await fetch(url); 20 + if (!resp.ok) throw new Error(`Failed to resolve DID: ${resp.status}`); 21 + const data: { handle: string } = await resp.json(); 22 + return data.handle; 23 + } 24 + 25 + async function fetchVouchers(targetDID: string): Promise<string[]> { 26 + const params = new URLSearchParams({ 27 + target: targetDID, 28 + collection: "dev.atvouch.graph.vouch", 29 + path: ".subject", 30 + }); 31 + const resp = await fetch(`${MICROCOSM}/links/distinct-dids?${params}`, { 32 + headers: { Accept: "application/json" }, 33 + }); 34 + if (!resp.ok) throw new Error(`Microcosm error: ${resp.status}`); 35 + const data: { linking_dids: string[] } = await resp.json(); 36 + return data.linking_dids; 37 + } 38 + 39 + export interface SessionInfo { 40 + did: string; 41 + handle: string; 42 + email?: string; 43 + emailConfirmed?: boolean; 44 + } 45 + 46 + export async function getSessionInfo(agent: OAuthUserAgent): Promise<SessionInfo> { 47 + const rpc = new XRPC({ handler: agent }); 48 + const resp = await rpc.get("com.atproto.server.getSession", {}); 49 + return resp.data as unknown as SessionInfo; 50 + } 51 + 52 + export async function createVouch(agent: OAuthUserAgent, handle: string): Promise<{ uri: string; subjectDid: string }> { 53 + const rpc = new XRPC({ handler: agent }); 54 + 55 + // Resolve handle to DID 56 + const resolveResp = await rpc.get("com.atproto.identity.resolveHandle", { 57 + params: { handle }, 58 + }); 59 + const subjectDid = (resolveResp.data as unknown as { did: string }).did; 60 + 61 + // Check if already vouched 62 + const existing = await listMyVouches(agent); 63 + if (existing.includes(subjectDid)) { 64 + throw new Error(`You have already vouched for ${handle}`); 65 + } 66 + 67 + // Generate TID (timestamp-based ID) 68 + const tid = generateTID(); 69 + 70 + // Create vouch record 71 + const createResp = await rpc.call("com.atproto.repo.createRecord", { 72 + data: { 73 + repo: agent.sub, 74 + collection: "dev.atvouch.graph.vouch", 75 + rkey: tid, 76 + record: { 77 + $type: "dev.atvouch.graph.vouch", 78 + subject: subjectDid, 79 + createdAt: new Date().toISOString(), 80 + }, 81 + }, 82 + }); 83 + 84 + return { 85 + uri: (createResp.data as unknown as { uri: string }).uri, 86 + subjectDid, 87 + }; 88 + } 89 + 90 + // List all DIDs the user has vouched for 91 + async function listMyVouches(agent: OAuthUserAgent): Promise<string[]> { 92 + const rpc = new XRPC({ handler: agent }); 93 + const subjects: string[] = []; 94 + let cursor: string | undefined; 95 + 96 + for (;;) { 97 + const resp = await rpc.get("com.atproto.repo.listRecords", { 98 + params: { 99 + repo: agent.sub, 100 + collection: "dev.atvouch.graph.vouch", 101 + limit: 100, 102 + cursor, 103 + }, 104 + }); 105 + 106 + const data = resp.data as unknown as { 107 + records: Array<{ value: { subject: string } }>; 108 + cursor?: string; 109 + }; 110 + 111 + for (const rec of data.records) { 112 + if (rec.value.subject) subjects.push(rec.value.subject); 113 + } 114 + 115 + if (!data.cursor) break; 116 + cursor = data.cursor; 117 + } 118 + 119 + return subjects; 120 + } 121 + 122 + export interface VouchEntry { 123 + did: string; 124 + handle: string | null; 125 + createdAt: string; 126 + } 127 + 128 + export async function listVouches(agent: OAuthUserAgent): Promise<VouchEntry[]> { 129 + const rpc = new XRPC({ handler: agent }); 130 + const entries: VouchEntry[] = []; 131 + let cursor: string | undefined; 132 + 133 + for (;;) { 134 + const resp = await rpc.get("com.atproto.repo.listRecords", { 135 + params: { 136 + repo: agent.sub, 137 + collection: "dev.atvouch.graph.vouch", 138 + limit: 100, 139 + cursor, 140 + }, 141 + }); 142 + 143 + const data = resp.data as unknown as { 144 + records: Array<{ value: { subject: string; createdAt: string } }>; 145 + cursor?: string; 146 + }; 147 + 148 + for (const rec of data.records) { 149 + if (rec.value.subject) { 150 + entries.push({ 151 + did: rec.value.subject, 152 + handle: null, 153 + createdAt: rec.value.createdAt, 154 + }); 155 + } 156 + } 157 + 158 + if (!data.cursor) break; 159 + cursor = data.cursor; 160 + } 161 + 162 + // Resolve DIDs to handles 163 + await Promise.all( 164 + entries.map(async (entry) => { 165 + try { 166 + entry.handle = await resolveDidToHandle(entry.did); 167 + } catch { 168 + // leave as null, display DID instead 169 + } 170 + }), 171 + ); 172 + 173 + return entries; 174 + } 175 + 176 + export interface CheckResult { 177 + targetDID: string; 178 + directVouch: boolean; 179 + paths: string[][]; 180 + handleMap: Record<string, string>; 181 + } 182 + 183 + export async function checkVouchPaths(agent: OAuthUserAgent, handle: string): Promise<CheckResult> { 184 + const targetDID = await resolveHandle(handle); 185 + const myDID = agent.sub; 186 + const myVouches = await listMyVouches(agent); 187 + 188 + // Direct vouch check 189 + if (myVouches.includes(targetDID)) { 190 + return { targetDID, directVouch: true, paths: [], handleMap: {} }; 191 + } 192 + 193 + // Build reverse graph from target (up to 3 levels) 194 + const reverseGraph: Record<string, Set<string>> = {}; 195 + 196 + // Level 1: who vouches for target 197 + const level1 = await fetchVouchers(targetDID); 198 + reverseGraph[targetDID] = new Set(level1); 199 + 200 + // Level 2: who vouches for each level-1 voucher 201 + const level2DIDs: string[] = []; 202 + for (const did of level1) { 203 + const vouchers = await fetchVouchers(did); 204 + reverseGraph[did] = new Set(vouchers); 205 + level2DIDs.push(...vouchers); 206 + } 207 + 208 + // Level 3: who vouches for each level-2 voucher 209 + for (const did of level2DIDs) { 210 + if (reverseGraph[did]) continue; 211 + const vouchers = await fetchVouchers(did); 212 + reverseGraph[did] = new Set(vouchers); 213 + } 214 + 215 + // Find paths 216 + const myVouchSet = new Set(myVouches); 217 + const paths: string[][] = []; 218 + 219 + // Depth 2: me -> X -> target 220 + for (const voucher of reverseGraph[targetDID] ?? []) { 221 + if (myVouchSet.has(voucher)) { 222 + paths.push([myDID, voucher, targetDID]); 223 + } 224 + } 225 + 226 + // Depth 3: me -> X -> Y -> target 227 + for (const yDID of reverseGraph[targetDID] ?? []) { 228 + for (const xDID of reverseGraph[yDID] ?? []) { 229 + if (myVouchSet.has(xDID)) { 230 + paths.push([myDID, xDID, yDID, targetDID]); 231 + } 232 + } 233 + } 234 + 235 + // Resolve DIDs to handles 236 + const handleMap: Record<string, string> = {}; 237 + if (paths.length > 0) { 238 + const uniqueDIDs = new Set(paths.flat()); 239 + handleMap[targetDID] = handle; 240 + 241 + for (const did of uniqueDIDs) { 242 + if (handleMap[did]) continue; 243 + try { 244 + handleMap[did] = await resolveDidToHandle(did); 245 + } catch { 246 + handleMap[did] = did; 247 + } 248 + } 249 + } 250 + 251 + return { targetDID, directVouch: false, paths, handleMap }; 252 + } 253 + 254 + // Generate a TID (Timestamp ID) matching AT Protocol spec 255 + function generateTID(): string { 256 + const now = BigInt(Date.now()) * 1000n; 257 + const clockId = BigInt(Math.floor(Math.random() * 1024)); 258 + const tid = (now << 10n) | clockId; 259 + 260 + const chars = "234567abcdefghijklmnopqrstuvwxyz"; 261 + let result = ""; 262 + let val = tid; 263 + for (let i = 0; i < 13; i++) { 264 + result = chars[Number(val & 31n)] + result; 265 + val >>= 5n; 266 + } 267 + return result; 268 + }
+89
frontend/src/auth.ts
··· 1 + import { 2 + configureOAuth, 3 + createAuthorizationUrl, 4 + finalizeAuthorization, 5 + getSession, 6 + deleteStoredSession, 7 + OAuthUserAgent, 8 + } from "@atcute/oauth-browser-client"; 9 + import type { Did, ActorIdentifier } from "@atcute/lexicons"; 10 + import { 11 + CompositeDidDocumentResolver, 12 + LocalActorResolver, 13 + PlcDidDocumentResolver, 14 + WebDidDocumentResolver, 15 + XrpcHandleResolver, 16 + } from "@atcute/identity-resolver"; 17 + 18 + const STORAGE_KEY = "atvouch_did"; 19 + 20 + export function initOAuth() { 21 + configureOAuth({ 22 + metadata: { 23 + client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 24 + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI, 25 + }, 26 + identityResolver: new LocalActorResolver({ 27 + handleResolver: new XrpcHandleResolver({ 28 + serviceUrl: "https://public.api.bsky.app", 29 + }), 30 + didDocumentResolver: new CompositeDidDocumentResolver({ 31 + methods: { 32 + plc: new PlcDidDocumentResolver(), 33 + web: new WebDidDocumentResolver(), 34 + }, 35 + }), 36 + }), 37 + }); 38 + } 39 + 40 + export async function startLogin(handle: string): Promise<void> { 41 + const authUrl = await createAuthorizationUrl({ 42 + scope: import.meta.env.VITE_OAUTH_SCOPE, 43 + target: { type: "account", identifier: handle as ActorIdentifier }, 44 + }); 45 + window.location.assign(authUrl); 46 + } 47 + 48 + export async function handleCallback(): Promise<OAuthUserAgent | null> { 49 + const params = new URLSearchParams(window.location.hash.slice(1) || window.location.search); 50 + if (!params.has("code") && !params.has("error")) { 51 + return null; 52 + } 53 + 54 + // Clean up the URL 55 + history.replaceState(null, "", window.location.pathname); 56 + 57 + if (params.has("error")) { 58 + throw new Error(`OAuth error: ${params.get("error")} - ${params.get("error_description")}`); 59 + } 60 + 61 + const result = await finalizeAuthorization(params); 62 + const agent = new OAuthUserAgent(result.session); 63 + localStorage.setItem(STORAGE_KEY, agent.sub); 64 + return agent; 65 + } 66 + 67 + export async function resumeSession(): Promise<OAuthUserAgent | null> { 68 + const did = localStorage.getItem(STORAGE_KEY); 69 + if (!did) return null; 70 + 71 + try { 72 + const session = await getSession(did as Did, { allowStale: true }); 73 + return new OAuthUserAgent(session); 74 + } catch { 75 + localStorage.removeItem(STORAGE_KEY); 76 + return null; 77 + } 78 + } 79 + 80 + export async function logout(agent: OAuthUserAgent): Promise<void> { 81 + const did = agent.sub; 82 + try { 83 + await agent.signOut(); 84 + } catch { 85 + // ignore signout errors 86 + } 87 + await deleteStoredSession(did); 88 + localStorage.removeItem(STORAGE_KEY); 89 + }
+10
frontend/src/main.tsx
··· 1 + import { StrictMode } from "react"; 2 + import { createRoot } from "react-dom/client"; 3 + import { App } from "./App"; 4 + import "./App.css"; 5 + 6 + createRoot(document.getElementById("root")!).render( 7 + <StrictMode> 8 + <App /> 9 + </StrictMode>, 10 + );
+1
frontend/src/vite-env.d.ts
··· 1 + /// <reference types="vite/client" />
+22
frontend/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 6 + "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 + "jsx": "react-jsx", 8 + "strict": true, 9 + "noEmit": true, 10 + "skipLibCheck": true, 11 + "esModuleInterop": true, 12 + "allowSyntheticDefaultImports": true, 13 + "forceConsistentCasingInFileNames": true, 14 + "resolveJsonModule": true, 15 + "isolatedModules": true, 16 + "noUnusedLocals": true, 17 + "noUnusedParameters": true, 18 + "noFallthroughCasesInSwitch": true 19 + }, 20 + "include": ["src/**/*.ts", "src/**/*.tsx"], 21 + "exclude": ["node_modules", "dist"] 22 + }
+58
frontend/vite.config.ts
··· 1 + import { writeFileSync } from "fs"; 2 + import { resolve } from "path"; 3 + import { defineConfig } from "vite"; 4 + import react from "@vitejs/plugin-react"; 5 + import metadata from "./public/client-metadata.json" with { type: "json" }; 6 + 7 + const SERVER_HOST = "127.0.0.1"; 8 + const SERVER_PORT = 3051; 9 + 10 + export default defineConfig({ 11 + plugins: [ 12 + react(), 13 + { 14 + name: "atvouch-oauth", 15 + config(_conf, { command }) { 16 + if (command === "build") { 17 + const baseUrl = process.env.OAUTH_BASE_URL; 18 + if (!baseUrl) { 19 + throw new Error("OAUTH_BASE_URL environment variable is required for production builds"); 20 + } 21 + process.env.VITE_OAUTH_CLIENT_ID = `${baseUrl}/client-metadata.json`; 22 + process.env.VITE_OAUTH_REDIRECT_URI = `${baseUrl}/`; 23 + } else { 24 + const redirectUri = `http://${SERVER_HOST}:${SERVER_PORT}${new URL(metadata.redirect_uris[0]).pathname}`; 25 + process.env.VITE_OAUTH_CLIENT_ID = 26 + `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}` + 27 + `&scope=${encodeURIComponent(metadata.scope)}`; 28 + process.env.VITE_OAUTH_REDIRECT_URI = redirectUri; 29 + } 30 + process.env.VITE_OAUTH_SCOPE = metadata.scope; 31 + }, 32 + writeBundle() { 33 + const baseUrl = process.env.OAUTH_BASE_URL; 34 + if (!baseUrl) return; 35 + const prodMetadata = { 36 + client_id: `${baseUrl}/client-metadata.json`, 37 + client_name: "atvouch", 38 + client_uri: baseUrl, 39 + redirect_uris: [`${baseUrl}/`], 40 + scope: metadata.scope, 41 + grant_types: ["authorization_code", "refresh_token"], 42 + response_types: ["code"], 43 + token_endpoint_auth_method: "none", 44 + application_type: "web", 45 + dpop_bound_access_tokens: true, 46 + }; 47 + writeFileSync( 48 + resolve(__dirname, "dist/client-metadata.json"), 49 + JSON.stringify(prodMetadata, null, 2), 50 + ); 51 + }, 52 + }, 53 + ], 54 + server: { 55 + host: SERVER_HOST, 56 + port: SERVER_PORT, 57 + }, 58 + });