handy online tools for AT Protocol boat.kelinci.net
atproto bluesky atcute typescript solidjs

Compare changes

Choose any two refs to compare.

Changed files
+5977 -2477
.vscode
src
+3
.gitignore
··· 1 1 node_modules/ 2 2 dist/ 3 3 4 + /.wrangler/ 5 + /.research/ 6 + 4 7 *.local 5 8 *.local.ts 6 9
-2
.npmrc
··· 1 1 auto-install-peers=false 2 2 public-hoist-pattern[]=workbox-window 3 - 4 - @jsr:registry=https://npm.jsr.io
+1 -1
.vscode/settings.json
··· 1 1 { 2 - "editor.defaultFormatter": "esbenp.prettier-vscode", 2 + "editor.defaultFormatter": "prettier.prettier-vscode", 3 3 "typescript.tsdk": "node_modules/typescript/lib", 4 4 "tailwindCSS.experimental.classRegex": ["tw`([^`]*)"] 5 5 }
+14
LICENSE
··· 1 + BSD Zero Clause License 2 + 3 + Copyright (c) 2025 Mary 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted. 7 + 8 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 + PERFORMANCE OF THIS SOFTWARE.
+32 -23
package.json
··· 4 4 "dev": "vite", 5 5 "build": "tsc -b && vite build", 6 6 "preview": "vite preview", 7 - "fmt": "prettier --cache --write ." 7 + "fmt": "PRETTIER_EXPERIMENTAL_CLI=1 prettier --cache --write ." 8 8 }, 9 9 "dependencies": { 10 - "@atcute/bluesky": "^1.0.12", 11 - "@atcute/car": "^2.0.1", 12 - "@atcute/cbor": "^2.1.1", 13 - "@atcute/client": "^2.0.7", 14 - "@atcute/crypto": "^2.2.0", 15 - "@atcute/multibase": "^1.1.2", 16 - "@badrap/valita": "^0.4.2", 17 - "@mary/array-fns": "npm:@jsr/mary__array-fns@^0.1.0", 18 - "@mary/events": "npm:@jsr/mary__events@^0.1.0", 10 + "@atcute/atproto": "^3.1.9", 11 + "@atcute/bluesky": "^3.2.13", 12 + "@atcute/car": "^5.0.0", 13 + "@atcute/cbor": "^2.2.8", 14 + "@atcute/cid": "^2.2.6", 15 + "@atcute/client": "^4.1.1", 16 + "@atcute/crypto": "^2.3.0", 17 + "@atcute/did-plc": "^0.2.0", 18 + "@atcute/identity": "^1.1.3", 19 + "@atcute/identity-resolver": "^1.2.0", 20 + "@atcute/lexicons": "^1.2.5", 21 + "@atcute/multibase": "^1.1.6", 22 + "@atcute/repo": "^0.1.0", 23 + "@atcute/tid": "^1.0.3", 24 + "@badrap/valita": "^0.4.6", 25 + "@mary/array-fns": "jsr:^0.1.5", 26 + "@mary/ds-queue": "jsr:^0.1.3", 27 + "@mary/events": "jsr:^0.2.0", 19 28 "@mary/solid-freeze": "npm:@externdefs/solid-freeze@^0.1.1", 20 - "@mary/tar": "npm:@jsr/mary__tar@^0.2.4", 21 - "nanoid": "^5.0.9", 29 + "@mary/tar": "jsr:^0.3.1", 30 + "nanoid": "^5.1.6", 22 31 "native-file-system-adapter": "^3.0.1", 23 - "solid-js": "^1.9.4" 32 + "solid-js": "^1.9.10" 24 33 }, 25 34 "devDependencies": { 26 35 "@tailwindcss/forms": "^0.5.10", 27 - "@types/node": "^22.12.0", 28 - "autoprefixer": "^10.4.20", 29 - "prettier": "^3.4.2", 30 - "prettier-plugin-tailwindcss": "^0.6.11", 31 - "tailwindcss": "^3.4.17", 32 - "terser": "^5.37.0", 33 - "typescript": "5.7.2", 34 - "vite": "^6.0.11", 35 - "vite-plugin-solid": "^2.11.0", 36 - "wrangler": "^3.106.0" 36 + "@types/node": "^22.19.2", 37 + "autoprefixer": "^10.4.22", 38 + "prettier": "^3.7.4", 39 + "prettier-plugin-tailwindcss": "^0.6.14", 40 + "tailwindcss": "^3.4.18", 41 + "terser": "^5.44.1", 42 + "typescript": "~5.9.3", 43 + "vite": "^7.2.7", 44 + "vite-plugin-solid": "^2.11.10", 45 + "wrangler": "^4.53.0" 37 46 } 38 47 }
+1378 -1224
pnpm-lock.yaml
··· 8 8 9 9 .: 10 10 dependencies: 11 + '@atcute/atproto': 12 + specifier: ^3.1.9 13 + version: 3.1.9 11 14 '@atcute/bluesky': 12 - specifier: ^1.0.12 13 - version: 1.0.12(@atcute/client@2.0.7) 15 + specifier: ^3.2.13 16 + version: 3.2.13 14 17 '@atcute/car': 15 - specifier: ^2.0.1 16 - version: 2.0.1 18 + specifier: ^5.0.0 19 + version: 5.0.0 17 20 '@atcute/cbor': 18 - specifier: ^2.1.1 19 - version: 2.1.1 21 + specifier: ^2.2.8 22 + version: 2.2.8 23 + '@atcute/cid': 24 + specifier: ^2.2.6 25 + version: 2.2.6 20 26 '@atcute/client': 21 - specifier: ^2.0.7 22 - version: 2.0.7 27 + specifier: ^4.1.1 28 + version: 4.1.1 23 29 '@atcute/crypto': 24 - specifier: ^2.2.0 25 - version: 2.2.0 30 + specifier: ^2.3.0 31 + version: 2.3.0 32 + '@atcute/did-plc': 33 + specifier: ^0.2.0 34 + version: 0.2.0 35 + '@atcute/identity': 36 + specifier: ^1.1.3 37 + version: 1.1.3 38 + '@atcute/identity-resolver': 39 + specifier: ^1.2.0 40 + version: 1.2.0(@atcute/identity@1.1.3) 41 + '@atcute/lexicons': 42 + specifier: ^1.2.5 43 + version: 1.2.5 26 44 '@atcute/multibase': 27 - specifier: ^1.1.2 28 - version: 1.1.2 45 + specifier: ^1.1.6 46 + version: 1.1.6 47 + '@atcute/repo': 48 + specifier: ^0.1.0 49 + version: 0.1.0 50 + '@atcute/tid': 51 + specifier: ^1.0.3 52 + version: 1.0.3 29 53 '@badrap/valita': 30 - specifier: ^0.4.2 31 - version: 0.4.2 54 + specifier: ^0.4.6 55 + version: 0.4.6 32 56 '@mary/array-fns': 33 - specifier: npm:@jsr/mary__array-fns@^0.1.0 34 - version: '@jsr/mary__array-fns@0.1.0' 57 + specifier: jsr:^0.1.5 58 + version: '@jsr/mary__array-fns@0.1.5' 59 + '@mary/ds-queue': 60 + specifier: jsr:^0.1.3 61 + version: '@jsr/mary__ds-queue@0.1.3' 35 62 '@mary/events': 36 - specifier: npm:@jsr/mary__events@^0.1.0 37 - version: '@jsr/mary__events@0.1.0' 63 + specifier: jsr:^0.2.0 64 + version: '@jsr/mary__events@0.2.0' 38 65 '@mary/solid-freeze': 39 66 specifier: npm:@externdefs/solid-freeze@^0.1.1 40 - version: '@externdefs/solid-freeze@0.1.1(solid-js@1.9.4)' 67 + version: '@externdefs/solid-freeze@0.1.1(solid-js@1.9.10)' 41 68 '@mary/tar': 42 - specifier: npm:@jsr/mary__tar@^0.2.4 43 - version: '@jsr/mary__tar@0.2.4' 69 + specifier: jsr:^0.3.1 70 + version: '@jsr/mary__tar@0.3.1' 44 71 nanoid: 45 - specifier: ^5.0.9 46 - version: 5.0.9 72 + specifier: ^5.1.6 73 + version: 5.1.6 47 74 native-file-system-adapter: 48 75 specifier: ^3.0.1 49 76 version: 3.0.1 50 77 solid-js: 51 - specifier: ^1.9.4 52 - version: 1.9.4 78 + specifier: ^1.9.10 79 + version: 1.9.10 53 80 devDependencies: 54 81 '@tailwindcss/forms': 55 82 specifier: ^0.5.10 56 - version: 0.5.10(tailwindcss@3.4.17) 83 + version: 0.5.10(tailwindcss@3.4.18) 57 84 '@types/node': 58 - specifier: ^22.12.0 59 - version: 22.12.0 85 + specifier: ^22.19.2 86 + version: 22.19.2 60 87 autoprefixer: 61 - specifier: ^10.4.20 62 - version: 10.4.20(postcss@8.5.1) 88 + specifier: ^10.4.22 89 + version: 10.4.22(postcss@8.5.6) 63 90 prettier: 64 - specifier: ^3.4.2 65 - version: 3.4.2 91 + specifier: ^3.7.4 92 + version: 3.7.4 66 93 prettier-plugin-tailwindcss: 67 - specifier: ^0.6.11 68 - version: 0.6.11(prettier@3.4.2) 94 + specifier: ^0.6.14 95 + version: 0.6.14(prettier@3.7.4) 69 96 tailwindcss: 70 - specifier: ^3.4.17 71 - version: 3.4.17 97 + specifier: ^3.4.18 98 + version: 3.4.18 72 99 terser: 73 - specifier: ^5.37.0 74 - version: 5.37.0 100 + specifier: ^5.44.1 101 + version: 5.44.1 75 102 typescript: 76 - specifier: 5.7.2 77 - version: 5.7.2 103 + specifier: ~5.9.3 104 + version: 5.9.3 78 105 vite: 79 - specifier: ^6.0.11 80 - version: 6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0) 106 + specifier: ^7.2.7 107 + version: 7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1) 81 108 vite-plugin-solid: 82 - specifier: ^2.11.0 83 - version: 2.11.0(solid-js@1.9.4)(vite@6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) 109 + specifier: ^2.11.10 110 + version: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1)) 84 111 wrangler: 85 - specifier: ^3.106.0 86 - version: 3.106.0 112 + specifier: ^4.53.0 113 + version: 4.53.0 87 114 88 115 packages: 89 116 ··· 91 118 resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 92 119 engines: {node: '>=10'} 93 120 94 - '@ampproject/remapping@2.3.0': 95 - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 96 - engines: {node: '>=6.0.0'} 121 + '@atcute/atproto@3.1.9': 122 + resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==} 123 + 124 + '@atcute/bluesky@3.2.13': 125 + resolution: {integrity: sha512-ZG/mqsCjVU6zvH6XsRw+oQglrsdu5R7mnncMO+Ux0KWbX2xJw4ZMFHfs7ZTC69dVPK9r/yle7YbpygZTOWDM9A==} 97 126 98 - '@atcute/bluesky@1.0.12': 99 - resolution: {integrity: sha512-oUM+MxD5asGYyQDOHBGay7f9ryhsBpQ8LTUmsEZvp4t/WG0ZV2AcFRWsG0DxB+CsmSTbP2DHLMZCatE3usmt+g==} 127 + '@atcute/car@5.0.0': 128 + resolution: {integrity: sha512-OIY2xTXv8lSpZsDSn/UYQtJSMvDw5Hi4Q+uyvmiqSM+fht08QRAEq/nxa5YFciPZ3nfDFnZ3//EgJw7QhkSXLQ==} 129 + 130 + '@atcute/cbor@2.2.8': 131 + resolution: {integrity: sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw==} 132 + 133 + '@atcute/cid@2.2.6': 134 + resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} 135 + 136 + '@atcute/client@4.1.1': 137 + resolution: {integrity: sha512-FROCbTTCeL5u4tO/n72jDEKyKqjdlXMB56Ehve3W/gnnLGCYWvN42sS7tvL1Mgu6sbO3yZwsXKDrmM2No4XpjA==} 138 + 139 + '@atcute/crypto@2.3.0': 140 + resolution: {integrity: sha512-w5pkJKCjbNMQu+F4JRHbR3ROQyhi1wbn+GSC6WDQamcYHkZmEZk1/eoI354bIQOOfkEM6aFLv718iskrkon4GQ==} 141 + 142 + '@atcute/did-plc@0.2.0': 143 + resolution: {integrity: sha512-1sGek8GRM/Ph7nLVRREm8FqM7g4shGckItvdVwJcRbUa8Rh0zOsXQa0QyYWAC0k40BhkqO9FwKXhJEaXCmF5oQ==} 144 + 145 + '@atcute/identity-resolver@1.2.0': 146 + resolution: {integrity: sha512-5UbSJfdV3JIkF8ksXz7g4nKBWasf2wROvzM66cfvTIWydWFO6/oS1KZd+zo9Eokje5Scf5+jsY9ZfgVARLepXg==} 100 147 peerDependencies: 101 - '@atcute/client': ^1.0.0 || ^2.0.0 148 + '@atcute/identity': ^1.0.0 102 149 103 - '@atcute/car@2.0.1': 104 - resolution: {integrity: sha512-Z1YLniYwChJaHUWJH+99Ru6tNGI3xBngAdlM8qe3xij/wTUaIg14QgXLdWqk6smWTV+bR5IJ+Iiylt2zIJ+XXQ==} 150 + '@atcute/identity@1.1.3': 151 + resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==} 105 152 106 - '@atcute/cbor@2.1.1': 107 - resolution: {integrity: sha512-F4sHzsA5DNqMeCxvZKxbvDRIRKBTJom9IEQQutGMMFpvO0TZyIieEKgSL/7Us18TXPWKUXZM38KyCWIlPTLxeg==} 153 + '@atcute/lexicons@1.2.5': 154 + resolution: {integrity: sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q==} 108 155 109 - '@atcute/cid@2.1.0': 110 - resolution: {integrity: sha512-Twsf5OKGk6QEqU1Z74feo1eRmnznYFzdCxtCISCutedCL2I2eGzD1F7JZRA+heTp5kgV5Bwvxvjn7VkGQhq3Sg==} 156 + '@atcute/mst@0.1.0': 157 + resolution: {integrity: sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw==} 111 158 112 - '@atcute/client@2.0.7': 113 - resolution: {integrity: sha512-bvNahrCGvhZw/EIx0HU/GOoKZEnUaAppbuZh7cu+VsOFA2tdFLnZJed9Hagh5Yz/eUX7QUh5NB4dRTRUdggSLQ==} 159 + '@atcute/multibase@1.1.6': 160 + resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==} 161 + 162 + '@atcute/repo@0.1.0': 163 + resolution: {integrity: sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw==} 114 164 115 - '@atcute/crypto@2.2.0': 116 - resolution: {integrity: sha512-Q/64Qn1AI8J0ZNy4hPDPpW/3poKf4OWRUxIYceCDI+btEOcIG5YMlhEQeZd6ojnI3oBMXy03sbOktekaBYRK9Q==} 165 + '@atcute/tid@1.0.3': 166 + resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==} 117 167 118 - '@atcute/multibase@1.1.2': 119 - resolution: {integrity: sha512-KFX+c7a/u2jSNcRw0rLaUHG/XEKf1A1c8XF5soHnsb1JMCShihf/anfZ1kJ4no/IlIp9HEHV3PQRQO2sWL6ASQ==} 168 + '@atcute/uint8array@1.0.6': 169 + resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==} 120 170 121 - '@atcute/uint8array@1.0.1': 122 - resolution: {integrity: sha512-AAnlFKyfDRgb9GNZJbhQ6OuMhbmNPirQyapb8KnmcEhxQZ3+tt+4NcwqekEegY4MpNqSTYeeTdyxq0wGZv1JHg==} 171 + '@atcute/util-fetch@1.0.4': 172 + resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==} 123 173 124 - '@atcute/varint@1.0.2': 125 - resolution: {integrity: sha512-0O31hePzzr4O3NGWHUKKOyta6CGSL+AtN8iir8grGxu9jXyI7DBARlw6PbgKA6uTAvsXdpmRmF8MX+p0TsLnNg==} 174 + '@atcute/varint@1.0.3': 175 + resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==} 126 176 127 - '@babel/code-frame@7.26.2': 128 - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} 177 + '@babel/code-frame@7.27.1': 178 + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} 129 179 engines: {node: '>=6.9.0'} 130 180 131 - '@babel/compat-data@7.26.5': 132 - resolution: {integrity: sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==} 181 + '@babel/compat-data@7.28.5': 182 + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} 133 183 engines: {node: '>=6.9.0'} 134 184 135 - '@babel/core@7.26.7': 136 - resolution: {integrity: sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==} 185 + '@babel/core@7.28.5': 186 + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} 137 187 engines: {node: '>=6.9.0'} 138 188 139 - '@babel/generator@7.26.5': 140 - resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} 189 + '@babel/generator@7.28.5': 190 + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} 141 191 engines: {node: '>=6.9.0'} 142 192 143 - '@babel/helper-compilation-targets@7.26.5': 144 - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} 193 + '@babel/helper-compilation-targets@7.27.2': 194 + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} 195 + engines: {node: '>=6.9.0'} 196 + 197 + '@babel/helper-globals@7.28.0': 198 + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} 145 199 engines: {node: '>=6.9.0'} 146 200 147 201 '@babel/helper-module-imports@7.18.6': 148 202 resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} 149 203 engines: {node: '>=6.9.0'} 150 204 151 - '@babel/helper-module-imports@7.25.9': 152 - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} 205 + '@babel/helper-module-imports@7.27.1': 206 + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} 153 207 engines: {node: '>=6.9.0'} 154 208 155 - '@babel/helper-module-transforms@7.26.0': 156 - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} 209 + '@babel/helper-module-transforms@7.28.3': 210 + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} 157 211 engines: {node: '>=6.9.0'} 158 212 peerDependencies: 159 213 '@babel/core': ^7.0.0 160 214 161 - '@babel/helper-plugin-utils@7.26.5': 162 - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} 215 + '@babel/helper-plugin-utils@7.27.1': 216 + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} 163 217 engines: {node: '>=6.9.0'} 164 218 165 - '@babel/helper-string-parser@7.25.9': 166 - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} 219 + '@babel/helper-string-parser@7.27.1': 220 + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 167 221 engines: {node: '>=6.9.0'} 168 222 169 - '@babel/helper-validator-identifier@7.25.9': 170 - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} 223 + '@babel/helper-validator-identifier@7.28.5': 224 + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} 171 225 engines: {node: '>=6.9.0'} 172 226 173 - '@babel/helper-validator-option@7.25.9': 174 - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} 227 + '@babel/helper-validator-option@7.27.1': 228 + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} 175 229 engines: {node: '>=6.9.0'} 176 230 177 - '@babel/helpers@7.26.7': 178 - resolution: {integrity: sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==} 231 + '@babel/helpers@7.28.4': 232 + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} 179 233 engines: {node: '>=6.9.0'} 180 234 181 - '@babel/parser@7.26.7': 182 - resolution: {integrity: sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==} 235 + '@babel/parser@7.28.5': 236 + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} 183 237 engines: {node: '>=6.0.0'} 184 238 hasBin: true 185 239 186 - '@babel/plugin-syntax-jsx@7.25.9': 187 - resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} 240 + '@babel/plugin-syntax-jsx@7.27.1': 241 + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} 188 242 engines: {node: '>=6.9.0'} 189 243 peerDependencies: 190 244 '@babel/core': ^7.0.0-0 191 245 192 - '@babel/template@7.25.9': 193 - resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} 246 + '@babel/template@7.27.2': 247 + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} 194 248 engines: {node: '>=6.9.0'} 195 249 196 - '@babel/traverse@7.26.7': 197 - resolution: {integrity: sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==} 250 + '@babel/traverse@7.28.5': 251 + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} 198 252 engines: {node: '>=6.9.0'} 199 253 200 - '@babel/types@7.26.7': 201 - resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==} 254 + '@babel/types@7.28.5': 255 + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} 202 256 engines: {node: '>=6.9.0'} 203 257 204 - '@badrap/valita@0.4.2': 205 - resolution: {integrity: sha512-Mwmr7k2iK0Yy0POLnAFUgab2mxKYeIsYXHY7sg3jo8XFsFHbG0SBmTcktXD0uW8N4WZePKf8s68QV7QDTGSdHA==} 258 + '@badrap/valita@0.4.6': 259 + resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 206 260 engines: {node: '>= 18'} 207 261 208 - '@cloudflare/kv-asset-handler@0.3.4': 209 - resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} 210 - engines: {node: '>=16.13'} 262 + '@cloudflare/kv-asset-handler@0.4.1': 263 + resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} 264 + engines: {node: '>=18.0.0'} 265 + 266 + '@cloudflare/unenv-preset@2.7.13': 267 + resolution: {integrity: sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==} 268 + peerDependencies: 269 + unenv: 2.0.0-rc.24 270 + workerd: ^1.20251202.0 271 + peerDependenciesMeta: 272 + workerd: 273 + optional: true 211 274 212 - '@cloudflare/workerd-darwin-64@1.20250124.0': 213 - resolution: {integrity: sha512-P5Z5KfVAuoCidIc0o2JPQZFLNTXDjtxN8vhtreCUr6V+xF5pqDNwQqeBDnDDF0gcszFQOYi2OZAB9e1MwssTwA==} 275 + '@cloudflare/workerd-darwin-64@1.20251202.0': 276 + resolution: {integrity: sha512-/uvEAWEukTWb1geHhbjGUeZqcSSSyYzp0mvoPUBl+l0ont4NVGao3fgwM0q8wtKvgoKCHSG6zcG23wj9Opj3Nw==} 214 277 engines: {node: '>=16'} 215 278 cpu: [x64] 216 279 os: [darwin] 217 280 218 - '@cloudflare/workerd-darwin-arm64@1.20250124.0': 219 - resolution: {integrity: sha512-lVxf6qIfmJ5rS6rmGKV7lt6ApY6nhD4kAQTK4vKYm/npk2sXod6LASIY0U4WBCwy4N+S75a8hP2QtmQf+KV3Iw==} 281 + '@cloudflare/workerd-darwin-arm64@1.20251202.0': 282 + resolution: {integrity: sha512-f52xRvcI9cWRd6400EZStRtXiRC5XKEud7K5aFIbbUv0VeINltujFQQ9nHWtsF6g1quIXWkjhh5u01gPAYNNXA==} 220 283 engines: {node: '>=16'} 221 284 cpu: [arm64] 222 285 os: [darwin] 223 286 224 - '@cloudflare/workerd-linux-64@1.20250124.0': 225 - resolution: {integrity: sha512-5S4GzN08vW/CfzaM1rVAkRhPPSDX1O1t7u0pj+xdbGl4GcazBzE4ZLre+y9OMplZ9PBCkxXkRWqHXzabWA1x4A==} 287 + '@cloudflare/workerd-linux-64@1.20251202.0': 288 + resolution: {integrity: sha512-HYXinF5RBH7oXbsFUMmwKCj+WltpYbf5mRKUBG5v3EuPhUjSIFB84U+58pDyfBJjcynHdy3EtvTWcvh/+lcgow==} 226 289 engines: {node: '>=16'} 227 290 cpu: [x64] 228 291 os: [linux] 229 292 230 - '@cloudflare/workerd-linux-arm64@1.20250124.0': 231 - resolution: {integrity: sha512-CHSYnutDfXgUWL9WcP0GbzIb5OyC9RZVCJGhKbDTQy6/uH7AivNcLzXtOhNdqetKjERmOxUbL9Us7vcMQLztog==} 293 + '@cloudflare/workerd-linux-arm64@1.20251202.0': 294 + resolution: {integrity: sha512-++L02Jdoxz7hEA9qDaQjbVU1RzQS+S+eqIi22DkPe2Tgiq2M3UfNpeu+75k5L9DGRIkZPYvwMBMbcmKvQqdIIg==} 232 295 engines: {node: '>=16'} 233 296 cpu: [arm64] 234 297 os: [linux] 235 298 236 - '@cloudflare/workerd-windows-64@1.20250124.0': 237 - resolution: {integrity: sha512-5TunEy5x4pNUQ10Z47qP5iF6m3X9uB2ZScKDLkNaWtbQ7EcMCapOWzuynVkTKIMBgDeKw6DAB8nbbkybPyMS9w==} 299 + '@cloudflare/workerd-windows-64@1.20251202.0': 300 + resolution: {integrity: sha512-gzeU6eDydTi7ib+Q9DD/c0hpXtqPucnHk2tfGU03mljPObYxzMkkPGgB5qxpksFvub3y4K0ChjqYxGJB4F+j3g==} 238 301 engines: {node: '>=16'} 239 302 cpu: [x64] 240 303 os: [win32] ··· 243 306 resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 244 307 engines: {node: '>=12'} 245 308 246 - '@esbuild-plugins/node-globals-polyfill@0.2.3': 247 - resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} 248 - peerDependencies: 249 - esbuild: '*' 309 + '@emnapi/runtime@1.7.1': 310 + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} 250 311 251 - '@esbuild-plugins/node-modules-polyfill@0.2.2': 252 - resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} 253 - peerDependencies: 254 - esbuild: '*' 312 + '@esbuild/aix-ppc64@0.25.12': 313 + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 314 + engines: {node: '>=18'} 315 + cpu: [ppc64] 316 + os: [aix] 255 317 256 - '@esbuild/aix-ppc64@0.24.2': 257 - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} 318 + '@esbuild/aix-ppc64@0.27.0': 319 + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} 258 320 engines: {node: '>=18'} 259 321 cpu: [ppc64] 260 322 os: [aix] 261 323 262 - '@esbuild/android-arm64@0.17.19': 263 - resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} 264 - engines: {node: '>=12'} 324 + '@esbuild/android-arm64@0.25.12': 325 + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 326 + engines: {node: '>=18'} 265 327 cpu: [arm64] 266 328 os: [android] 267 329 268 - '@esbuild/android-arm64@0.24.2': 269 - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} 330 + '@esbuild/android-arm64@0.27.0': 331 + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} 270 332 engines: {node: '>=18'} 271 333 cpu: [arm64] 272 334 os: [android] 273 335 274 - '@esbuild/android-arm@0.17.19': 275 - resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} 276 - engines: {node: '>=12'} 336 + '@esbuild/android-arm@0.25.12': 337 + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 338 + engines: {node: '>=18'} 277 339 cpu: [arm] 278 340 os: [android] 279 341 280 - '@esbuild/android-arm@0.24.2': 281 - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} 342 + '@esbuild/android-arm@0.27.0': 343 + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} 282 344 engines: {node: '>=18'} 283 345 cpu: [arm] 284 346 os: [android] 285 347 286 - '@esbuild/android-x64@0.17.19': 287 - resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} 288 - engines: {node: '>=12'} 348 + '@esbuild/android-x64@0.25.12': 349 + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 350 + engines: {node: '>=18'} 289 351 cpu: [x64] 290 352 os: [android] 291 353 292 - '@esbuild/android-x64@0.24.2': 293 - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} 354 + '@esbuild/android-x64@0.27.0': 355 + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} 294 356 engines: {node: '>=18'} 295 357 cpu: [x64] 296 358 os: [android] 297 359 298 - '@esbuild/darwin-arm64@0.17.19': 299 - resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} 300 - engines: {node: '>=12'} 360 + '@esbuild/darwin-arm64@0.25.12': 361 + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 362 + engines: {node: '>=18'} 301 363 cpu: [arm64] 302 364 os: [darwin] 303 365 304 - '@esbuild/darwin-arm64@0.24.2': 305 - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} 366 + '@esbuild/darwin-arm64@0.27.0': 367 + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} 306 368 engines: {node: '>=18'} 307 369 cpu: [arm64] 308 370 os: [darwin] 309 371 310 - '@esbuild/darwin-x64@0.17.19': 311 - resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} 312 - engines: {node: '>=12'} 372 + '@esbuild/darwin-x64@0.25.12': 373 + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 374 + engines: {node: '>=18'} 313 375 cpu: [x64] 314 376 os: [darwin] 315 377 316 - '@esbuild/darwin-x64@0.24.2': 317 - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} 378 + '@esbuild/darwin-x64@0.27.0': 379 + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} 318 380 engines: {node: '>=18'} 319 381 cpu: [x64] 320 382 os: [darwin] 321 383 322 - '@esbuild/freebsd-arm64@0.17.19': 323 - resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} 324 - engines: {node: '>=12'} 384 + '@esbuild/freebsd-arm64@0.25.12': 385 + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 386 + engines: {node: '>=18'} 325 387 cpu: [arm64] 326 388 os: [freebsd] 327 389 328 - '@esbuild/freebsd-arm64@0.24.2': 329 - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} 390 + '@esbuild/freebsd-arm64@0.27.0': 391 + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} 330 392 engines: {node: '>=18'} 331 393 cpu: [arm64] 332 394 os: [freebsd] 333 395 334 - '@esbuild/freebsd-x64@0.17.19': 335 - resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} 336 - engines: {node: '>=12'} 396 + '@esbuild/freebsd-x64@0.25.12': 397 + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 398 + engines: {node: '>=18'} 337 399 cpu: [x64] 338 400 os: [freebsd] 339 401 340 - '@esbuild/freebsd-x64@0.24.2': 341 - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} 402 + '@esbuild/freebsd-x64@0.27.0': 403 + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} 342 404 engines: {node: '>=18'} 343 405 cpu: [x64] 344 406 os: [freebsd] 345 407 346 - '@esbuild/linux-arm64@0.17.19': 347 - resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} 348 - engines: {node: '>=12'} 408 + '@esbuild/linux-arm64@0.25.12': 409 + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 410 + engines: {node: '>=18'} 349 411 cpu: [arm64] 350 412 os: [linux] 351 413 352 - '@esbuild/linux-arm64@0.24.2': 353 - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} 414 + '@esbuild/linux-arm64@0.27.0': 415 + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} 354 416 engines: {node: '>=18'} 355 417 cpu: [arm64] 356 418 os: [linux] 357 419 358 - '@esbuild/linux-arm@0.17.19': 359 - resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} 360 - engines: {node: '>=12'} 420 + '@esbuild/linux-arm@0.25.12': 421 + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 422 + engines: {node: '>=18'} 361 423 cpu: [arm] 362 424 os: [linux] 363 425 364 - '@esbuild/linux-arm@0.24.2': 365 - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} 426 + '@esbuild/linux-arm@0.27.0': 427 + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} 366 428 engines: {node: '>=18'} 367 429 cpu: [arm] 368 430 os: [linux] 369 431 370 - '@esbuild/linux-ia32@0.17.19': 371 - resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} 372 - engines: {node: '>=12'} 432 + '@esbuild/linux-ia32@0.25.12': 433 + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} 434 + engines: {node: '>=18'} 373 435 cpu: [ia32] 374 436 os: [linux] 375 437 376 - '@esbuild/linux-ia32@0.24.2': 377 - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} 438 + '@esbuild/linux-ia32@0.27.0': 439 + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} 378 440 engines: {node: '>=18'} 379 441 cpu: [ia32] 380 442 os: [linux] 381 443 382 - '@esbuild/linux-loong64@0.17.19': 383 - resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} 384 - engines: {node: '>=12'} 444 + '@esbuild/linux-loong64@0.25.12': 445 + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 446 + engines: {node: '>=18'} 385 447 cpu: [loong64] 386 448 os: [linux] 387 449 388 - '@esbuild/linux-loong64@0.24.2': 389 - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} 450 + '@esbuild/linux-loong64@0.27.0': 451 + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} 390 452 engines: {node: '>=18'} 391 453 cpu: [loong64] 392 454 os: [linux] 393 455 394 - '@esbuild/linux-mips64el@0.17.19': 395 - resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} 396 - engines: {node: '>=12'} 456 + '@esbuild/linux-mips64el@0.25.12': 457 + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 458 + engines: {node: '>=18'} 397 459 cpu: [mips64el] 398 460 os: [linux] 399 461 400 - '@esbuild/linux-mips64el@0.24.2': 401 - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} 462 + '@esbuild/linux-mips64el@0.27.0': 463 + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} 402 464 engines: {node: '>=18'} 403 465 cpu: [mips64el] 404 466 os: [linux] 405 467 406 - '@esbuild/linux-ppc64@0.17.19': 407 - resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} 408 - engines: {node: '>=12'} 468 + '@esbuild/linux-ppc64@0.25.12': 469 + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 470 + engines: {node: '>=18'} 409 471 cpu: [ppc64] 410 472 os: [linux] 411 473 412 - '@esbuild/linux-ppc64@0.24.2': 413 - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} 474 + '@esbuild/linux-ppc64@0.27.0': 475 + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} 414 476 engines: {node: '>=18'} 415 477 cpu: [ppc64] 416 478 os: [linux] 417 479 418 - '@esbuild/linux-riscv64@0.17.19': 419 - resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} 420 - engines: {node: '>=12'} 480 + '@esbuild/linux-riscv64@0.25.12': 481 + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 482 + engines: {node: '>=18'} 421 483 cpu: [riscv64] 422 484 os: [linux] 423 485 424 - '@esbuild/linux-riscv64@0.24.2': 425 - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} 486 + '@esbuild/linux-riscv64@0.27.0': 487 + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} 426 488 engines: {node: '>=18'} 427 489 cpu: [riscv64] 428 490 os: [linux] 429 491 430 - '@esbuild/linux-s390x@0.17.19': 431 - resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} 432 - engines: {node: '>=12'} 492 + '@esbuild/linux-s390x@0.25.12': 493 + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 494 + engines: {node: '>=18'} 433 495 cpu: [s390x] 434 496 os: [linux] 435 497 436 - '@esbuild/linux-s390x@0.24.2': 437 - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} 498 + '@esbuild/linux-s390x@0.27.0': 499 + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} 438 500 engines: {node: '>=18'} 439 501 cpu: [s390x] 440 502 os: [linux] 441 503 442 - '@esbuild/linux-x64@0.17.19': 443 - resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} 444 - engines: {node: '>=12'} 504 + '@esbuild/linux-x64@0.25.12': 505 + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 506 + engines: {node: '>=18'} 445 507 cpu: [x64] 446 508 os: [linux] 447 509 448 - '@esbuild/linux-x64@0.24.2': 449 - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} 510 + '@esbuild/linux-x64@0.27.0': 511 + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} 450 512 engines: {node: '>=18'} 451 513 cpu: [x64] 452 514 os: [linux] 453 515 454 - '@esbuild/netbsd-arm64@0.24.2': 455 - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} 516 + '@esbuild/netbsd-arm64@0.25.12': 517 + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 456 518 engines: {node: '>=18'} 457 519 cpu: [arm64] 458 520 os: [netbsd] 459 521 460 - '@esbuild/netbsd-x64@0.17.19': 461 - resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} 462 - engines: {node: '>=12'} 522 + '@esbuild/netbsd-arm64@0.27.0': 523 + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} 524 + engines: {node: '>=18'} 525 + cpu: [arm64] 526 + os: [netbsd] 527 + 528 + '@esbuild/netbsd-x64@0.25.12': 529 + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 530 + engines: {node: '>=18'} 463 531 cpu: [x64] 464 532 os: [netbsd] 465 533 466 - '@esbuild/netbsd-x64@0.24.2': 467 - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} 534 + '@esbuild/netbsd-x64@0.27.0': 535 + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} 468 536 engines: {node: '>=18'} 469 537 cpu: [x64] 470 538 os: [netbsd] 471 539 472 - '@esbuild/openbsd-arm64@0.24.2': 473 - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} 540 + '@esbuild/openbsd-arm64@0.25.12': 541 + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 542 + engines: {node: '>=18'} 543 + cpu: [arm64] 544 + os: [openbsd] 545 + 546 + '@esbuild/openbsd-arm64@0.27.0': 547 + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} 474 548 engines: {node: '>=18'} 475 549 cpu: [arm64] 476 550 os: [openbsd] 477 551 478 - '@esbuild/openbsd-x64@0.17.19': 479 - resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} 480 - engines: {node: '>=12'} 552 + '@esbuild/openbsd-x64@0.25.12': 553 + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} 554 + engines: {node: '>=18'} 481 555 cpu: [x64] 482 556 os: [openbsd] 483 557 484 - '@esbuild/openbsd-x64@0.24.2': 485 - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} 558 + '@esbuild/openbsd-x64@0.27.0': 559 + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} 486 560 engines: {node: '>=18'} 487 561 cpu: [x64] 488 562 os: [openbsd] 489 563 490 - '@esbuild/sunos-x64@0.17.19': 491 - resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} 492 - engines: {node: '>=12'} 564 + '@esbuild/openharmony-arm64@0.25.12': 565 + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 566 + engines: {node: '>=18'} 567 + cpu: [arm64] 568 + os: [openharmony] 569 + 570 + '@esbuild/openharmony-arm64@0.27.0': 571 + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} 572 + engines: {node: '>=18'} 573 + cpu: [arm64] 574 + os: [openharmony] 575 + 576 + '@esbuild/sunos-x64@0.25.12': 577 + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 578 + engines: {node: '>=18'} 493 579 cpu: [x64] 494 580 os: [sunos] 495 581 496 - '@esbuild/sunos-x64@0.24.2': 497 - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} 582 + '@esbuild/sunos-x64@0.27.0': 583 + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} 498 584 engines: {node: '>=18'} 499 585 cpu: [x64] 500 586 os: [sunos] 501 587 502 - '@esbuild/win32-arm64@0.17.19': 503 - resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} 504 - engines: {node: '>=12'} 588 + '@esbuild/win32-arm64@0.25.12': 589 + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 590 + engines: {node: '>=18'} 505 591 cpu: [arm64] 506 592 os: [win32] 507 593 508 - '@esbuild/win32-arm64@0.24.2': 509 - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} 594 + '@esbuild/win32-arm64@0.27.0': 595 + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} 510 596 engines: {node: '>=18'} 511 597 cpu: [arm64] 512 598 os: [win32] 513 599 514 - '@esbuild/win32-ia32@0.17.19': 515 - resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} 516 - engines: {node: '>=12'} 600 + '@esbuild/win32-ia32@0.25.12': 601 + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 602 + engines: {node: '>=18'} 517 603 cpu: [ia32] 518 604 os: [win32] 519 605 520 - '@esbuild/win32-ia32@0.24.2': 521 - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} 606 + '@esbuild/win32-ia32@0.27.0': 607 + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} 522 608 engines: {node: '>=18'} 523 609 cpu: [ia32] 524 610 os: [win32] 525 611 526 - '@esbuild/win32-x64@0.17.19': 527 - resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} 528 - engines: {node: '>=12'} 612 + '@esbuild/win32-x64@0.25.12': 613 + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 614 + engines: {node: '>=18'} 529 615 cpu: [x64] 530 616 os: [win32] 531 617 532 - '@esbuild/win32-x64@0.24.2': 533 - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} 618 + '@esbuild/win32-x64@0.27.0': 619 + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} 534 620 engines: {node: '>=18'} 535 621 cpu: [x64] 536 622 os: [win32] ··· 540 626 peerDependencies: 541 627 solid-js: ^1.8.5 542 628 543 - '@fastify/busboy@2.1.1': 544 - resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 545 - engines: {node: '>=14'} 629 + '@img/sharp-darwin-arm64@0.33.5': 630 + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 631 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 632 + cpu: [arm64] 633 + os: [darwin] 634 + 635 + '@img/sharp-darwin-x64@0.33.5': 636 + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 637 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 638 + cpu: [x64] 639 + os: [darwin] 640 + 641 + '@img/sharp-libvips-darwin-arm64@1.0.4': 642 + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 643 + cpu: [arm64] 644 + os: [darwin] 645 + 646 + '@img/sharp-libvips-darwin-x64@1.0.4': 647 + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 648 + cpu: [x64] 649 + os: [darwin] 650 + 651 + '@img/sharp-libvips-linux-arm64@1.0.4': 652 + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 653 + cpu: [arm64] 654 + os: [linux] 655 + 656 + '@img/sharp-libvips-linux-arm@1.0.5': 657 + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 658 + cpu: [arm] 659 + os: [linux] 660 + 661 + '@img/sharp-libvips-linux-s390x@1.0.4': 662 + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 663 + cpu: [s390x] 664 + os: [linux] 665 + 666 + '@img/sharp-libvips-linux-x64@1.0.4': 667 + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 668 + cpu: [x64] 669 + os: [linux] 670 + 671 + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 672 + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 673 + cpu: [arm64] 674 + os: [linux] 675 + 676 + '@img/sharp-libvips-linuxmusl-x64@1.0.4': 677 + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 678 + cpu: [x64] 679 + os: [linux] 680 + 681 + '@img/sharp-linux-arm64@0.33.5': 682 + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 683 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 684 + cpu: [arm64] 685 + os: [linux] 686 + 687 + '@img/sharp-linux-arm@0.33.5': 688 + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 689 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 690 + cpu: [arm] 691 + os: [linux] 692 + 693 + '@img/sharp-linux-s390x@0.33.5': 694 + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 695 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 696 + cpu: [s390x] 697 + os: [linux] 698 + 699 + '@img/sharp-linux-x64@0.33.5': 700 + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 701 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 702 + cpu: [x64] 703 + os: [linux] 704 + 705 + '@img/sharp-linuxmusl-arm64@0.33.5': 706 + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 707 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 708 + cpu: [arm64] 709 + os: [linux] 710 + 711 + '@img/sharp-linuxmusl-x64@0.33.5': 712 + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 713 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 714 + cpu: [x64] 715 + os: [linux] 716 + 717 + '@img/sharp-wasm32@0.33.5': 718 + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 719 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 720 + cpu: [wasm32] 721 + 722 + '@img/sharp-win32-ia32@0.33.5': 723 + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 724 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 725 + cpu: [ia32] 726 + os: [win32] 727 + 728 + '@img/sharp-win32-x64@0.33.5': 729 + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 730 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 731 + cpu: [x64] 732 + os: [win32] 546 733 547 - '@isaacs/cliui@8.0.2': 548 - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 549 - engines: {node: '>=12'} 734 + '@jridgewell/gen-mapping@0.3.13': 735 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 550 736 551 - '@jridgewell/gen-mapping@0.3.8': 552 - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 553 - engines: {node: '>=6.0.0'} 737 + '@jridgewell/remapping@2.3.5': 738 + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} 554 739 555 740 '@jridgewell/resolve-uri@3.1.2': 556 741 resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 557 742 engines: {node: '>=6.0.0'} 558 743 559 - '@jridgewell/set-array@1.2.1': 560 - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 561 - engines: {node: '>=6.0.0'} 562 - 563 - '@jridgewell/source-map@0.3.6': 564 - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} 744 + '@jridgewell/source-map@0.3.11': 745 + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} 565 746 566 - '@jridgewell/sourcemap-codec@1.5.0': 567 - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 747 + '@jridgewell/sourcemap-codec@1.5.5': 748 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 568 749 569 - '@jridgewell/trace-mapping@0.3.25': 570 - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 750 + '@jridgewell/trace-mapping@0.3.31': 751 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 571 752 572 753 '@jridgewell/trace-mapping@0.3.9': 573 754 resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 574 755 575 - '@jsr/mary__array-fns@0.1.0': 576 - resolution: {integrity: sha512-rG8Ng1Arl86T1Lv4of4LC8OcdpGxcxG/j8K01K6lyG0lI9imLT7e6FknuMVs5MQ5jH6NUBYnPOfmVAv4sd/OkA==, tarball: https://npm.jsr.io/~/11/@jsr/mary__array-fns/0.1.0.tgz} 756 + '@jsr/mary__array-fns@0.1.5': 757 + resolution: {integrity: sha512-gI4scq/Hh9GtFUJfS8cvZf5nr+cs7udvrEpMv75grws5/0LIwBycKeeJcNi4+xNl6x4CGW6Fp46puhtJiQOpMg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__array-fns/0.1.5.tgz} 577 758 578 - '@jsr/mary__events@0.1.0': 579 - resolution: {integrity: sha512-oS6jVOaXTaNEa6avRncwrEtUYaBKrq/HEybPa9Z3aoeMs+RSly0vn0KcOj/fy2H6iTBkeh3wa8+/9nFjhKyKIg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__events/0.1.0.tgz} 759 + '@jsr/mary__ds-queue@0.1.3': 760 + resolution: {integrity: sha512-gGqIHXiAmhUUtonNI6YVvL7VlXjEHUpGdc7RGU8BLP4XnFvqovDTH5y9VlBZmvozTWgTIMoZF6/1//sMrvYKtQ==, tarball: https://npm.jsr.io/~/11/@jsr/mary__ds-queue/0.1.3.tgz} 580 761 581 - '@jsr/mary__tar@0.2.4': 582 - resolution: {integrity: sha512-jFjPcZj8DRSukPLZOt6+h74cVFdfdTMG9gzbW67YByCJTD52PEpe2sNcfCSw4mQ8hZBNgwiufCPyYL8hR9yicA==, tarball: https://npm.jsr.io/~/11/@jsr/mary__tar/0.2.4.tgz} 762 + '@jsr/mary__events@0.2.0': 763 + resolution: {integrity: sha512-WcBRbtuTno3zcfXKd7SEeKr1lAJF+CQ8BCv+PEEMmNKNqFurkEksGxRB3UDPZxIxjJ7sAqMVTL26wRuMpAcIeA==, tarball: https://npm.jsr.io/~/11/@jsr/mary__events/0.2.0.tgz} 764 + 765 + '@jsr/mary__tar@0.3.1': 766 + resolution: {integrity: sha512-T803kucwCLVOXFJGzVbpkT5vRK6fARy5HL6xMiLK5hJFck72bsAeluENlRnvD0kFPSlFNp/5EJWfTHnpDK0qYA==, tarball: https://npm.jsr.io/~/11/@jsr/mary__tar/0.3.1.tgz} 583 767 584 - '@noble/secp256k1@2.2.3': 585 - resolution: {integrity: sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==} 768 + '@noble/secp256k1@3.0.0': 769 + resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} 586 770 587 771 '@nodelib/fs.scandir@2.1.5': 588 772 resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} ··· 596 780 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 597 781 engines: {node: '>= 8'} 598 782 599 - '@pkgjs/parseargs@0.11.0': 600 - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 601 - engines: {node: '>=14'} 783 + '@poppinss/colors@4.1.5': 784 + resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} 602 785 603 - '@rollup/rollup-android-arm-eabi@4.32.1': 604 - resolution: {integrity: sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==} 786 + '@poppinss/dumper@0.6.5': 787 + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} 788 + 789 + '@poppinss/exception@1.2.2': 790 + resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} 791 + 792 + '@rollup/rollup-android-arm-eabi@4.53.3': 793 + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} 605 794 cpu: [arm] 606 795 os: [android] 607 796 608 - '@rollup/rollup-android-arm64@4.32.1': 609 - resolution: {integrity: sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==} 797 + '@rollup/rollup-android-arm64@4.53.3': 798 + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} 610 799 cpu: [arm64] 611 800 os: [android] 612 801 613 - '@rollup/rollup-darwin-arm64@4.32.1': 614 - resolution: {integrity: sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==} 802 + '@rollup/rollup-darwin-arm64@4.53.3': 803 + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} 615 804 cpu: [arm64] 616 805 os: [darwin] 617 806 618 - '@rollup/rollup-darwin-x64@4.32.1': 619 - resolution: {integrity: sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==} 807 + '@rollup/rollup-darwin-x64@4.53.3': 808 + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} 620 809 cpu: [x64] 621 810 os: [darwin] 622 811 623 - '@rollup/rollup-freebsd-arm64@4.32.1': 624 - resolution: {integrity: sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==} 812 + '@rollup/rollup-freebsd-arm64@4.53.3': 813 + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} 625 814 cpu: [arm64] 626 815 os: [freebsd] 627 816 628 - '@rollup/rollup-freebsd-x64@4.32.1': 629 - resolution: {integrity: sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==} 817 + '@rollup/rollup-freebsd-x64@4.53.3': 818 + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} 630 819 cpu: [x64] 631 820 os: [freebsd] 632 821 633 - '@rollup/rollup-linux-arm-gnueabihf@4.32.1': 634 - resolution: {integrity: sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==} 822 + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 823 + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} 635 824 cpu: [arm] 636 825 os: [linux] 637 826 638 - '@rollup/rollup-linux-arm-musleabihf@4.32.1': 639 - resolution: {integrity: sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==} 827 + '@rollup/rollup-linux-arm-musleabihf@4.53.3': 828 + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} 640 829 cpu: [arm] 641 830 os: [linux] 642 831 643 - '@rollup/rollup-linux-arm64-gnu@4.32.1': 644 - resolution: {integrity: sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==} 832 + '@rollup/rollup-linux-arm64-gnu@4.53.3': 833 + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} 645 834 cpu: [arm64] 646 835 os: [linux] 647 836 648 - '@rollup/rollup-linux-arm64-musl@4.32.1': 649 - resolution: {integrity: sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==} 837 + '@rollup/rollup-linux-arm64-musl@4.53.3': 838 + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} 650 839 cpu: [arm64] 651 840 os: [linux] 652 841 653 - '@rollup/rollup-linux-loongarch64-gnu@4.32.1': 654 - resolution: {integrity: sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==} 842 + '@rollup/rollup-linux-loong64-gnu@4.53.3': 843 + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} 655 844 cpu: [loong64] 656 845 os: [linux] 657 846 658 - '@rollup/rollup-linux-powerpc64le-gnu@4.32.1': 659 - resolution: {integrity: sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==} 847 + '@rollup/rollup-linux-ppc64-gnu@4.53.3': 848 + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} 660 849 cpu: [ppc64] 661 850 os: [linux] 662 851 663 - '@rollup/rollup-linux-riscv64-gnu@4.32.1': 664 - resolution: {integrity: sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==} 852 + '@rollup/rollup-linux-riscv64-gnu@4.53.3': 853 + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} 665 854 cpu: [riscv64] 666 855 os: [linux] 667 856 668 - '@rollup/rollup-linux-s390x-gnu@4.32.1': 669 - resolution: {integrity: sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==} 857 + '@rollup/rollup-linux-riscv64-musl@4.53.3': 858 + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} 859 + cpu: [riscv64] 860 + os: [linux] 861 + 862 + '@rollup/rollup-linux-s390x-gnu@4.53.3': 863 + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} 670 864 cpu: [s390x] 671 865 os: [linux] 672 866 673 - '@rollup/rollup-linux-x64-gnu@4.32.1': 674 - resolution: {integrity: sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==} 867 + '@rollup/rollup-linux-x64-gnu@4.53.3': 868 + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} 675 869 cpu: [x64] 676 870 os: [linux] 677 871 678 - '@rollup/rollup-linux-x64-musl@4.32.1': 679 - resolution: {integrity: sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==} 872 + '@rollup/rollup-linux-x64-musl@4.53.3': 873 + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} 680 874 cpu: [x64] 681 875 os: [linux] 682 876 683 - '@rollup/rollup-win32-arm64-msvc@4.32.1': 684 - resolution: {integrity: sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==} 877 + '@rollup/rollup-openharmony-arm64@4.53.3': 878 + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} 879 + cpu: [arm64] 880 + os: [openharmony] 881 + 882 + '@rollup/rollup-win32-arm64-msvc@4.53.3': 883 + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} 685 884 cpu: [arm64] 686 885 os: [win32] 687 886 688 - '@rollup/rollup-win32-ia32-msvc@4.32.1': 689 - resolution: {integrity: sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==} 887 + '@rollup/rollup-win32-ia32-msvc@4.53.3': 888 + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} 690 889 cpu: [ia32] 691 890 os: [win32] 692 891 693 - '@rollup/rollup-win32-x64-msvc@4.32.1': 694 - resolution: {integrity: sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==} 892 + '@rollup/rollup-win32-x64-gnu@4.53.3': 893 + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} 894 + cpu: [x64] 895 + os: [win32] 896 + 897 + '@rollup/rollup-win32-x64-msvc@4.53.3': 898 + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} 695 899 cpu: [x64] 696 900 os: [win32] 697 901 902 + '@sindresorhus/is@7.1.1': 903 + resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==} 904 + engines: {node: '>=18'} 905 + 906 + '@speed-highlight/core@1.2.12': 907 + resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} 908 + 909 + '@standard-schema/spec@1.0.0': 910 + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 911 + 698 912 '@tailwindcss/forms@0.5.10': 699 913 resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} 700 914 peerDependencies: ··· 703 917 '@types/babel__core@7.20.5': 704 918 resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 705 919 706 - '@types/babel__generator@7.6.8': 707 - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} 920 + '@types/babel__generator@7.27.0': 921 + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} 708 922 709 923 '@types/babel__template@7.4.4': 710 924 resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 711 925 712 - '@types/babel__traverse@7.20.6': 713 - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} 926 + '@types/babel__traverse@7.28.0': 927 + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} 714 928 715 - '@types/estree@1.0.6': 716 - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 929 + '@types/estree@1.0.8': 930 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 717 931 718 - '@types/node@22.12.0': 719 - resolution: {integrity: sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==} 932 + '@types/node@22.19.2': 933 + resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==} 720 934 721 - acorn-walk@8.3.4: 722 - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} 935 + acorn-walk@8.3.2: 936 + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 723 937 engines: {node: '>=0.4.0'} 724 938 725 939 acorn@8.14.0: ··· 727 941 engines: {node: '>=0.4.0'} 728 942 hasBin: true 729 943 730 - ansi-regex@5.0.1: 731 - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 732 - engines: {node: '>=8'} 733 - 734 - ansi-regex@6.1.0: 735 - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 736 - engines: {node: '>=12'} 737 - 738 - ansi-styles@4.3.0: 739 - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 740 - engines: {node: '>=8'} 741 - 742 - ansi-styles@6.2.1: 743 - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 744 - engines: {node: '>=12'} 944 + acorn@8.15.0: 945 + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 946 + engines: {node: '>=0.4.0'} 947 + hasBin: true 745 948 746 949 any-promise@1.3.0: 747 950 resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} ··· 753 956 arg@5.0.2: 754 957 resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 755 958 756 - as-table@1.0.55: 757 - resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 758 - 759 - autoprefixer@10.4.20: 760 - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} 959 + autoprefixer@10.4.22: 960 + resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==} 761 961 engines: {node: ^10 || ^12 || >=14} 762 962 hasBin: true 763 963 peerDependencies: 764 964 postcss: ^8.1.0 765 965 766 - babel-plugin-jsx-dom-expressions@0.39.6: 767 - resolution: {integrity: sha512-HMkTn5A3NyydEgG7HKmm48YcnsQQyqeT6SKNWh2TrS6nn5rOLeHDfg5hPbrRUCFUqaT9WGn5NInQfMc3qne3Dg==} 966 + babel-plugin-jsx-dom-expressions@0.40.3: 967 + resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==} 768 968 peerDependencies: 769 969 '@babel/core': ^7.20.12 770 970 771 - babel-preset-solid@1.9.3: 772 - resolution: {integrity: sha512-jvlx5wDp8s+bEF9sGFw/84SInXOA51ttkUEroQziKMbxplXThVKt83qB6bDTa1HuLNatdU9FHpFOiQWs1tLQIg==} 971 + babel-preset-solid@1.9.10: 972 + resolution: {integrity: sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==} 773 973 peerDependencies: 774 974 '@babel/core': ^7.0.0 975 + solid-js: ^1.9.10 976 + peerDependenciesMeta: 977 + solid-js: 978 + optional: true 775 979 776 - balanced-match@1.0.2: 777 - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 980 + baseline-browser-mapping@2.9.5: 981 + resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} 982 + hasBin: true 778 983 779 984 binary-extensions@2.3.0: 780 985 resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} ··· 783 988 blake3-wasm@2.1.5: 784 989 resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 785 990 786 - brace-expansion@2.0.1: 787 - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 788 - 789 991 braces@3.0.3: 790 992 resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 791 993 engines: {node: '>=8'} 792 994 793 - browserslist@4.24.4: 794 - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} 995 + browserslist@4.28.1: 996 + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} 795 997 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 796 998 hasBin: true 797 999 ··· 802 1004 resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 803 1005 engines: {node: '>= 6'} 804 1006 805 - caniuse-lite@1.0.30001696: 806 - resolution: {integrity: sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==} 807 - 808 - capnp-ts@0.7.0: 809 - resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} 1007 + caniuse-lite@1.0.30001760: 1008 + resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} 810 1009 811 1010 chokidar@3.6.0: 812 1011 resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} ··· 818 1017 819 1018 color-name@1.1.4: 820 1019 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 1020 + 1021 + color-string@1.9.1: 1022 + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 1023 + 1024 + color@4.2.3: 1025 + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 1026 + engines: {node: '>=12.5.0'} 821 1027 822 1028 commander@2.20.3: 823 1029 resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} ··· 826 1032 resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 827 1033 engines: {node: '>= 6'} 828 1034 829 - confbox@0.1.8: 830 - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} 831 - 832 1035 convert-source-map@2.0.0: 833 1036 resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 834 1037 835 - cookie@0.7.2: 836 - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 837 - engines: {node: '>= 0.6'} 838 - 839 - cross-spawn@7.0.6: 840 - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 841 - engines: {node: '>= 8'} 1038 + cookie@1.1.1: 1039 + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} 1040 + engines: {node: '>=18'} 842 1041 843 1042 cssesc@3.0.0: 844 1043 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 845 1044 engines: {node: '>=4'} 846 1045 hasBin: true 847 1046 848 - csstype@3.1.3: 849 - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 1047 + csstype@3.2.3: 1048 + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 850 1049 851 - data-uri-to-buffer@2.0.2: 852 - resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 853 - 854 - debug@4.4.0: 855 - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 1050 + debug@4.4.3: 1051 + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 856 1052 engines: {node: '>=6.0'} 857 1053 peerDependencies: 858 1054 supports-color: '*' ··· 860 1056 supports-color: 861 1057 optional: true 862 1058 863 - defu@6.1.4: 864 - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 1059 + detect-libc@2.1.2: 1060 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 1061 + engines: {node: '>=8'} 865 1062 866 1063 didyoumean@1.2.2: 867 1064 resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} ··· 869 1066 dlv@1.1.3: 870 1067 resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 871 1068 872 - eastasianwidth@0.2.0: 873 - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 874 - 875 - electron-to-chromium@1.5.90: 876 - resolution: {integrity: sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==} 877 - 878 - emoji-regex@8.0.0: 879 - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 880 - 881 - emoji-regex@9.2.2: 882 - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 1069 + electron-to-chromium@1.5.267: 1070 + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} 883 1071 884 - entities@4.5.0: 885 - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 1072 + entities@6.0.1: 1073 + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 886 1074 engines: {node: '>=0.12'} 887 1075 888 - esbuild@0.17.19: 889 - resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} 890 - engines: {node: '>=12'} 1076 + error-stack-parser-es@1.0.5: 1077 + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} 1078 + 1079 + esbuild@0.25.12: 1080 + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 1081 + engines: {node: '>=18'} 891 1082 hasBin: true 892 1083 893 - esbuild@0.24.2: 894 - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} 1084 + esbuild@0.27.0: 1085 + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} 895 1086 engines: {node: '>=18'} 896 1087 hasBin: true 897 1088 ··· 899 1090 resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 900 1091 engines: {node: '>=6'} 901 1092 902 - escape-string-regexp@4.0.0: 903 - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 904 - engines: {node: '>=10'} 905 - 906 - estree-walker@0.6.1: 907 - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 1093 + esm-env@1.2.2: 1094 + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 908 1095 909 1096 exit-hook@2.2.1: 910 1097 resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} ··· 914 1101 resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 915 1102 engines: {node: '>=8.6.0'} 916 1103 917 - fastq@1.18.0: 918 - resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} 1104 + fastq@1.19.1: 1105 + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 1106 + 1107 + fdir@6.5.0: 1108 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 1109 + engines: {node: '>=12.0.0'} 1110 + peerDependencies: 1111 + picomatch: ^3 || ^4 1112 + peerDependenciesMeta: 1113 + picomatch: 1114 + optional: true 919 1115 920 1116 fetch-blob@3.2.0: 921 1117 resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} ··· 925 1121 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 926 1122 engines: {node: '>=8'} 927 1123 928 - foreground-child@3.3.0: 929 - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 930 - engines: {node: '>=14'} 931 - 932 - fraction.js@4.3.7: 933 - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} 1124 + fraction.js@5.3.4: 1125 + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} 934 1126 935 1127 fsevents@2.3.3: 936 1128 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} ··· 943 1135 gensync@1.0.0-beta.2: 944 1136 resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 945 1137 engines: {node: '>=6.9.0'} 946 - 947 - get-source@2.0.12: 948 - resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 949 1138 950 1139 glob-parent@5.1.2: 951 1140 resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} ··· 958 1147 glob-to-regexp@0.4.1: 959 1148 resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 960 1149 961 - glob@10.4.5: 962 - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 963 - hasBin: true 964 - 965 - globals@11.12.0: 966 - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 967 - engines: {node: '>=4'} 968 - 969 1150 hasown@2.0.2: 970 1151 resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 971 1152 engines: {node: '>= 0.4'} ··· 973 1154 html-entities@2.3.3: 974 1155 resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 975 1156 1157 + is-arrayish@0.3.4: 1158 + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} 1159 + 976 1160 is-binary-path@2.1.0: 977 1161 resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 978 1162 engines: {node: '>=8'} ··· 985 1169 resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 986 1170 engines: {node: '>=0.10.0'} 987 1171 988 - is-fullwidth-code-point@3.0.0: 989 - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 990 - engines: {node: '>=8'} 991 - 992 1172 is-glob@4.0.3: 993 1173 resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 994 1174 engines: {node: '>=0.10.0'} ··· 1000 1180 is-what@4.1.16: 1001 1181 resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 1002 1182 engines: {node: '>=12.13'} 1003 - 1004 - isexe@2.0.0: 1005 - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 1006 - 1007 - jackspeak@3.4.3: 1008 - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 1009 1183 1010 1184 jiti@1.21.7: 1011 1185 resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} ··· 1024 1198 engines: {node: '>=6'} 1025 1199 hasBin: true 1026 1200 1201 + kleur@4.1.5: 1202 + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 1203 + engines: {node: '>=6'} 1204 + 1027 1205 lilconfig@3.1.3: 1028 1206 resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 1029 1207 engines: {node: '>=14'} ··· 1031 1209 lines-and-columns@1.2.4: 1032 1210 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 1033 1211 1034 - lru-cache@10.4.3: 1035 - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 1036 - 1037 1212 lru-cache@5.1.1: 1038 1213 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 1039 - 1040 - magic-string@0.25.9: 1041 - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 1042 1214 1043 1215 merge-anything@5.1.7: 1044 1216 resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} ··· 1061 1233 resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} 1062 1234 hasBin: true 1063 1235 1064 - miniflare@3.20250124.0: 1065 - resolution: {integrity: sha512-ewsetUwhj4FqeLoE3UMqYHHyCYIOPzdhlpF9CHuHpMZbfLvI9SPd+VrKrLfOgyAF97EHqVWb6WamIrLdgtj6Kg==} 1066 - engines: {node: '>=16.13'} 1236 + miniflare@4.20251202.1: 1237 + resolution: {integrity: sha512-cRp2QNgnt9wpLMoNs4MOzzomyfe9UTS9sPRxIpUvxMl+mweCZ0FHpWWQvCnU7wWlfAP8VGZrHwqSsV5ERA6ahQ==} 1238 + engines: {node: '>=18.0.0'} 1067 1239 hasBin: true 1068 1240 1069 - minimatch@9.0.5: 1070 - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 1071 - engines: {node: '>=16 || 14 >=14.17'} 1072 - 1073 - minipass@7.1.2: 1074 - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 1075 - engines: {node: '>=16 || 14 >=14.17'} 1076 - 1077 - mlly@1.7.4: 1078 - resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} 1079 - 1080 1241 ms@2.1.3: 1081 1242 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1082 1243 1083 - mustache@4.2.0: 1084 - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 1085 - hasBin: true 1086 - 1087 1244 mz@2.7.0: 1088 1245 resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 1089 1246 1090 - nanoid@3.3.8: 1091 - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 1247 + nanoid@3.3.11: 1248 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1092 1249 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1093 1250 hasBin: true 1094 1251 1095 - nanoid@5.0.9: 1096 - resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} 1252 + nanoid@5.1.6: 1253 + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} 1097 1254 engines: {node: ^18 || >=20} 1098 1255 hasBin: true 1099 1256 ··· 1104 1261 node-domexception@1.0.0: 1105 1262 resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} 1106 1263 engines: {node: '>=10.5.0'} 1264 + deprecated: Use your platform's native DOMException instead 1107 1265 1108 - node-releases@2.0.19: 1109 - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} 1266 + node-releases@2.0.27: 1267 + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} 1110 1268 1111 1269 normalize-path@3.0.0: 1112 1270 resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} ··· 1124 1282 resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 1125 1283 engines: {node: '>= 6'} 1126 1284 1127 - ohash@1.1.4: 1128 - resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} 1129 - 1130 - package-json-from-dist@1.0.1: 1131 - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 1132 - 1133 - parse5@7.2.1: 1134 - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} 1135 - 1136 - path-key@3.1.1: 1137 - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1138 - engines: {node: '>=8'} 1285 + parse5@7.3.0: 1286 + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 1139 1287 1140 1288 path-parse@1.0.7: 1141 1289 resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 1142 1290 1143 - path-scurry@1.11.1: 1144 - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 1145 - engines: {node: '>=16 || 14 >=14.18'} 1146 - 1147 1291 path-to-regexp@6.3.0: 1148 1292 resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 1149 1293 1150 - pathe@1.1.2: 1151 - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 1152 - 1153 - pathe@2.0.2: 1154 - resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} 1294 + pathe@2.0.3: 1295 + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1155 1296 1156 1297 picocolors@1.1.1: 1157 1298 resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} ··· 1160 1301 resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1161 1302 engines: {node: '>=8.6'} 1162 1303 1304 + picomatch@4.0.3: 1305 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 1306 + engines: {node: '>=12'} 1307 + 1163 1308 pify@2.3.0: 1164 1309 resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 1165 1310 engines: {node: '>=0.10.0'} 1166 1311 1167 - pirates@4.0.6: 1168 - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 1312 + pirates@4.0.7: 1313 + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} 1169 1314 engines: {node: '>= 6'} 1170 - 1171 - pkg-types@1.3.1: 1172 - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} 1173 1315 1174 1316 postcss-import@15.1.0: 1175 1317 resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} ··· 1177 1319 peerDependencies: 1178 1320 postcss: ^8.0.0 1179 1321 1180 - postcss-js@4.0.1: 1181 - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 1322 + postcss-js@4.1.0: 1323 + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} 1182 1324 engines: {node: ^12 || ^14 || >= 16} 1183 1325 peerDependencies: 1184 1326 postcss: ^8.4.21 1185 1327 1186 - postcss-load-config@4.0.2: 1187 - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 1188 - engines: {node: '>= 14'} 1328 + postcss-load-config@6.0.1: 1329 + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} 1330 + engines: {node: '>= 18'} 1189 1331 peerDependencies: 1332 + jiti: '>=1.21.0' 1190 1333 postcss: '>=8.0.9' 1191 - ts-node: '>=9.0.0' 1334 + tsx: ^4.8.1 1335 + yaml: ^2.4.2 1192 1336 peerDependenciesMeta: 1337 + jiti: 1338 + optional: true 1193 1339 postcss: 1194 1340 optional: true 1195 - ts-node: 1341 + tsx: 1342 + optional: true 1343 + yaml: 1196 1344 optional: true 1197 1345 1198 1346 postcss-nested@6.2.0: ··· 1208 1356 postcss-value-parser@4.2.0: 1209 1357 resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 1210 1358 1211 - postcss@8.5.1: 1212 - resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} 1359 + postcss@8.5.6: 1360 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1213 1361 engines: {node: ^10 || ^12 || >=14} 1214 1362 1215 - prettier-plugin-tailwindcss@0.6.11: 1216 - resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==} 1363 + prettier-plugin-tailwindcss@0.6.14: 1364 + resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} 1217 1365 engines: {node: '>=14.21.3'} 1218 1366 peerDependencies: 1219 1367 '@ianvs/prettier-plugin-sort-imports': '*' 1368 + '@prettier/plugin-hermes': '*' 1369 + '@prettier/plugin-oxc': '*' 1220 1370 '@prettier/plugin-pug': '*' 1221 1371 '@shopify/prettier-plugin-liquid': '*' 1222 1372 '@trivago/prettier-plugin-sort-imports': '*' ··· 1236 1386 peerDependenciesMeta: 1237 1387 '@ianvs/prettier-plugin-sort-imports': 1238 1388 optional: true 1389 + '@prettier/plugin-hermes': 1390 + optional: true 1391 + '@prettier/plugin-oxc': 1392 + optional: true 1239 1393 '@prettier/plugin-pug': 1240 1394 optional: true 1241 1395 '@shopify/prettier-plugin-liquid': ··· 1267 1421 prettier-plugin-svelte: 1268 1422 optional: true 1269 1423 1270 - prettier@3.4.2: 1271 - resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} 1424 + prettier@3.7.4: 1425 + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} 1272 1426 engines: {node: '>=14'} 1273 1427 hasBin: true 1274 - 1275 - printable-characters@1.0.42: 1276 - resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 1277 1428 1278 1429 queue-microtask@1.2.3: 1279 1430 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} ··· 1285 1436 resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1286 1437 engines: {node: '>=8.10.0'} 1287 1438 1288 - resolve@1.22.10: 1289 - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 1439 + resolve@1.22.11: 1440 + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} 1290 1441 engines: {node: '>= 0.4'} 1291 1442 hasBin: true 1292 1443 1293 - reusify@1.0.4: 1294 - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 1444 + reusify@1.1.0: 1445 + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 1295 1446 engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1296 1447 1297 - rollup-plugin-inject@3.0.2: 1298 - resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} 1299 - deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. 1300 - 1301 - rollup-plugin-node-polyfills@0.2.1: 1302 - resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} 1303 - 1304 - rollup-pluginutils@2.8.2: 1305 - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 1306 - 1307 - rollup@4.32.1: 1308 - resolution: {integrity: sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==} 1448 + rollup@4.53.3: 1449 + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} 1309 1450 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1310 1451 hasBin: true 1311 1452 ··· 1316 1457 resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1317 1458 hasBin: true 1318 1459 1319 - seroval-plugins@1.2.0: 1320 - resolution: {integrity: sha512-hULTbfzSe81jGWLH8TAJjkEvw6JWMqOo9Uq+4V4vg+HNq53hyHldM9ZOfjdzokcFysiTp9aFdV2vJpZFqKeDjQ==} 1460 + semver@7.7.3: 1461 + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 1462 + engines: {node: '>=10'} 1463 + hasBin: true 1464 + 1465 + seroval-plugins@1.3.3: 1466 + resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} 1321 1467 engines: {node: '>=10'} 1322 1468 peerDependencies: 1323 1469 seroval: ^1.0 1324 1470 1325 - seroval@1.2.0: 1326 - resolution: {integrity: sha512-GURoU99ko2UiAgUC3qDCk59Jb3Ss4Po8VIMGkG8j5PFo2Q7y0YSMP8QG9NuL/fJCoTz9V1XZUbpNIMXPOfaGpA==} 1471 + seroval@1.3.2: 1472 + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} 1327 1473 engines: {node: '>=10'} 1328 1474 1329 - shebang-command@2.0.0: 1330 - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1331 - engines: {node: '>=8'} 1332 - 1333 - shebang-regex@3.0.0: 1334 - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1335 - engines: {node: '>=8'} 1475 + sharp@0.33.5: 1476 + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 1477 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1336 1478 1337 - signal-exit@4.1.0: 1338 - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1339 - engines: {node: '>=14'} 1479 + simple-swizzle@0.2.4: 1480 + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} 1340 1481 1341 - solid-js@1.9.4: 1342 - resolution: {integrity: sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==} 1482 + solid-js@1.9.10: 1483 + resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} 1343 1484 1344 1485 solid-refresh@0.6.3: 1345 1486 resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} ··· 1356 1497 source-map@0.6.1: 1357 1498 resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1358 1499 engines: {node: '>=0.10.0'} 1359 - 1360 - sourcemap-codec@1.4.8: 1361 - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 1362 - deprecated: Please use @jridgewell/sourcemap-codec instead 1363 - 1364 - stacktracey@2.1.8: 1365 - resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 1366 1500 1367 1501 stoppable@1.1.0: 1368 1502 resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 1369 1503 engines: {node: '>=4', npm: '>=6'} 1370 1504 1371 - string-width@4.2.3: 1372 - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1373 - engines: {node: '>=8'} 1374 - 1375 - string-width@5.1.2: 1376 - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 1377 - engines: {node: '>=12'} 1378 - 1379 - strip-ansi@6.0.1: 1380 - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1381 - engines: {node: '>=8'} 1382 - 1383 - strip-ansi@7.1.0: 1384 - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 1385 - engines: {node: '>=12'} 1386 - 1387 - sucrase@3.35.0: 1388 - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 1505 + sucrase@3.35.1: 1506 + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} 1389 1507 engines: {node: '>=16 || 14 >=14.17'} 1390 1508 hasBin: true 1391 1509 1510 + supports-color@10.2.2: 1511 + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} 1512 + engines: {node: '>=18'} 1513 + 1392 1514 supports-preserve-symlinks-flag@1.0.0: 1393 1515 resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 1394 1516 engines: {node: '>= 0.4'} 1395 1517 1396 - tailwindcss@3.4.17: 1397 - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} 1518 + tailwindcss@3.4.18: 1519 + resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} 1398 1520 engines: {node: '>=14.0.0'} 1399 1521 hasBin: true 1400 1522 1401 - terser@5.37.0: 1402 - resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} 1523 + terser@5.44.1: 1524 + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} 1403 1525 engines: {node: '>=10'} 1404 1526 hasBin: true 1405 1527 ··· 1409 1531 1410 1532 thenify@3.3.1: 1411 1533 resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 1534 + 1535 + tinyglobby@0.2.15: 1536 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1537 + engines: {node: '>=12.0.0'} 1412 1538 1413 1539 to-regex-range@5.0.1: 1414 1540 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} ··· 1420 1546 tslib@2.8.1: 1421 1547 resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1422 1548 1423 - typescript@5.7.2: 1424 - resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} 1549 + typescript@5.9.3: 1550 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1425 1551 engines: {node: '>=14.17'} 1426 1552 hasBin: true 1427 1553 1428 - ufo@1.5.4: 1429 - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} 1554 + undici-types@6.21.0: 1555 + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1430 1556 1431 - undici-types@6.20.0: 1432 - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 1557 + undici@7.14.0: 1558 + resolution: {integrity: sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==} 1559 + engines: {node: '>=20.18.1'} 1433 1560 1434 - undici@5.28.5: 1435 - resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} 1436 - engines: {node: '>=14.0'} 1561 + unenv@2.0.0-rc.24: 1562 + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} 1437 1563 1438 - unenv@2.0.0-rc.1: 1439 - resolution: {integrity: sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==} 1440 - 1441 - update-browserslist-db@1.1.2: 1442 - resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} 1564 + update-browserslist-db@1.2.2: 1565 + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} 1443 1566 hasBin: true 1444 1567 peerDependencies: 1445 1568 browserslist: '>= 4.21.0' ··· 1447 1570 util-deprecate@1.0.2: 1448 1571 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1449 1572 1450 - validate-html-nesting@1.2.2: 1451 - resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} 1452 - 1453 - vite-plugin-solid@2.11.0: 1454 - resolution: {integrity: sha512-G+NiwDj4EAeUE0wt3Ur9f+Lt9oMUuLd0FIxYuqwJSqRacKQRteCwUFzNy8zMEt88xWokngQhiFjfJMhjc1fDXw==} 1573 + vite-plugin-solid@2.11.10: 1574 + resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} 1455 1575 peerDependencies: 1456 1576 '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 1457 1577 solid-js: ^1.7.2 1458 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 1578 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 1459 1579 peerDependenciesMeta: 1460 1580 '@testing-library/jest-dom': 1461 1581 optional: true 1462 1582 1463 - vite@6.0.11: 1464 - resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} 1465 - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1583 + vite@7.2.7: 1584 + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} 1585 + engines: {node: ^20.19.0 || >=22.12.0} 1466 1586 hasBin: true 1467 1587 peerDependencies: 1468 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1588 + '@types/node': ^20.19.0 || >=22.12.0 1469 1589 jiti: '>=1.21.0' 1470 - less: '*' 1590 + less: ^4.0.0 1471 1591 lightningcss: ^1.21.0 1472 - sass: '*' 1473 - sass-embedded: '*' 1474 - stylus: '*' 1475 - sugarss: '*' 1592 + sass: ^1.70.0 1593 + sass-embedded: ^1.70.0 1594 + stylus: '>=0.54.8' 1595 + sugarss: ^5.0.0 1476 1596 terser: ^5.16.0 1477 1597 tsx: ^4.8.1 1478 1598 yaml: ^2.4.2 ··· 1500 1620 yaml: 1501 1621 optional: true 1502 1622 1503 - vitefu@1.0.5: 1504 - resolution: {integrity: sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==} 1623 + vitefu@1.1.1: 1624 + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} 1505 1625 peerDependencies: 1506 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 1626 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 1507 1627 peerDependenciesMeta: 1508 1628 vite: 1509 1629 optional: true ··· 1512 1632 resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 1513 1633 engines: {node: '>= 8'} 1514 1634 1515 - which@2.0.2: 1516 - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1517 - engines: {node: '>= 8'} 1518 - hasBin: true 1519 - 1520 - workerd@1.20250124.0: 1521 - resolution: {integrity: sha512-EnT9gN3M9/UHRFPZptKgK36DLOW8WfJV7cjNs3zstVbmF5cpFaHCAzX7tXWBO6zyvW/+EjklJPFtOvfatiZsuQ==} 1635 + workerd@1.20251202.0: 1636 + resolution: {integrity: sha512-p08YfrUMHkjCECNdT36r+6DpJIZX4kixbZ4n6GMUcLR5Gh18fakSCsiQrh72iOm4M9QHv/rM7P8YvCrUPWT5sg==} 1522 1637 engines: {node: '>=16'} 1523 1638 hasBin: true 1524 1639 1525 - wrangler@3.106.0: 1526 - resolution: {integrity: sha512-jKPBtASIdiihU9AJBJRvWktqdc2prTy41LUjMk6Sq6BCZePrDnS9VWhQWovoYojreSd+dKhU+ggL53fNKvifRg==} 1527 - engines: {node: '>=16.17.0'} 1640 + wrangler@4.53.0: 1641 + resolution: {integrity: sha512-/wvnHlRnlHsqaeIgGbmcEJE5NFYdTUWHCKow+U5Tv2XwQXI9vXUqBwCLAGy/BwqyS5nnycRt2kppqCzgHgyb7Q==} 1642 + engines: {node: '>=20.0.0'} 1528 1643 hasBin: true 1529 1644 peerDependencies: 1530 - '@cloudflare/workers-types': ^4.20250121.0 1645 + '@cloudflare/workers-types': ^4.20251202.0 1531 1646 peerDependenciesMeta: 1532 1647 '@cloudflare/workers-types': 1533 1648 optional: true 1534 1649 1535 - wrap-ansi@7.0.0: 1536 - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1537 - engines: {node: '>=10'} 1538 - 1539 - wrap-ansi@8.1.0: 1540 - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 1541 - engines: {node: '>=12'} 1542 - 1543 1650 ws@8.18.0: 1544 1651 resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 1545 1652 engines: {node: '>=10.0.0'} ··· 1555 1662 yallist@3.1.1: 1556 1663 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1557 1664 1558 - yaml@2.7.0: 1559 - resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} 1560 - engines: {node: '>= 14'} 1561 - hasBin: true 1665 + youch-core@0.3.3: 1666 + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} 1562 1667 1563 - youch@3.3.4: 1564 - resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} 1668 + youch@4.1.0-beta.10: 1669 + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} 1565 1670 1566 - zod@3.24.1: 1567 - resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} 1671 + zod@3.22.3: 1672 + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} 1568 1673 1569 1674 snapshots: 1570 1675 1571 1676 '@alloc/quick-lru@5.2.0': {} 1572 1677 1573 - '@ampproject/remapping@2.3.0': 1678 + '@atcute/atproto@3.1.9': 1679 + dependencies: 1680 + '@atcute/lexicons': 1.2.5 1681 + 1682 + '@atcute/bluesky@3.2.13': 1683 + dependencies: 1684 + '@atcute/atproto': 3.1.9 1685 + '@atcute/lexicons': 1.2.5 1686 + 1687 + '@atcute/car@5.0.0': 1688 + dependencies: 1689 + '@atcute/cbor': 2.2.8 1690 + '@atcute/cid': 2.2.6 1691 + '@atcute/uint8array': 1.0.6 1692 + '@atcute/varint': 1.0.3 1693 + 1694 + '@atcute/cbor@2.2.8': 1695 + dependencies: 1696 + '@atcute/cid': 2.2.6 1697 + '@atcute/multibase': 1.1.6 1698 + '@atcute/uint8array': 1.0.6 1699 + 1700 + '@atcute/cid@2.2.6': 1574 1701 dependencies: 1575 - '@jridgewell/gen-mapping': 0.3.8 1576 - '@jridgewell/trace-mapping': 0.3.25 1702 + '@atcute/multibase': 1.1.6 1703 + '@atcute/uint8array': 1.0.6 1577 1704 1578 - '@atcute/bluesky@1.0.12(@atcute/client@2.0.7)': 1705 + '@atcute/client@4.1.1': 1706 + dependencies: 1707 + '@atcute/identity': 1.1.3 1708 + '@atcute/lexicons': 1.2.5 1709 + 1710 + '@atcute/crypto@2.3.0': 1711 + dependencies: 1712 + '@atcute/multibase': 1.1.6 1713 + '@atcute/uint8array': 1.0.6 1714 + '@noble/secp256k1': 3.0.0 1715 + 1716 + '@atcute/did-plc@0.2.0': 1579 1717 dependencies: 1580 - '@atcute/client': 2.0.7 1718 + '@atcute/cbor': 2.2.8 1719 + '@atcute/cid': 2.2.6 1720 + '@atcute/crypto': 2.3.0 1721 + '@atcute/identity': 1.1.3 1722 + '@atcute/lexicons': 1.2.5 1723 + '@atcute/multibase': 1.1.6 1724 + '@atcute/uint8array': 1.0.6 1725 + '@badrap/valita': 0.4.6 1581 1726 1582 - '@atcute/car@2.0.1': 1727 + '@atcute/identity-resolver@1.2.0(@atcute/identity@1.1.3)': 1583 1728 dependencies: 1584 - '@atcute/cbor': 2.1.1 1585 - '@atcute/cid': 2.1.0 1586 - '@atcute/varint': 1.0.2 1729 + '@atcute/identity': 1.1.3 1730 + '@atcute/lexicons': 1.2.5 1731 + '@atcute/util-fetch': 1.0.4 1732 + '@badrap/valita': 0.4.6 1587 1733 1588 - '@atcute/cbor@2.1.1': 1734 + '@atcute/identity@1.1.3': 1589 1735 dependencies: 1590 - '@atcute/cid': 2.1.0 1591 - '@atcute/multibase': 1.1.2 1592 - '@atcute/uint8array': 1.0.1 1736 + '@atcute/lexicons': 1.2.5 1737 + '@badrap/valita': 0.4.6 1593 1738 1594 - '@atcute/cid@2.1.0': 1739 + '@atcute/lexicons@1.2.5': 1595 1740 dependencies: 1596 - '@atcute/multibase': 1.1.2 1597 - '@atcute/uint8array': 1.0.1 1598 - '@atcute/varint': 1.0.2 1741 + '@standard-schema/spec': 1.0.0 1742 + esm-env: 1.2.2 1599 1743 1600 - '@atcute/client@2.0.7': {} 1744 + '@atcute/mst@0.1.0': 1745 + dependencies: 1746 + '@atcute/cbor': 2.2.8 1747 + '@atcute/cid': 2.2.6 1748 + '@atcute/uint8array': 1.0.6 1601 1749 1602 - '@atcute/crypto@2.2.0': 1750 + '@atcute/multibase@1.1.6': 1603 1751 dependencies: 1604 - '@atcute/multibase': 1.1.2 1605 - '@atcute/uint8array': 1.0.1 1606 - '@noble/secp256k1': 2.2.3 1752 + '@atcute/uint8array': 1.0.6 1607 1753 1608 - '@atcute/multibase@1.1.2': 1754 + '@atcute/repo@0.1.0': 1609 1755 dependencies: 1610 - '@atcute/uint8array': 1.0.1 1756 + '@atcute/car': 5.0.0 1757 + '@atcute/cbor': 2.2.8 1758 + '@atcute/cid': 2.2.6 1759 + '@atcute/crypto': 2.3.0 1760 + '@atcute/lexicons': 1.2.5 1761 + '@atcute/mst': 0.1.0 1762 + '@atcute/uint8array': 1.0.6 1611 1763 1612 - '@atcute/uint8array@1.0.1': {} 1764 + '@atcute/tid@1.0.3': {} 1613 1765 1614 - '@atcute/varint@1.0.2': {} 1766 + '@atcute/uint8array@1.0.6': {} 1615 1767 1616 - '@babel/code-frame@7.26.2': 1768 + '@atcute/util-fetch@1.0.4': 1617 1769 dependencies: 1618 - '@babel/helper-validator-identifier': 7.25.9 1770 + '@badrap/valita': 0.4.6 1771 + 1772 + '@atcute/varint@1.0.3': {} 1773 + 1774 + '@babel/code-frame@7.27.1': 1775 + dependencies: 1776 + '@babel/helper-validator-identifier': 7.28.5 1619 1777 js-tokens: 4.0.0 1620 1778 picocolors: 1.1.1 1621 1779 1622 - '@babel/compat-data@7.26.5': {} 1780 + '@babel/compat-data@7.28.5': {} 1623 1781 1624 - '@babel/core@7.26.7': 1782 + '@babel/core@7.28.5': 1625 1783 dependencies: 1626 - '@ampproject/remapping': 2.3.0 1627 - '@babel/code-frame': 7.26.2 1628 - '@babel/generator': 7.26.5 1629 - '@babel/helper-compilation-targets': 7.26.5 1630 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.7) 1631 - '@babel/helpers': 7.26.7 1632 - '@babel/parser': 7.26.7 1633 - '@babel/template': 7.25.9 1634 - '@babel/traverse': 7.26.7 1635 - '@babel/types': 7.26.7 1784 + '@babel/code-frame': 7.27.1 1785 + '@babel/generator': 7.28.5 1786 + '@babel/helper-compilation-targets': 7.27.2 1787 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) 1788 + '@babel/helpers': 7.28.4 1789 + '@babel/parser': 7.28.5 1790 + '@babel/template': 7.27.2 1791 + '@babel/traverse': 7.28.5 1792 + '@babel/types': 7.28.5 1793 + '@jridgewell/remapping': 2.3.5 1636 1794 convert-source-map: 2.0.0 1637 - debug: 4.4.0 1795 + debug: 4.4.3 1638 1796 gensync: 1.0.0-beta.2 1639 1797 json5: 2.2.3 1640 1798 semver: 6.3.1 1641 1799 transitivePeerDependencies: 1642 1800 - supports-color 1643 1801 1644 - '@babel/generator@7.26.5': 1802 + '@babel/generator@7.28.5': 1645 1803 dependencies: 1646 - '@babel/parser': 7.26.7 1647 - '@babel/types': 7.26.7 1648 - '@jridgewell/gen-mapping': 0.3.8 1649 - '@jridgewell/trace-mapping': 0.3.25 1804 + '@babel/parser': 7.28.5 1805 + '@babel/types': 7.28.5 1806 + '@jridgewell/gen-mapping': 0.3.13 1807 + '@jridgewell/trace-mapping': 0.3.31 1650 1808 jsesc: 3.1.0 1651 1809 1652 - '@babel/helper-compilation-targets@7.26.5': 1810 + '@babel/helper-compilation-targets@7.27.2': 1653 1811 dependencies: 1654 - '@babel/compat-data': 7.26.5 1655 - '@babel/helper-validator-option': 7.25.9 1656 - browserslist: 4.24.4 1812 + '@babel/compat-data': 7.28.5 1813 + '@babel/helper-validator-option': 7.27.1 1814 + browserslist: 4.28.1 1657 1815 lru-cache: 5.1.1 1658 1816 semver: 6.3.1 1817 + 1818 + '@babel/helper-globals@7.28.0': {} 1659 1819 1660 1820 '@babel/helper-module-imports@7.18.6': 1661 1821 dependencies: 1662 - '@babel/types': 7.26.7 1822 + '@babel/types': 7.28.5 1663 1823 1664 - '@babel/helper-module-imports@7.25.9': 1824 + '@babel/helper-module-imports@7.27.1': 1665 1825 dependencies: 1666 - '@babel/traverse': 7.26.7 1667 - '@babel/types': 7.26.7 1826 + '@babel/traverse': 7.28.5 1827 + '@babel/types': 7.28.5 1668 1828 transitivePeerDependencies: 1669 1829 - supports-color 1670 1830 1671 - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.7)': 1831 + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': 1672 1832 dependencies: 1673 - '@babel/core': 7.26.7 1674 - '@babel/helper-module-imports': 7.25.9 1675 - '@babel/helper-validator-identifier': 7.25.9 1676 - '@babel/traverse': 7.26.7 1833 + '@babel/core': 7.28.5 1834 + '@babel/helper-module-imports': 7.27.1 1835 + '@babel/helper-validator-identifier': 7.28.5 1836 + '@babel/traverse': 7.28.5 1677 1837 transitivePeerDependencies: 1678 1838 - supports-color 1679 1839 1680 - '@babel/helper-plugin-utils@7.26.5': {} 1840 + '@babel/helper-plugin-utils@7.27.1': {} 1681 1841 1682 - '@babel/helper-string-parser@7.25.9': {} 1842 + '@babel/helper-string-parser@7.27.1': {} 1683 1843 1684 - '@babel/helper-validator-identifier@7.25.9': {} 1844 + '@babel/helper-validator-identifier@7.28.5': {} 1685 1845 1686 - '@babel/helper-validator-option@7.25.9': {} 1846 + '@babel/helper-validator-option@7.27.1': {} 1687 1847 1688 - '@babel/helpers@7.26.7': 1848 + '@babel/helpers@7.28.4': 1689 1849 dependencies: 1690 - '@babel/template': 7.25.9 1691 - '@babel/types': 7.26.7 1850 + '@babel/template': 7.27.2 1851 + '@babel/types': 7.28.5 1692 1852 1693 - '@babel/parser@7.26.7': 1853 + '@babel/parser@7.28.5': 1694 1854 dependencies: 1695 - '@babel/types': 7.26.7 1855 + '@babel/types': 7.28.5 1696 1856 1697 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.7)': 1857 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': 1698 1858 dependencies: 1699 - '@babel/core': 7.26.7 1700 - '@babel/helper-plugin-utils': 7.26.5 1859 + '@babel/core': 7.28.5 1860 + '@babel/helper-plugin-utils': 7.27.1 1701 1861 1702 - '@babel/template@7.25.9': 1862 + '@babel/template@7.27.2': 1703 1863 dependencies: 1704 - '@babel/code-frame': 7.26.2 1705 - '@babel/parser': 7.26.7 1706 - '@babel/types': 7.26.7 1864 + '@babel/code-frame': 7.27.1 1865 + '@babel/parser': 7.28.5 1866 + '@babel/types': 7.28.5 1707 1867 1708 - '@babel/traverse@7.26.7': 1868 + '@babel/traverse@7.28.5': 1709 1869 dependencies: 1710 - '@babel/code-frame': 7.26.2 1711 - '@babel/generator': 7.26.5 1712 - '@babel/parser': 7.26.7 1713 - '@babel/template': 7.25.9 1714 - '@babel/types': 7.26.7 1715 - debug: 4.4.0 1716 - globals: 11.12.0 1870 + '@babel/code-frame': 7.27.1 1871 + '@babel/generator': 7.28.5 1872 + '@babel/helper-globals': 7.28.0 1873 + '@babel/parser': 7.28.5 1874 + '@babel/template': 7.27.2 1875 + '@babel/types': 7.28.5 1876 + debug: 4.4.3 1717 1877 transitivePeerDependencies: 1718 1878 - supports-color 1719 1879 1720 - '@babel/types@7.26.7': 1880 + '@babel/types@7.28.5': 1721 1881 dependencies: 1722 - '@babel/helper-string-parser': 7.25.9 1723 - '@babel/helper-validator-identifier': 7.25.9 1882 + '@babel/helper-string-parser': 7.27.1 1883 + '@babel/helper-validator-identifier': 7.28.5 1724 1884 1725 - '@badrap/valita@0.4.2': {} 1885 + '@badrap/valita@0.4.6': {} 1726 1886 1727 - '@cloudflare/kv-asset-handler@0.3.4': 1887 + '@cloudflare/kv-asset-handler@0.4.1': 1728 1888 dependencies: 1729 1889 mime: 3.0.0 1730 1890 1731 - '@cloudflare/workerd-darwin-64@1.20250124.0': 1891 + '@cloudflare/unenv-preset@2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251202.0)': 1892 + dependencies: 1893 + unenv: 2.0.0-rc.24 1894 + optionalDependencies: 1895 + workerd: 1.20251202.0 1896 + 1897 + '@cloudflare/workerd-darwin-64@1.20251202.0': 1732 1898 optional: true 1733 1899 1734 - '@cloudflare/workerd-darwin-arm64@1.20250124.0': 1900 + '@cloudflare/workerd-darwin-arm64@1.20251202.0': 1735 1901 optional: true 1736 1902 1737 - '@cloudflare/workerd-linux-64@1.20250124.0': 1903 + '@cloudflare/workerd-linux-64@1.20251202.0': 1738 1904 optional: true 1739 1905 1740 - '@cloudflare/workerd-linux-arm64@1.20250124.0': 1906 + '@cloudflare/workerd-linux-arm64@1.20251202.0': 1741 1907 optional: true 1742 1908 1743 - '@cloudflare/workerd-windows-64@1.20250124.0': 1909 + '@cloudflare/workerd-windows-64@1.20251202.0': 1744 1910 optional: true 1745 1911 1746 1912 '@cspotcode/source-map-support@0.8.1': 1747 1913 dependencies: 1748 1914 '@jridgewell/trace-mapping': 0.3.9 1749 1915 1750 - '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': 1916 + '@emnapi/runtime@1.7.1': 1751 1917 dependencies: 1752 - esbuild: 0.17.19 1918 + tslib: 2.8.1 1919 + optional: true 1920 + 1921 + '@esbuild/aix-ppc64@0.25.12': 1922 + optional: true 1923 + 1924 + '@esbuild/aix-ppc64@0.27.0': 1925 + optional: true 1926 + 1927 + '@esbuild/android-arm64@0.25.12': 1928 + optional: true 1929 + 1930 + '@esbuild/android-arm64@0.27.0': 1931 + optional: true 1932 + 1933 + '@esbuild/android-arm@0.25.12': 1934 + optional: true 1935 + 1936 + '@esbuild/android-arm@0.27.0': 1937 + optional: true 1938 + 1939 + '@esbuild/android-x64@0.25.12': 1940 + optional: true 1941 + 1942 + '@esbuild/android-x64@0.27.0': 1943 + optional: true 1944 + 1945 + '@esbuild/darwin-arm64@0.25.12': 1946 + optional: true 1947 + 1948 + '@esbuild/darwin-arm64@0.27.0': 1949 + optional: true 1950 + 1951 + '@esbuild/darwin-x64@0.25.12': 1952 + optional: true 1753 1953 1754 - '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)': 1755 - dependencies: 1756 - esbuild: 0.17.19 1757 - escape-string-regexp: 4.0.0 1758 - rollup-plugin-node-polyfills: 0.2.1 1954 + '@esbuild/darwin-x64@0.27.0': 1955 + optional: true 1759 1956 1760 - '@esbuild/aix-ppc64@0.24.2': 1957 + '@esbuild/freebsd-arm64@0.25.12': 1761 1958 optional: true 1762 1959 1763 - '@esbuild/android-arm64@0.17.19': 1960 + '@esbuild/freebsd-arm64@0.27.0': 1764 1961 optional: true 1765 1962 1766 - '@esbuild/android-arm64@0.24.2': 1963 + '@esbuild/freebsd-x64@0.25.12': 1767 1964 optional: true 1768 1965 1769 - '@esbuild/android-arm@0.17.19': 1966 + '@esbuild/freebsd-x64@0.27.0': 1770 1967 optional: true 1771 1968 1772 - '@esbuild/android-arm@0.24.2': 1969 + '@esbuild/linux-arm64@0.25.12': 1773 1970 optional: true 1774 1971 1775 - '@esbuild/android-x64@0.17.19': 1972 + '@esbuild/linux-arm64@0.27.0': 1776 1973 optional: true 1777 1974 1778 - '@esbuild/android-x64@0.24.2': 1975 + '@esbuild/linux-arm@0.25.12': 1779 1976 optional: true 1780 1977 1781 - '@esbuild/darwin-arm64@0.17.19': 1978 + '@esbuild/linux-arm@0.27.0': 1782 1979 optional: true 1783 1980 1784 - '@esbuild/darwin-arm64@0.24.2': 1981 + '@esbuild/linux-ia32@0.25.12': 1785 1982 optional: true 1786 1983 1787 - '@esbuild/darwin-x64@0.17.19': 1984 + '@esbuild/linux-ia32@0.27.0': 1788 1985 optional: true 1789 1986 1790 - '@esbuild/darwin-x64@0.24.2': 1987 + '@esbuild/linux-loong64@0.25.12': 1791 1988 optional: true 1792 1989 1793 - '@esbuild/freebsd-arm64@0.17.19': 1990 + '@esbuild/linux-loong64@0.27.0': 1794 1991 optional: true 1795 1992 1796 - '@esbuild/freebsd-arm64@0.24.2': 1993 + '@esbuild/linux-mips64el@0.25.12': 1797 1994 optional: true 1798 1995 1799 - '@esbuild/freebsd-x64@0.17.19': 1996 + '@esbuild/linux-mips64el@0.27.0': 1800 1997 optional: true 1801 1998 1802 - '@esbuild/freebsd-x64@0.24.2': 1999 + '@esbuild/linux-ppc64@0.25.12': 1803 2000 optional: true 1804 2001 1805 - '@esbuild/linux-arm64@0.17.19': 2002 + '@esbuild/linux-ppc64@0.27.0': 1806 2003 optional: true 1807 2004 1808 - '@esbuild/linux-arm64@0.24.2': 2005 + '@esbuild/linux-riscv64@0.25.12': 1809 2006 optional: true 1810 2007 1811 - '@esbuild/linux-arm@0.17.19': 2008 + '@esbuild/linux-riscv64@0.27.0': 2009 + optional: true 2010 + 2011 + '@esbuild/linux-s390x@0.25.12': 2012 + optional: true 2013 + 2014 + '@esbuild/linux-s390x@0.27.0': 2015 + optional: true 2016 + 2017 + '@esbuild/linux-x64@0.25.12': 2018 + optional: true 2019 + 2020 + '@esbuild/linux-x64@0.27.0': 2021 + optional: true 2022 + 2023 + '@esbuild/netbsd-arm64@0.25.12': 2024 + optional: true 2025 + 2026 + '@esbuild/netbsd-arm64@0.27.0': 2027 + optional: true 2028 + 2029 + '@esbuild/netbsd-x64@0.25.12': 1812 2030 optional: true 1813 2031 1814 - '@esbuild/linux-arm@0.24.2': 2032 + '@esbuild/netbsd-x64@0.27.0': 1815 2033 optional: true 1816 2034 1817 - '@esbuild/linux-ia32@0.17.19': 2035 + '@esbuild/openbsd-arm64@0.25.12': 1818 2036 optional: true 1819 2037 1820 - '@esbuild/linux-ia32@0.24.2': 2038 + '@esbuild/openbsd-arm64@0.27.0': 1821 2039 optional: true 1822 2040 1823 - '@esbuild/linux-loong64@0.17.19': 2041 + '@esbuild/openbsd-x64@0.25.12': 1824 2042 optional: true 1825 2043 1826 - '@esbuild/linux-loong64@0.24.2': 2044 + '@esbuild/openbsd-x64@0.27.0': 1827 2045 optional: true 1828 2046 1829 - '@esbuild/linux-mips64el@0.17.19': 2047 + '@esbuild/openharmony-arm64@0.25.12': 2048 + optional: true 2049 + 2050 + '@esbuild/openharmony-arm64@0.27.0': 2051 + optional: true 2052 + 2053 + '@esbuild/sunos-x64@0.25.12': 1830 2054 optional: true 1831 2055 1832 - '@esbuild/linux-mips64el@0.24.2': 2056 + '@esbuild/sunos-x64@0.27.0': 1833 2057 optional: true 1834 2058 1835 - '@esbuild/linux-ppc64@0.17.19': 2059 + '@esbuild/win32-arm64@0.25.12': 1836 2060 optional: true 1837 2061 1838 - '@esbuild/linux-ppc64@0.24.2': 2062 + '@esbuild/win32-arm64@0.27.0': 1839 2063 optional: true 1840 2064 1841 - '@esbuild/linux-riscv64@0.17.19': 2065 + '@esbuild/win32-ia32@0.25.12': 1842 2066 optional: true 1843 2067 1844 - '@esbuild/linux-riscv64@0.24.2': 2068 + '@esbuild/win32-ia32@0.27.0': 1845 2069 optional: true 1846 2070 1847 - '@esbuild/linux-s390x@0.17.19': 2071 + '@esbuild/win32-x64@0.25.12': 1848 2072 optional: true 1849 2073 1850 - '@esbuild/linux-s390x@0.24.2': 2074 + '@esbuild/win32-x64@0.27.0': 1851 2075 optional: true 1852 2076 1853 - '@esbuild/linux-x64@0.17.19': 2077 + '@externdefs/solid-freeze@0.1.1(solid-js@1.9.10)': 2078 + dependencies: 2079 + solid-js: 1.9.10 2080 + 2081 + '@img/sharp-darwin-arm64@0.33.5': 2082 + optionalDependencies: 2083 + '@img/sharp-libvips-darwin-arm64': 1.0.4 1854 2084 optional: true 1855 2085 1856 - '@esbuild/linux-x64@0.24.2': 2086 + '@img/sharp-darwin-x64@0.33.5': 2087 + optionalDependencies: 2088 + '@img/sharp-libvips-darwin-x64': 1.0.4 1857 2089 optional: true 1858 2090 1859 - '@esbuild/netbsd-arm64@0.24.2': 2091 + '@img/sharp-libvips-darwin-arm64@1.0.4': 1860 2092 optional: true 1861 2093 1862 - '@esbuild/netbsd-x64@0.17.19': 2094 + '@img/sharp-libvips-darwin-x64@1.0.4': 1863 2095 optional: true 1864 2096 1865 - '@esbuild/netbsd-x64@0.24.2': 2097 + '@img/sharp-libvips-linux-arm64@1.0.4': 1866 2098 optional: true 1867 2099 1868 - '@esbuild/openbsd-arm64@0.24.2': 2100 + '@img/sharp-libvips-linux-arm@1.0.5': 1869 2101 optional: true 1870 2102 1871 - '@esbuild/openbsd-x64@0.17.19': 2103 + '@img/sharp-libvips-linux-s390x@1.0.4': 1872 2104 optional: true 1873 2105 1874 - '@esbuild/openbsd-x64@0.24.2': 2106 + '@img/sharp-libvips-linux-x64@1.0.4': 1875 2107 optional: true 1876 2108 1877 - '@esbuild/sunos-x64@0.17.19': 2109 + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 1878 2110 optional: true 1879 2111 1880 - '@esbuild/sunos-x64@0.24.2': 2112 + '@img/sharp-libvips-linuxmusl-x64@1.0.4': 1881 2113 optional: true 1882 2114 1883 - '@esbuild/win32-arm64@0.17.19': 2115 + '@img/sharp-linux-arm64@0.33.5': 2116 + optionalDependencies: 2117 + '@img/sharp-libvips-linux-arm64': 1.0.4 1884 2118 optional: true 1885 2119 1886 - '@esbuild/win32-arm64@0.24.2': 2120 + '@img/sharp-linux-arm@0.33.5': 2121 + optionalDependencies: 2122 + '@img/sharp-libvips-linux-arm': 1.0.5 1887 2123 optional: true 1888 2124 1889 - '@esbuild/win32-ia32@0.17.19': 2125 + '@img/sharp-linux-s390x@0.33.5': 2126 + optionalDependencies: 2127 + '@img/sharp-libvips-linux-s390x': 1.0.4 1890 2128 optional: true 1891 2129 1892 - '@esbuild/win32-ia32@0.24.2': 2130 + '@img/sharp-linux-x64@0.33.5': 2131 + optionalDependencies: 2132 + '@img/sharp-libvips-linux-x64': 1.0.4 1893 2133 optional: true 1894 2134 1895 - '@esbuild/win32-x64@0.17.19': 2135 + '@img/sharp-linuxmusl-arm64@0.33.5': 2136 + optionalDependencies: 2137 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 1896 2138 optional: true 1897 2139 1898 - '@esbuild/win32-x64@0.24.2': 2140 + '@img/sharp-linuxmusl-x64@0.33.5': 2141 + optionalDependencies: 2142 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 1899 2143 optional: true 1900 2144 1901 - '@externdefs/solid-freeze@0.1.1(solid-js@1.9.4)': 2145 + '@img/sharp-wasm32@0.33.5': 1902 2146 dependencies: 1903 - solid-js: 1.9.4 2147 + '@emnapi/runtime': 1.7.1 2148 + optional: true 1904 2149 1905 - '@fastify/busboy@2.1.1': {} 2150 + '@img/sharp-win32-ia32@0.33.5': 2151 + optional: true 1906 2152 1907 - '@isaacs/cliui@8.0.2': 2153 + '@img/sharp-win32-x64@0.33.5': 2154 + optional: true 2155 + 2156 + '@jridgewell/gen-mapping@0.3.13': 1908 2157 dependencies: 1909 - string-width: 5.1.2 1910 - string-width-cjs: string-width@4.2.3 1911 - strip-ansi: 7.1.0 1912 - strip-ansi-cjs: strip-ansi@6.0.1 1913 - wrap-ansi: 8.1.0 1914 - wrap-ansi-cjs: wrap-ansi@7.0.0 2158 + '@jridgewell/sourcemap-codec': 1.5.5 2159 + '@jridgewell/trace-mapping': 0.3.31 1915 2160 1916 - '@jridgewell/gen-mapping@0.3.8': 2161 + '@jridgewell/remapping@2.3.5': 1917 2162 dependencies: 1918 - '@jridgewell/set-array': 1.2.1 1919 - '@jridgewell/sourcemap-codec': 1.5.0 1920 - '@jridgewell/trace-mapping': 0.3.25 2163 + '@jridgewell/gen-mapping': 0.3.13 2164 + '@jridgewell/trace-mapping': 0.3.31 1921 2165 1922 2166 '@jridgewell/resolve-uri@3.1.2': {} 1923 2167 1924 - '@jridgewell/set-array@1.2.1': {} 1925 - 1926 - '@jridgewell/source-map@0.3.6': 2168 + '@jridgewell/source-map@0.3.11': 1927 2169 dependencies: 1928 - '@jridgewell/gen-mapping': 0.3.8 1929 - '@jridgewell/trace-mapping': 0.3.25 2170 + '@jridgewell/gen-mapping': 0.3.13 2171 + '@jridgewell/trace-mapping': 0.3.31 1930 2172 1931 - '@jridgewell/sourcemap-codec@1.5.0': {} 2173 + '@jridgewell/sourcemap-codec@1.5.5': {} 1932 2174 1933 - '@jridgewell/trace-mapping@0.3.25': 2175 + '@jridgewell/trace-mapping@0.3.31': 1934 2176 dependencies: 1935 2177 '@jridgewell/resolve-uri': 3.1.2 1936 - '@jridgewell/sourcemap-codec': 1.5.0 2178 + '@jridgewell/sourcemap-codec': 1.5.5 1937 2179 1938 2180 '@jridgewell/trace-mapping@0.3.9': 1939 2181 dependencies: 1940 2182 '@jridgewell/resolve-uri': 3.1.2 1941 - '@jridgewell/sourcemap-codec': 1.5.0 2183 + '@jridgewell/sourcemap-codec': 1.5.5 1942 2184 1943 - '@jsr/mary__array-fns@0.1.0': {} 2185 + '@jsr/mary__array-fns@0.1.5': {} 2186 + 2187 + '@jsr/mary__ds-queue@0.1.3': {} 1944 2188 1945 - '@jsr/mary__events@0.1.0': {} 2189 + '@jsr/mary__events@0.2.0': {} 1946 2190 1947 - '@jsr/mary__tar@0.2.4': {} 2191 + '@jsr/mary__tar@0.3.1': {} 1948 2192 1949 - '@noble/secp256k1@2.2.3': {} 2193 + '@noble/secp256k1@3.0.0': {} 1950 2194 1951 2195 '@nodelib/fs.scandir@2.1.5': 1952 2196 dependencies: ··· 1958 2202 '@nodelib/fs.walk@1.2.8': 1959 2203 dependencies: 1960 2204 '@nodelib/fs.scandir': 2.1.5 1961 - fastq: 1.18.0 2205 + fastq: 1.19.1 1962 2206 1963 - '@pkgjs/parseargs@0.11.0': 2207 + '@poppinss/colors@4.1.5': 2208 + dependencies: 2209 + kleur: 4.1.5 2210 + 2211 + '@poppinss/dumper@0.6.5': 2212 + dependencies: 2213 + '@poppinss/colors': 4.1.5 2214 + '@sindresorhus/is': 7.1.1 2215 + supports-color: 10.2.2 2216 + 2217 + '@poppinss/exception@1.2.2': {} 2218 + 2219 + '@rollup/rollup-android-arm-eabi@4.53.3': 1964 2220 optional: true 1965 2221 1966 - '@rollup/rollup-android-arm-eabi@4.32.1': 2222 + '@rollup/rollup-android-arm64@4.53.3': 1967 2223 optional: true 1968 2224 1969 - '@rollup/rollup-android-arm64@4.32.1': 2225 + '@rollup/rollup-darwin-arm64@4.53.3': 1970 2226 optional: true 1971 2227 1972 - '@rollup/rollup-darwin-arm64@4.32.1': 2228 + '@rollup/rollup-darwin-x64@4.53.3': 1973 2229 optional: true 1974 2230 1975 - '@rollup/rollup-darwin-x64@4.32.1': 2231 + '@rollup/rollup-freebsd-arm64@4.53.3': 1976 2232 optional: true 1977 2233 1978 - '@rollup/rollup-freebsd-arm64@4.32.1': 2234 + '@rollup/rollup-freebsd-x64@4.53.3': 1979 2235 optional: true 1980 2236 1981 - '@rollup/rollup-freebsd-x64@4.32.1': 2237 + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 1982 2238 optional: true 1983 2239 1984 - '@rollup/rollup-linux-arm-gnueabihf@4.32.1': 2240 + '@rollup/rollup-linux-arm-musleabihf@4.53.3': 1985 2241 optional: true 1986 2242 1987 - '@rollup/rollup-linux-arm-musleabihf@4.32.1': 2243 + '@rollup/rollup-linux-arm64-gnu@4.53.3': 1988 2244 optional: true 1989 2245 1990 - '@rollup/rollup-linux-arm64-gnu@4.32.1': 2246 + '@rollup/rollup-linux-arm64-musl@4.53.3': 1991 2247 optional: true 1992 2248 1993 - '@rollup/rollup-linux-arm64-musl@4.32.1': 2249 + '@rollup/rollup-linux-loong64-gnu@4.53.3': 1994 2250 optional: true 1995 2251 1996 - '@rollup/rollup-linux-loongarch64-gnu@4.32.1': 2252 + '@rollup/rollup-linux-ppc64-gnu@4.53.3': 1997 2253 optional: true 1998 2254 1999 - '@rollup/rollup-linux-powerpc64le-gnu@4.32.1': 2255 + '@rollup/rollup-linux-riscv64-gnu@4.53.3': 2000 2256 optional: true 2001 2257 2002 - '@rollup/rollup-linux-riscv64-gnu@4.32.1': 2258 + '@rollup/rollup-linux-riscv64-musl@4.53.3': 2003 2259 optional: true 2004 2260 2005 - '@rollup/rollup-linux-s390x-gnu@4.32.1': 2261 + '@rollup/rollup-linux-s390x-gnu@4.53.3': 2262 + optional: true 2263 + 2264 + '@rollup/rollup-linux-x64-gnu@4.53.3': 2265 + optional: true 2266 + 2267 + '@rollup/rollup-linux-x64-musl@4.53.3': 2006 2268 optional: true 2007 2269 2008 - '@rollup/rollup-linux-x64-gnu@4.32.1': 2270 + '@rollup/rollup-openharmony-arm64@4.53.3': 2009 2271 optional: true 2010 2272 2011 - '@rollup/rollup-linux-x64-musl@4.32.1': 2273 + '@rollup/rollup-win32-arm64-msvc@4.53.3': 2012 2274 optional: true 2013 2275 2014 - '@rollup/rollup-win32-arm64-msvc@4.32.1': 2276 + '@rollup/rollup-win32-ia32-msvc@4.53.3': 2015 2277 optional: true 2016 2278 2017 - '@rollup/rollup-win32-ia32-msvc@4.32.1': 2279 + '@rollup/rollup-win32-x64-gnu@4.53.3': 2018 2280 optional: true 2019 2281 2020 - '@rollup/rollup-win32-x64-msvc@4.32.1': 2282 + '@rollup/rollup-win32-x64-msvc@4.53.3': 2021 2283 optional: true 2022 2284 2023 - '@tailwindcss/forms@0.5.10(tailwindcss@3.4.17)': 2285 + '@sindresorhus/is@7.1.1': {} 2286 + 2287 + '@speed-highlight/core@1.2.12': {} 2288 + 2289 + '@standard-schema/spec@1.0.0': {} 2290 + 2291 + '@tailwindcss/forms@0.5.10(tailwindcss@3.4.18)': 2024 2292 dependencies: 2025 2293 mini-svg-data-uri: 1.4.4 2026 - tailwindcss: 3.4.17 2294 + tailwindcss: 3.4.18 2027 2295 2028 2296 '@types/babel__core@7.20.5': 2029 2297 dependencies: 2030 - '@babel/parser': 7.26.7 2031 - '@babel/types': 7.26.7 2032 - '@types/babel__generator': 7.6.8 2298 + '@babel/parser': 7.28.5 2299 + '@babel/types': 7.28.5 2300 + '@types/babel__generator': 7.27.0 2033 2301 '@types/babel__template': 7.4.4 2034 - '@types/babel__traverse': 7.20.6 2302 + '@types/babel__traverse': 7.28.0 2035 2303 2036 - '@types/babel__generator@7.6.8': 2304 + '@types/babel__generator@7.27.0': 2037 2305 dependencies: 2038 - '@babel/types': 7.26.7 2306 + '@babel/types': 7.28.5 2039 2307 2040 2308 '@types/babel__template@7.4.4': 2041 2309 dependencies: 2042 - '@babel/parser': 7.26.7 2043 - '@babel/types': 7.26.7 2310 + '@babel/parser': 7.28.5 2311 + '@babel/types': 7.28.5 2044 2312 2045 - '@types/babel__traverse@7.20.6': 2313 + '@types/babel__traverse@7.28.0': 2046 2314 dependencies: 2047 - '@babel/types': 7.26.7 2315 + '@babel/types': 7.28.5 2048 2316 2049 - '@types/estree@1.0.6': {} 2317 + '@types/estree@1.0.8': {} 2050 2318 2051 - '@types/node@22.12.0': 2319 + '@types/node@22.19.2': 2052 2320 dependencies: 2053 - undici-types: 6.20.0 2321 + undici-types: 6.21.0 2054 2322 2055 - acorn-walk@8.3.4: 2056 - dependencies: 2057 - acorn: 8.14.0 2323 + acorn-walk@8.3.2: {} 2058 2324 2059 2325 acorn@8.14.0: {} 2060 2326 2061 - ansi-regex@5.0.1: {} 2062 - 2063 - ansi-regex@6.1.0: {} 2064 - 2065 - ansi-styles@4.3.0: 2066 - dependencies: 2067 - color-convert: 2.0.1 2068 - 2069 - ansi-styles@6.2.1: {} 2327 + acorn@8.15.0: {} 2070 2328 2071 2329 any-promise@1.3.0: {} 2072 2330 ··· 2077 2335 2078 2336 arg@5.0.2: {} 2079 2337 2080 - as-table@1.0.55: 2081 - dependencies: 2082 - printable-characters: 1.0.42 2083 - 2084 - autoprefixer@10.4.20(postcss@8.5.1): 2338 + autoprefixer@10.4.22(postcss@8.5.6): 2085 2339 dependencies: 2086 - browserslist: 4.24.4 2087 - caniuse-lite: 1.0.30001696 2088 - fraction.js: 4.3.7 2340 + browserslist: 4.28.1 2341 + caniuse-lite: 1.0.30001760 2342 + fraction.js: 5.3.4 2089 2343 normalize-range: 0.1.2 2090 2344 picocolors: 1.1.1 2091 - postcss: 8.5.1 2345 + postcss: 8.5.6 2092 2346 postcss-value-parser: 4.2.0 2093 2347 2094 - babel-plugin-jsx-dom-expressions@0.39.6(@babel/core@7.26.7): 2348 + babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5): 2095 2349 dependencies: 2096 - '@babel/core': 7.26.7 2350 + '@babel/core': 7.28.5 2097 2351 '@babel/helper-module-imports': 7.18.6 2098 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.7) 2099 - '@babel/types': 7.26.7 2352 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) 2353 + '@babel/types': 7.28.5 2100 2354 html-entities: 2.3.3 2101 - parse5: 7.2.1 2102 - validate-html-nesting: 1.2.2 2355 + parse5: 7.3.0 2103 2356 2104 - babel-preset-solid@1.9.3(@babel/core@7.26.7): 2357 + babel-preset-solid@1.9.10(@babel/core@7.28.5)(solid-js@1.9.10): 2105 2358 dependencies: 2106 - '@babel/core': 7.26.7 2107 - babel-plugin-jsx-dom-expressions: 0.39.6(@babel/core@7.26.7) 2359 + '@babel/core': 7.28.5 2360 + babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.28.5) 2361 + optionalDependencies: 2362 + solid-js: 1.9.10 2108 2363 2109 - balanced-match@1.0.2: {} 2364 + baseline-browser-mapping@2.9.5: {} 2110 2365 2111 2366 binary-extensions@2.3.0: {} 2112 2367 2113 2368 blake3-wasm@2.1.5: {} 2114 2369 2115 - brace-expansion@2.0.1: 2116 - dependencies: 2117 - balanced-match: 1.0.2 2118 - 2119 2370 braces@3.0.3: 2120 2371 dependencies: 2121 2372 fill-range: 7.1.1 2122 2373 2123 - browserslist@4.24.4: 2374 + browserslist@4.28.1: 2124 2375 dependencies: 2125 - caniuse-lite: 1.0.30001696 2126 - electron-to-chromium: 1.5.90 2127 - node-releases: 2.0.19 2128 - update-browserslist-db: 1.1.2(browserslist@4.24.4) 2376 + baseline-browser-mapping: 2.9.5 2377 + caniuse-lite: 1.0.30001760 2378 + electron-to-chromium: 1.5.267 2379 + node-releases: 2.0.27 2380 + update-browserslist-db: 1.2.2(browserslist@4.28.1) 2129 2381 2130 2382 buffer-from@1.1.2: {} 2131 2383 2132 2384 camelcase-css@2.0.1: {} 2133 2385 2134 - caniuse-lite@1.0.30001696: {} 2135 - 2136 - capnp-ts@0.7.0: 2137 - dependencies: 2138 - debug: 4.4.0 2139 - tslib: 2.8.1 2140 - transitivePeerDependencies: 2141 - - supports-color 2386 + caniuse-lite@1.0.30001760: {} 2142 2387 2143 2388 chokidar@3.6.0: 2144 2389 dependencies: ··· 2158 2403 2159 2404 color-name@1.1.4: {} 2160 2405 2406 + color-string@1.9.1: 2407 + dependencies: 2408 + color-name: 1.1.4 2409 + simple-swizzle: 0.2.4 2410 + 2411 + color@4.2.3: 2412 + dependencies: 2413 + color-convert: 2.0.1 2414 + color-string: 1.9.1 2415 + 2161 2416 commander@2.20.3: {} 2162 2417 2163 2418 commander@4.1.1: {} 2164 - 2165 - confbox@0.1.8: {} 2166 2419 2167 2420 convert-source-map@2.0.0: {} 2168 2421 2169 - cookie@0.7.2: {} 2170 - 2171 - cross-spawn@7.0.6: 2172 - dependencies: 2173 - path-key: 3.1.1 2174 - shebang-command: 2.0.0 2175 - which: 2.0.2 2422 + cookie@1.1.1: {} 2176 2423 2177 2424 cssesc@3.0.0: {} 2178 2425 2179 - csstype@3.1.3: {} 2426 + csstype@3.2.3: {} 2180 2427 2181 - data-uri-to-buffer@2.0.2: {} 2182 - 2183 - debug@4.4.0: 2428 + debug@4.4.3: 2184 2429 dependencies: 2185 2430 ms: 2.1.3 2186 2431 2187 - defu@6.1.4: {} 2432 + detect-libc@2.1.2: {} 2188 2433 2189 2434 didyoumean@1.2.2: {} 2190 2435 2191 2436 dlv@1.1.3: {} 2192 2437 2193 - eastasianwidth@0.2.0: {} 2438 + electron-to-chromium@1.5.267: {} 2194 2439 2195 - electron-to-chromium@1.5.90: {} 2440 + entities@6.0.1: {} 2196 2441 2197 - emoji-regex@8.0.0: {} 2442 + error-stack-parser-es@1.0.5: {} 2198 2443 2199 - emoji-regex@9.2.2: {} 2200 - 2201 - entities@4.5.0: {} 2202 - 2203 - esbuild@0.17.19: 2444 + esbuild@0.25.12: 2204 2445 optionalDependencies: 2205 - '@esbuild/android-arm': 0.17.19 2206 - '@esbuild/android-arm64': 0.17.19 2207 - '@esbuild/android-x64': 0.17.19 2208 - '@esbuild/darwin-arm64': 0.17.19 2209 - '@esbuild/darwin-x64': 0.17.19 2210 - '@esbuild/freebsd-arm64': 0.17.19 2211 - '@esbuild/freebsd-x64': 0.17.19 2212 - '@esbuild/linux-arm': 0.17.19 2213 - '@esbuild/linux-arm64': 0.17.19 2214 - '@esbuild/linux-ia32': 0.17.19 2215 - '@esbuild/linux-loong64': 0.17.19 2216 - '@esbuild/linux-mips64el': 0.17.19 2217 - '@esbuild/linux-ppc64': 0.17.19 2218 - '@esbuild/linux-riscv64': 0.17.19 2219 - '@esbuild/linux-s390x': 0.17.19 2220 - '@esbuild/linux-x64': 0.17.19 2221 - '@esbuild/netbsd-x64': 0.17.19 2222 - '@esbuild/openbsd-x64': 0.17.19 2223 - '@esbuild/sunos-x64': 0.17.19 2224 - '@esbuild/win32-arm64': 0.17.19 2225 - '@esbuild/win32-ia32': 0.17.19 2226 - '@esbuild/win32-x64': 0.17.19 2446 + '@esbuild/aix-ppc64': 0.25.12 2447 + '@esbuild/android-arm': 0.25.12 2448 + '@esbuild/android-arm64': 0.25.12 2449 + '@esbuild/android-x64': 0.25.12 2450 + '@esbuild/darwin-arm64': 0.25.12 2451 + '@esbuild/darwin-x64': 0.25.12 2452 + '@esbuild/freebsd-arm64': 0.25.12 2453 + '@esbuild/freebsd-x64': 0.25.12 2454 + '@esbuild/linux-arm': 0.25.12 2455 + '@esbuild/linux-arm64': 0.25.12 2456 + '@esbuild/linux-ia32': 0.25.12 2457 + '@esbuild/linux-loong64': 0.25.12 2458 + '@esbuild/linux-mips64el': 0.25.12 2459 + '@esbuild/linux-ppc64': 0.25.12 2460 + '@esbuild/linux-riscv64': 0.25.12 2461 + '@esbuild/linux-s390x': 0.25.12 2462 + '@esbuild/linux-x64': 0.25.12 2463 + '@esbuild/netbsd-arm64': 0.25.12 2464 + '@esbuild/netbsd-x64': 0.25.12 2465 + '@esbuild/openbsd-arm64': 0.25.12 2466 + '@esbuild/openbsd-x64': 0.25.12 2467 + '@esbuild/openharmony-arm64': 0.25.12 2468 + '@esbuild/sunos-x64': 0.25.12 2469 + '@esbuild/win32-arm64': 0.25.12 2470 + '@esbuild/win32-ia32': 0.25.12 2471 + '@esbuild/win32-x64': 0.25.12 2227 2472 2228 - esbuild@0.24.2: 2473 + esbuild@0.27.0: 2229 2474 optionalDependencies: 2230 - '@esbuild/aix-ppc64': 0.24.2 2231 - '@esbuild/android-arm': 0.24.2 2232 - '@esbuild/android-arm64': 0.24.2 2233 - '@esbuild/android-x64': 0.24.2 2234 - '@esbuild/darwin-arm64': 0.24.2 2235 - '@esbuild/darwin-x64': 0.24.2 2236 - '@esbuild/freebsd-arm64': 0.24.2 2237 - '@esbuild/freebsd-x64': 0.24.2 2238 - '@esbuild/linux-arm': 0.24.2 2239 - '@esbuild/linux-arm64': 0.24.2 2240 - '@esbuild/linux-ia32': 0.24.2 2241 - '@esbuild/linux-loong64': 0.24.2 2242 - '@esbuild/linux-mips64el': 0.24.2 2243 - '@esbuild/linux-ppc64': 0.24.2 2244 - '@esbuild/linux-riscv64': 0.24.2 2245 - '@esbuild/linux-s390x': 0.24.2 2246 - '@esbuild/linux-x64': 0.24.2 2247 - '@esbuild/netbsd-arm64': 0.24.2 2248 - '@esbuild/netbsd-x64': 0.24.2 2249 - '@esbuild/openbsd-arm64': 0.24.2 2250 - '@esbuild/openbsd-x64': 0.24.2 2251 - '@esbuild/sunos-x64': 0.24.2 2252 - '@esbuild/win32-arm64': 0.24.2 2253 - '@esbuild/win32-ia32': 0.24.2 2254 - '@esbuild/win32-x64': 0.24.2 2475 + '@esbuild/aix-ppc64': 0.27.0 2476 + '@esbuild/android-arm': 0.27.0 2477 + '@esbuild/android-arm64': 0.27.0 2478 + '@esbuild/android-x64': 0.27.0 2479 + '@esbuild/darwin-arm64': 0.27.0 2480 + '@esbuild/darwin-x64': 0.27.0 2481 + '@esbuild/freebsd-arm64': 0.27.0 2482 + '@esbuild/freebsd-x64': 0.27.0 2483 + '@esbuild/linux-arm': 0.27.0 2484 + '@esbuild/linux-arm64': 0.27.0 2485 + '@esbuild/linux-ia32': 0.27.0 2486 + '@esbuild/linux-loong64': 0.27.0 2487 + '@esbuild/linux-mips64el': 0.27.0 2488 + '@esbuild/linux-ppc64': 0.27.0 2489 + '@esbuild/linux-riscv64': 0.27.0 2490 + '@esbuild/linux-s390x': 0.27.0 2491 + '@esbuild/linux-x64': 0.27.0 2492 + '@esbuild/netbsd-arm64': 0.27.0 2493 + '@esbuild/netbsd-x64': 0.27.0 2494 + '@esbuild/openbsd-arm64': 0.27.0 2495 + '@esbuild/openbsd-x64': 0.27.0 2496 + '@esbuild/openharmony-arm64': 0.27.0 2497 + '@esbuild/sunos-x64': 0.27.0 2498 + '@esbuild/win32-arm64': 0.27.0 2499 + '@esbuild/win32-ia32': 0.27.0 2500 + '@esbuild/win32-x64': 0.27.0 2255 2501 2256 2502 escalade@3.2.0: {} 2257 2503 2258 - escape-string-regexp@4.0.0: {} 2259 - 2260 - estree-walker@0.6.1: {} 2504 + esm-env@1.2.2: {} 2261 2505 2262 2506 exit-hook@2.2.1: {} 2263 2507 ··· 2269 2513 merge2: 1.4.1 2270 2514 micromatch: 4.0.8 2271 2515 2272 - fastq@1.18.0: 2516 + fastq@1.19.1: 2273 2517 dependencies: 2274 - reusify: 1.0.4 2518 + reusify: 1.1.0 2519 + 2520 + fdir@6.5.0(picomatch@4.0.3): 2521 + optionalDependencies: 2522 + picomatch: 4.0.3 2275 2523 2276 2524 fetch-blob@3.2.0: 2277 2525 dependencies: ··· 2283 2531 dependencies: 2284 2532 to-regex-range: 5.0.1 2285 2533 2286 - foreground-child@3.3.0: 2287 - dependencies: 2288 - cross-spawn: 7.0.6 2289 - signal-exit: 4.1.0 2290 - 2291 - fraction.js@4.3.7: {} 2534 + fraction.js@5.3.4: {} 2292 2535 2293 2536 fsevents@2.3.3: 2294 2537 optional: true ··· 2297 2540 2298 2541 gensync@1.0.0-beta.2: {} 2299 2542 2300 - get-source@2.0.12: 2301 - dependencies: 2302 - data-uri-to-buffer: 2.0.2 2303 - source-map: 0.6.1 2304 - 2305 2543 glob-parent@5.1.2: 2306 2544 dependencies: 2307 2545 is-glob: 4.0.3 ··· 2312 2550 2313 2551 glob-to-regexp@0.4.1: {} 2314 2552 2315 - glob@10.4.5: 2316 - dependencies: 2317 - foreground-child: 3.3.0 2318 - jackspeak: 3.4.3 2319 - minimatch: 9.0.5 2320 - minipass: 7.1.2 2321 - package-json-from-dist: 1.0.1 2322 - path-scurry: 1.11.1 2323 - 2324 - globals@11.12.0: {} 2325 - 2326 2553 hasown@2.0.2: 2327 2554 dependencies: 2328 2555 function-bind: 1.1.2 2329 2556 2330 2557 html-entities@2.3.3: {} 2331 2558 2559 + is-arrayish@0.3.4: {} 2560 + 2332 2561 is-binary-path@2.1.0: 2333 2562 dependencies: 2334 2563 binary-extensions: 2.3.0 ··· 2338 2567 hasown: 2.0.2 2339 2568 2340 2569 is-extglob@2.1.1: {} 2341 - 2342 - is-fullwidth-code-point@3.0.0: {} 2343 2570 2344 2571 is-glob@4.0.3: 2345 2572 dependencies: ··· 2349 2576 2350 2577 is-what@4.1.16: {} 2351 2578 2352 - isexe@2.0.0: {} 2353 - 2354 - jackspeak@3.4.3: 2355 - dependencies: 2356 - '@isaacs/cliui': 8.0.2 2357 - optionalDependencies: 2358 - '@pkgjs/parseargs': 0.11.0 2359 - 2360 2579 jiti@1.21.7: {} 2361 2580 2362 2581 js-tokens@4.0.0: {} ··· 2365 2584 2366 2585 json5@2.2.3: {} 2367 2586 2587 + kleur@4.1.5: {} 2588 + 2368 2589 lilconfig@3.1.3: {} 2369 2590 2370 2591 lines-and-columns@1.2.4: {} 2371 - 2372 - lru-cache@10.4.3: {} 2373 2592 2374 2593 lru-cache@5.1.1: 2375 2594 dependencies: 2376 2595 yallist: 3.1.1 2377 2596 2378 - magic-string@0.25.9: 2379 - dependencies: 2380 - sourcemap-codec: 1.4.8 2381 - 2382 2597 merge-anything@5.1.7: 2383 2598 dependencies: 2384 2599 is-what: 4.1.16 ··· 2394 2609 2395 2610 mini-svg-data-uri@1.4.4: {} 2396 2611 2397 - miniflare@3.20250124.0: 2612 + miniflare@4.20251202.1: 2398 2613 dependencies: 2399 2614 '@cspotcode/source-map-support': 0.8.1 2400 2615 acorn: 8.14.0 2401 - acorn-walk: 8.3.4 2402 - capnp-ts: 0.7.0 2616 + acorn-walk: 8.3.2 2403 2617 exit-hook: 2.2.1 2404 2618 glob-to-regexp: 0.4.1 2619 + sharp: 0.33.5 2405 2620 stoppable: 1.1.0 2406 - undici: 5.28.5 2407 - workerd: 1.20250124.0 2621 + undici: 7.14.0 2622 + workerd: 1.20251202.0 2408 2623 ws: 8.18.0 2409 - youch: 3.3.4 2410 - zod: 3.24.1 2624 + youch: 4.1.0-beta.10 2625 + zod: 3.22.3 2411 2626 transitivePeerDependencies: 2412 2627 - bufferutil 2413 - - supports-color 2414 2628 - utf-8-validate 2415 2629 2416 - minimatch@9.0.5: 2417 - dependencies: 2418 - brace-expansion: 2.0.1 2419 - 2420 - minipass@7.1.2: {} 2421 - 2422 - mlly@1.7.4: 2423 - dependencies: 2424 - acorn: 8.14.0 2425 - pathe: 2.0.2 2426 - pkg-types: 1.3.1 2427 - ufo: 1.5.4 2428 - 2429 2630 ms@2.1.3: {} 2430 - 2431 - mustache@4.2.0: {} 2432 2631 2433 2632 mz@2.7.0: 2434 2633 dependencies: ··· 2436 2635 object-assign: 4.1.1 2437 2636 thenify-all: 1.6.0 2438 2637 2439 - nanoid@3.3.8: {} 2638 + nanoid@3.3.11: {} 2440 2639 2441 - nanoid@5.0.9: {} 2640 + nanoid@5.1.6: {} 2442 2641 2443 2642 native-file-system-adapter@3.0.1: 2444 2643 optionalDependencies: ··· 2447 2646 node-domexception@1.0.0: 2448 2647 optional: true 2449 2648 2450 - node-releases@2.0.19: {} 2649 + node-releases@2.0.27: {} 2451 2650 2452 2651 normalize-path@3.0.0: {} 2453 2652 ··· 2457 2656 2458 2657 object-hash@3.0.0: {} 2459 2658 2460 - ohash@1.1.4: {} 2461 - 2462 - package-json-from-dist@1.0.1: {} 2463 - 2464 - parse5@7.2.1: 2659 + parse5@7.3.0: 2465 2660 dependencies: 2466 - entities: 4.5.0 2467 - 2468 - path-key@3.1.1: {} 2661 + entities: 6.0.1 2469 2662 2470 2663 path-parse@1.0.7: {} 2471 2664 2472 - path-scurry@1.11.1: 2473 - dependencies: 2474 - lru-cache: 10.4.3 2475 - minipass: 7.1.2 2476 - 2477 2665 path-to-regexp@6.3.0: {} 2478 2666 2479 - pathe@1.1.2: {} 2480 - 2481 - pathe@2.0.2: {} 2667 + pathe@2.0.3: {} 2482 2668 2483 2669 picocolors@1.1.1: {} 2484 2670 2485 2671 picomatch@2.3.1: {} 2486 2672 2487 - pify@2.3.0: {} 2673 + picomatch@4.0.3: {} 2488 2674 2489 - pirates@4.0.6: {} 2675 + pify@2.3.0: {} 2490 2676 2491 - pkg-types@1.3.1: 2492 - dependencies: 2493 - confbox: 0.1.8 2494 - mlly: 1.7.4 2495 - pathe: 2.0.2 2677 + pirates@4.0.7: {} 2496 2678 2497 - postcss-import@15.1.0(postcss@8.5.1): 2679 + postcss-import@15.1.0(postcss@8.5.6): 2498 2680 dependencies: 2499 - postcss: 8.5.1 2681 + postcss: 8.5.6 2500 2682 postcss-value-parser: 4.2.0 2501 2683 read-cache: 1.0.0 2502 - resolve: 1.22.10 2684 + resolve: 1.22.11 2503 2685 2504 - postcss-js@4.0.1(postcss@8.5.1): 2686 + postcss-js@4.1.0(postcss@8.5.6): 2505 2687 dependencies: 2506 2688 camelcase-css: 2.0.1 2507 - postcss: 8.5.1 2689 + postcss: 8.5.6 2508 2690 2509 - postcss-load-config@4.0.2(postcss@8.5.1): 2691 + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): 2510 2692 dependencies: 2511 2693 lilconfig: 3.1.3 2512 - yaml: 2.7.0 2513 2694 optionalDependencies: 2514 - postcss: 8.5.1 2695 + jiti: 1.21.7 2696 + postcss: 8.5.6 2515 2697 2516 - postcss-nested@6.2.0(postcss@8.5.1): 2698 + postcss-nested@6.2.0(postcss@8.5.6): 2517 2699 dependencies: 2518 - postcss: 8.5.1 2700 + postcss: 8.5.6 2519 2701 postcss-selector-parser: 6.1.2 2520 2702 2521 2703 postcss-selector-parser@6.1.2: ··· 2525 2707 2526 2708 postcss-value-parser@4.2.0: {} 2527 2709 2528 - postcss@8.5.1: 2710 + postcss@8.5.6: 2529 2711 dependencies: 2530 - nanoid: 3.3.8 2712 + nanoid: 3.3.11 2531 2713 picocolors: 1.1.1 2532 2714 source-map-js: 1.2.1 2533 2715 2534 - prettier-plugin-tailwindcss@0.6.11(prettier@3.4.2): 2716 + prettier-plugin-tailwindcss@0.6.14(prettier@3.7.4): 2535 2717 dependencies: 2536 - prettier: 3.4.2 2718 + prettier: 3.7.4 2537 2719 2538 - prettier@3.4.2: {} 2539 - 2540 - printable-characters@1.0.42: {} 2720 + prettier@3.7.4: {} 2541 2721 2542 2722 queue-microtask@1.2.3: {} 2543 2723 ··· 2549 2729 dependencies: 2550 2730 picomatch: 2.3.1 2551 2731 2552 - resolve@1.22.10: 2732 + resolve@1.22.11: 2553 2733 dependencies: 2554 2734 is-core-module: 2.16.1 2555 2735 path-parse: 1.0.7 2556 2736 supports-preserve-symlinks-flag: 1.0.0 2557 2737 2558 - reusify@1.0.4: {} 2738 + reusify@1.1.0: {} 2559 2739 2560 - rollup-plugin-inject@3.0.2: 2740 + rollup@4.53.3: 2561 2741 dependencies: 2562 - estree-walker: 0.6.1 2563 - magic-string: 0.25.9 2564 - rollup-pluginutils: 2.8.2 2565 - 2566 - rollup-plugin-node-polyfills@0.2.1: 2567 - dependencies: 2568 - rollup-plugin-inject: 3.0.2 2569 - 2570 - rollup-pluginutils@2.8.2: 2571 - dependencies: 2572 - estree-walker: 0.6.1 2573 - 2574 - rollup@4.32.1: 2575 - dependencies: 2576 - '@types/estree': 1.0.6 2742 + '@types/estree': 1.0.8 2577 2743 optionalDependencies: 2578 - '@rollup/rollup-android-arm-eabi': 4.32.1 2579 - '@rollup/rollup-android-arm64': 4.32.1 2580 - '@rollup/rollup-darwin-arm64': 4.32.1 2581 - '@rollup/rollup-darwin-x64': 4.32.1 2582 - '@rollup/rollup-freebsd-arm64': 4.32.1 2583 - '@rollup/rollup-freebsd-x64': 4.32.1 2584 - '@rollup/rollup-linux-arm-gnueabihf': 4.32.1 2585 - '@rollup/rollup-linux-arm-musleabihf': 4.32.1 2586 - '@rollup/rollup-linux-arm64-gnu': 4.32.1 2587 - '@rollup/rollup-linux-arm64-musl': 4.32.1 2588 - '@rollup/rollup-linux-loongarch64-gnu': 4.32.1 2589 - '@rollup/rollup-linux-powerpc64le-gnu': 4.32.1 2590 - '@rollup/rollup-linux-riscv64-gnu': 4.32.1 2591 - '@rollup/rollup-linux-s390x-gnu': 4.32.1 2592 - '@rollup/rollup-linux-x64-gnu': 4.32.1 2593 - '@rollup/rollup-linux-x64-musl': 4.32.1 2594 - '@rollup/rollup-win32-arm64-msvc': 4.32.1 2595 - '@rollup/rollup-win32-ia32-msvc': 4.32.1 2596 - '@rollup/rollup-win32-x64-msvc': 4.32.1 2744 + '@rollup/rollup-android-arm-eabi': 4.53.3 2745 + '@rollup/rollup-android-arm64': 4.53.3 2746 + '@rollup/rollup-darwin-arm64': 4.53.3 2747 + '@rollup/rollup-darwin-x64': 4.53.3 2748 + '@rollup/rollup-freebsd-arm64': 4.53.3 2749 + '@rollup/rollup-freebsd-x64': 4.53.3 2750 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 2751 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 2752 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 2753 + '@rollup/rollup-linux-arm64-musl': 4.53.3 2754 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 2755 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 2756 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 2757 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 2758 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 2759 + '@rollup/rollup-linux-x64-gnu': 4.53.3 2760 + '@rollup/rollup-linux-x64-musl': 4.53.3 2761 + '@rollup/rollup-openharmony-arm64': 4.53.3 2762 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 2763 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 2764 + '@rollup/rollup-win32-x64-gnu': 4.53.3 2765 + '@rollup/rollup-win32-x64-msvc': 4.53.3 2597 2766 fsevents: 2.3.3 2598 2767 2599 2768 run-parallel@1.2.0: ··· 2602 2771 2603 2772 semver@6.3.1: {} 2604 2773 2605 - seroval-plugins@1.2.0(seroval@1.2.0): 2774 + semver@7.7.3: {} 2775 + 2776 + seroval-plugins@1.3.3(seroval@1.3.2): 2606 2777 dependencies: 2607 - seroval: 1.2.0 2778 + seroval: 1.3.2 2608 2779 2609 - seroval@1.2.0: {} 2780 + seroval@1.3.2: {} 2610 2781 2611 - shebang-command@2.0.0: 2782 + sharp@0.33.5: 2612 2783 dependencies: 2613 - shebang-regex: 3.0.0 2614 - 2615 - shebang-regex@3.0.0: {} 2784 + color: 4.2.3 2785 + detect-libc: 2.1.2 2786 + semver: 7.7.3 2787 + optionalDependencies: 2788 + '@img/sharp-darwin-arm64': 0.33.5 2789 + '@img/sharp-darwin-x64': 0.33.5 2790 + '@img/sharp-libvips-darwin-arm64': 1.0.4 2791 + '@img/sharp-libvips-darwin-x64': 1.0.4 2792 + '@img/sharp-libvips-linux-arm': 1.0.5 2793 + '@img/sharp-libvips-linux-arm64': 1.0.4 2794 + '@img/sharp-libvips-linux-s390x': 1.0.4 2795 + '@img/sharp-libvips-linux-x64': 1.0.4 2796 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 2797 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 2798 + '@img/sharp-linux-arm': 0.33.5 2799 + '@img/sharp-linux-arm64': 0.33.5 2800 + '@img/sharp-linux-s390x': 0.33.5 2801 + '@img/sharp-linux-x64': 0.33.5 2802 + '@img/sharp-linuxmusl-arm64': 0.33.5 2803 + '@img/sharp-linuxmusl-x64': 0.33.5 2804 + '@img/sharp-wasm32': 0.33.5 2805 + '@img/sharp-win32-ia32': 0.33.5 2806 + '@img/sharp-win32-x64': 0.33.5 2616 2807 2617 - signal-exit@4.1.0: {} 2808 + simple-swizzle@0.2.4: 2809 + dependencies: 2810 + is-arrayish: 0.3.4 2618 2811 2619 - solid-js@1.9.4: 2812 + solid-js@1.9.10: 2620 2813 dependencies: 2621 - csstype: 3.1.3 2622 - seroval: 1.2.0 2623 - seroval-plugins: 1.2.0(seroval@1.2.0) 2814 + csstype: 3.2.3 2815 + seroval: 1.3.2 2816 + seroval-plugins: 1.3.3(seroval@1.3.2) 2624 2817 2625 - solid-refresh@0.6.3(solid-js@1.9.4): 2818 + solid-refresh@0.6.3(solid-js@1.9.10): 2626 2819 dependencies: 2627 - '@babel/generator': 7.26.5 2628 - '@babel/helper-module-imports': 7.25.9 2629 - '@babel/types': 7.26.7 2630 - solid-js: 1.9.4 2820 + '@babel/generator': 7.28.5 2821 + '@babel/helper-module-imports': 7.27.1 2822 + '@babel/types': 7.28.5 2823 + solid-js: 1.9.10 2631 2824 transitivePeerDependencies: 2632 2825 - supports-color 2633 2826 ··· 2639 2832 source-map: 0.6.1 2640 2833 2641 2834 source-map@0.6.1: {} 2642 - 2643 - sourcemap-codec@1.4.8: {} 2644 - 2645 - stacktracey@2.1.8: 2646 - dependencies: 2647 - as-table: 1.0.55 2648 - get-source: 2.0.12 2649 2835 2650 2836 stoppable@1.1.0: {} 2651 2837 2652 - string-width@4.2.3: 2653 - dependencies: 2654 - emoji-regex: 8.0.0 2655 - is-fullwidth-code-point: 3.0.0 2656 - strip-ansi: 6.0.1 2657 - 2658 - string-width@5.1.2: 2659 - dependencies: 2660 - eastasianwidth: 0.2.0 2661 - emoji-regex: 9.2.2 2662 - strip-ansi: 7.1.0 2663 - 2664 - strip-ansi@6.0.1: 2665 - dependencies: 2666 - ansi-regex: 5.0.1 2667 - 2668 - strip-ansi@7.1.0: 2838 + sucrase@3.35.1: 2669 2839 dependencies: 2670 - ansi-regex: 6.1.0 2671 - 2672 - sucrase@3.35.0: 2673 - dependencies: 2674 - '@jridgewell/gen-mapping': 0.3.8 2840 + '@jridgewell/gen-mapping': 0.3.13 2675 2841 commander: 4.1.1 2676 - glob: 10.4.5 2677 2842 lines-and-columns: 1.2.4 2678 2843 mz: 2.7.0 2679 - pirates: 4.0.6 2844 + pirates: 4.0.7 2845 + tinyglobby: 0.2.15 2680 2846 ts-interface-checker: 0.1.13 2681 2847 2848 + supports-color@10.2.2: {} 2849 + 2682 2850 supports-preserve-symlinks-flag@1.0.0: {} 2683 2851 2684 - tailwindcss@3.4.17: 2852 + tailwindcss@3.4.18: 2685 2853 dependencies: 2686 2854 '@alloc/quick-lru': 5.2.0 2687 2855 arg: 5.0.2 ··· 2697 2865 normalize-path: 3.0.0 2698 2866 object-hash: 3.0.0 2699 2867 picocolors: 1.1.1 2700 - postcss: 8.5.1 2701 - postcss-import: 15.1.0(postcss@8.5.1) 2702 - postcss-js: 4.0.1(postcss@8.5.1) 2703 - postcss-load-config: 4.0.2(postcss@8.5.1) 2704 - postcss-nested: 6.2.0(postcss@8.5.1) 2868 + postcss: 8.5.6 2869 + postcss-import: 15.1.0(postcss@8.5.6) 2870 + postcss-js: 4.1.0(postcss@8.5.6) 2871 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) 2872 + postcss-nested: 6.2.0(postcss@8.5.6) 2705 2873 postcss-selector-parser: 6.1.2 2706 - resolve: 1.22.10 2707 - sucrase: 3.35.0 2874 + resolve: 1.22.11 2875 + sucrase: 3.35.1 2708 2876 transitivePeerDependencies: 2709 - - ts-node 2877 + - tsx 2878 + - yaml 2710 2879 2711 - terser@5.37.0: 2880 + terser@5.44.1: 2712 2881 dependencies: 2713 - '@jridgewell/source-map': 0.3.6 2714 - acorn: 8.14.0 2882 + '@jridgewell/source-map': 0.3.11 2883 + acorn: 8.15.0 2715 2884 commander: 2.20.3 2716 2885 source-map-support: 0.5.21 2717 2886 ··· 2723 2892 dependencies: 2724 2893 any-promise: 1.3.0 2725 2894 2895 + tinyglobby@0.2.15: 2896 + dependencies: 2897 + fdir: 6.5.0(picomatch@4.0.3) 2898 + picomatch: 4.0.3 2899 + 2726 2900 to-regex-range@5.0.1: 2727 2901 dependencies: 2728 2902 is-number: 7.0.0 2729 2903 2730 2904 ts-interface-checker@0.1.13: {} 2731 2905 2732 - tslib@2.8.1: {} 2906 + tslib@2.8.1: 2907 + optional: true 2733 2908 2734 - typescript@5.7.2: {} 2735 - 2736 - ufo@1.5.4: {} 2909 + typescript@5.9.3: {} 2737 2910 2738 - undici-types@6.20.0: {} 2911 + undici-types@6.21.0: {} 2739 2912 2740 - undici@5.28.5: 2741 - dependencies: 2742 - '@fastify/busboy': 2.1.1 2913 + undici@7.14.0: {} 2743 2914 2744 - unenv@2.0.0-rc.1: 2915 + unenv@2.0.0-rc.24: 2745 2916 dependencies: 2746 - defu: 6.1.4 2747 - mlly: 1.7.4 2748 - ohash: 1.1.4 2749 - pathe: 1.1.2 2750 - ufo: 1.5.4 2917 + pathe: 2.0.3 2751 2918 2752 - update-browserslist-db@1.1.2(browserslist@4.24.4): 2919 + update-browserslist-db@1.2.2(browserslist@4.28.1): 2753 2920 dependencies: 2754 - browserslist: 4.24.4 2921 + browserslist: 4.28.1 2755 2922 escalade: 3.2.0 2756 2923 picocolors: 1.1.1 2757 2924 2758 2925 util-deprecate@1.0.2: {} 2759 2926 2760 - validate-html-nesting@1.2.2: {} 2761 - 2762 - vite-plugin-solid@2.11.0(solid-js@1.9.4)(vite@6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)): 2927 + vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1)): 2763 2928 dependencies: 2764 - '@babel/core': 7.26.7 2929 + '@babel/core': 7.28.5 2765 2930 '@types/babel__core': 7.20.5 2766 - babel-preset-solid: 1.9.3(@babel/core@7.26.7) 2931 + babel-preset-solid: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10) 2767 2932 merge-anything: 5.1.7 2768 - solid-js: 1.9.4 2769 - solid-refresh: 0.6.3(solid-js@1.9.4) 2770 - vite: 6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0) 2771 - vitefu: 1.0.5(vite@6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) 2933 + solid-js: 1.9.10 2934 + solid-refresh: 0.6.3(solid-js@1.9.10) 2935 + vite: 7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1) 2936 + vitefu: 1.1.1(vite@7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1)) 2772 2937 transitivePeerDependencies: 2773 2938 - supports-color 2774 2939 2775 - vite@6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0): 2940 + vite@7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1): 2776 2941 dependencies: 2777 - esbuild: 0.24.2 2778 - postcss: 8.5.1 2779 - rollup: 4.32.1 2942 + esbuild: 0.25.12 2943 + fdir: 6.5.0(picomatch@4.0.3) 2944 + picomatch: 4.0.3 2945 + postcss: 8.5.6 2946 + rollup: 4.53.3 2947 + tinyglobby: 0.2.15 2780 2948 optionalDependencies: 2781 - '@types/node': 22.12.0 2949 + '@types/node': 22.19.2 2782 2950 fsevents: 2.3.3 2783 2951 jiti: 1.21.7 2784 - terser: 5.37.0 2785 - yaml: 2.7.0 2952 + terser: 5.44.1 2786 2953 2787 - vitefu@1.0.5(vite@6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)): 2954 + vitefu@1.1.1(vite@7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1)): 2788 2955 optionalDependencies: 2789 - vite: 6.0.11(@types/node@22.12.0)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0) 2956 + vite: 7.2.7(@types/node@22.19.2)(jiti@1.21.7)(terser@5.44.1) 2790 2957 2791 2958 web-streams-polyfill@3.3.3: 2792 2959 optional: true 2793 2960 2794 - which@2.0.2: 2795 - dependencies: 2796 - isexe: 2.0.0 2797 - 2798 - workerd@1.20250124.0: 2961 + workerd@1.20251202.0: 2799 2962 optionalDependencies: 2800 - '@cloudflare/workerd-darwin-64': 1.20250124.0 2801 - '@cloudflare/workerd-darwin-arm64': 1.20250124.0 2802 - '@cloudflare/workerd-linux-64': 1.20250124.0 2803 - '@cloudflare/workerd-linux-arm64': 1.20250124.0 2804 - '@cloudflare/workerd-windows-64': 1.20250124.0 2963 + '@cloudflare/workerd-darwin-64': 1.20251202.0 2964 + '@cloudflare/workerd-darwin-arm64': 1.20251202.0 2965 + '@cloudflare/workerd-linux-64': 1.20251202.0 2966 + '@cloudflare/workerd-linux-arm64': 1.20251202.0 2967 + '@cloudflare/workerd-windows-64': 1.20251202.0 2805 2968 2806 - wrangler@3.106.0: 2969 + wrangler@4.53.0: 2807 2970 dependencies: 2808 - '@cloudflare/kv-asset-handler': 0.3.4 2809 - '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) 2810 - '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) 2971 + '@cloudflare/kv-asset-handler': 0.4.1 2972 + '@cloudflare/unenv-preset': 2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251202.0) 2811 2973 blake3-wasm: 2.1.5 2812 - esbuild: 0.17.19 2813 - miniflare: 3.20250124.0 2974 + esbuild: 0.27.0 2975 + miniflare: 4.20251202.1 2814 2976 path-to-regexp: 6.3.0 2815 - unenv: 2.0.0-rc.1 2816 - workerd: 1.20250124.0 2977 + unenv: 2.0.0-rc.24 2978 + workerd: 1.20251202.0 2817 2979 optionalDependencies: 2818 2980 fsevents: 2.3.3 2819 2981 transitivePeerDependencies: 2820 2982 - bufferutil 2821 - - supports-color 2822 2983 - utf-8-validate 2823 2984 2824 - wrap-ansi@7.0.0: 2825 - dependencies: 2826 - ansi-styles: 4.3.0 2827 - string-width: 4.2.3 2828 - strip-ansi: 6.0.1 2829 - 2830 - wrap-ansi@8.1.0: 2831 - dependencies: 2832 - ansi-styles: 6.2.1 2833 - string-width: 5.1.2 2834 - strip-ansi: 7.1.0 2835 - 2836 2985 ws@8.18.0: {} 2837 2986 2838 2987 yallist@3.1.1: {} 2839 2988 2840 - yaml@2.7.0: {} 2989 + youch-core@0.3.3: 2990 + dependencies: 2991 + '@poppinss/exception': 1.2.2 2992 + error-stack-parser-es: 1.0.5 2841 2993 2842 - youch@3.3.4: 2994 + youch@4.1.0-beta.10: 2843 2995 dependencies: 2844 - cookie: 0.7.2 2845 - mustache: 4.2.0 2846 - stacktracey: 2.1.8 2996 + '@poppinss/colors': 4.1.5 2997 + '@poppinss/dumper': 0.6.5 2998 + '@speed-highlight/core': 1.2.12 2999 + cookie: 1.1.1 3000 + youch-core: 0.3.3 2847 3001 2848 - zod@3.24.1: {} 3002 + zod@3.22.3: {}
+2
pnpm-workspace.yaml
··· 1 + onlyBuiltDependencies: 2 + - esbuild
+15 -47
src/api/queries/did-doc.ts
··· 1 - import { At } from '@atcute/client/lexicons'; 1 + import type { DidDocument } from '@atcute/identity'; 2 + import { 3 + CompositeDidDocumentResolver, 4 + PlcDidDocumentResolver, 5 + WebDidDocumentResolver, 6 + } from '@atcute/identity-resolver'; 7 + import type { AtprotoDid } from '@atcute/lexicons/syntax'; 2 8 3 - import { didDocument, DidDocument } from '../types/did-doc'; 4 - import { DID_PLC_RE, DID_WEB_RE } from '../utils/strings'; 9 + const didDocumentResolver = new CompositeDidDocumentResolver({ 10 + methods: { 11 + plc: new PlcDidDocumentResolver(), 12 + web: new WebDidDocumentResolver(), 13 + }, 14 + }); 5 15 6 16 export const getDidDocument = async ({ 7 17 did, 8 18 signal, 9 19 }: { 10 - did: At.DID; 20 + did: AtprotoDid; 11 21 signal?: AbortSignal; 12 22 }): Promise<DidDocument> => { 13 - const colon_index = did.indexOf(':', 4); 14 - 15 - const type = did.slice(4, colon_index); 16 - const ident = did.slice(colon_index + 1); 17 - 18 - let rawDoc: any; 19 - 20 - if (type === 'plc') { 21 - if (!DID_PLC_RE.test(did)) { 22 - throw new Error(`invalid did:plc identifier`); 23 - } 24 - 25 - const origin = import.meta.env.VITE_PLC_DIRECTORY_URL; 26 - const response = await fetch(`${origin}/${did}`, { signal }); 27 - 28 - if (response.status === 404) { 29 - throw new Error(`did not found in directory`); 30 - } else if (!response.ok) { 31 - throw new Error(`directory is unreachable`); 32 - } 33 - 34 - const json = await response.json(); 35 - 36 - rawDoc = json; 37 - } else if (type === 'web') { 38 - if (!DID_WEB_RE.test(did)) { 39 - throw new Error(`invalid did:web identifier`); 40 - } 41 - 42 - const response = await fetch(`https://${ident}/.well-known/did.json`, { signal }); 43 - 44 - if (!response.ok) { 45 - throw new Error(`did document is unreachable`); 46 - } 47 - 48 - const json = await response.json(); 49 - 50 - rawDoc = json; 51 - } else { 52 - throw new Error(`unsupported did method`); 53 - } 54 - 55 - return didDocument.parse(rawDoc, { mode: 'passthrough' }); 23 + return didDocumentResolver.resolve(did, { signal }); 56 24 };
+15 -19
src/api/queries/handle.ts
··· 1 - import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 - import { At } from '@atcute/client/lexicons'; 1 + import { XrpcHandleResolver } from '@atcute/identity-resolver'; 2 + import { type AtprotoDid, type Handle, isHandle } from '@atcute/lexicons/syntax'; 3 3 4 - import { appViewRpc } from '~/globals/rpc'; 4 + const handleResolver = new XrpcHandleResolver({ 5 + serviceUrl: import.meta.env.VITE_APPVIEW_URL, 6 + }); 5 7 6 8 export const resolveHandleViaAppView = async ({ 7 9 handle, 8 10 signal, 9 11 }: { 10 - handle: string; 12 + handle: Handle; 11 13 signal?: AbortSignal; 12 - }): Promise<At.DID> => { 13 - const { data } = await appViewRpc.get('com.atproto.identity.resolveHandle', { 14 - signal: signal, 15 - params: { handle: handle }, 16 - }); 14 + }): Promise<AtprotoDid> => { 15 + if (!isHandle(handle)) { 16 + throw new Error(`invalid handle: ${handle}`); 17 + } 17 18 18 - return data.did; 19 + return await handleResolver.resolve(handle, { signal }); 19 20 }; 20 21 21 22 export const resolveHandleViaPds = async ({ ··· 24 25 signal, 25 26 }: { 26 27 service: string; 27 - handle: string; 28 + handle: Handle; 28 29 signal?: AbortSignal; 29 - }): Promise<At.DID> => { 30 - const rpc = new XRPC({ handler: simpleFetchHandler({ service }) }); 30 + }): Promise<AtprotoDid> => { 31 + const resolver = new XrpcHandleResolver({ serviceUrl: service }); 31 32 32 - const { data } = await rpc.get('com.atproto.identity.resolveHandle', { 33 - signal, 34 - params: { handle }, 35 - }); 36 - 37 - return data.did; 33 + return await resolver.resolve(handle, { signal }); 38 34 };
+4 -5
src/api/queries/plc.ts
··· 1 - import { At } from '@atcute/client/lexicons'; 1 + import { defs } from '@atcute/did-plc'; 2 + import type { Did } from '@atcute/lexicons/syntax'; 2 3 3 - import { plcLogEntries } from '../types/plc'; 4 - 5 - export const getPlcAuditLogs = async ({ did, signal }: { did: At.DID; signal?: AbortSignal }) => { 4 + export const getPlcAuditLogs = async ({ did, signal }: { did: Did<'plc'>; signal?: AbortSignal }) => { 6 5 const origin = import.meta.env.VITE_PLC_DIRECTORY_URL; 7 6 const response = await fetch(`${origin}/${did}/log/audit`, { signal }); 8 7 if (!response.ok) { ··· 10 9 } 11 10 12 11 const json = await response.json(); 13 - return plcLogEntries.parse(json); 12 + return defs.indexedEntryLog.parse(json); 14 13 };
-86
src/api/types/did-doc.ts
··· 1 - import * as v from '@badrap/valita'; 2 - 3 - import { didString, serviceUrlString, urlString } from './strings'; 4 - 5 - const PUBLIC_KEY_MULTIBASE_RE = /^z[a-km-zA-HJ-NP-Z1-9]+$/; 6 - 7 - const verificationMethod = v.object({ 8 - id: v.string(), 9 - type: v.string(), 10 - controller: didString, 11 - publicKeyMultibase: v 12 - .string() 13 - .assert((input) => PUBLIC_KEY_MULTIBASE_RE.test(input), `must be a valid base58btc multibase key`), 14 - }); 15 - 16 - const service = v 17 - .object({ 18 - id: v.string(), 19 - type: v.string(), 20 - serviceEndpoint: v.union(urlString, v.record(urlString), v.array(urlString)), 21 - }) 22 - .chain((input) => { 23 - switch (input.type) { 24 - case 'AtprotoPersonalDataServer': 25 - case 'AtprotoLabeler': 26 - case 'BskyFeedGenerator': 27 - case 'BskyNotificationService': { 28 - const result = serviceUrlString.try(input.serviceEndpoint); 29 - if (!result.ok) { 30 - return v.err({ 31 - message: `must be a valid atproto service url`, 32 - path: ['serviceEndpoint'], 33 - }); 34 - } 35 - } 36 - } 37 - 38 - return v.ok(input); 39 - }); 40 - 41 - export const didDocument = v.object({ 42 - '@context': v.array(urlString), 43 - id: didString, 44 - alsoKnownAs: v.array(urlString).optional(() => []), 45 - verificationMethod: v.array(verificationMethod).optional(() => []), 46 - service: v.array(service).chain((input) => { 47 - for (let i = 0, len = input.length; i < len; i++) { 48 - const service = input[i]; 49 - const id = service.id; 50 - 51 - for (let j = 0; j < i; j++) { 52 - if (input[j].id === id) { 53 - return v.err({ 54 - message: `duplicate service id`, 55 - path: [i, 'id'], 56 - }); 57 - } 58 - } 59 - } 60 - 61 - return v.ok(input); 62 - }), 63 - }); 64 - 65 - export type DidDocument = v.Infer<typeof didDocument>; 66 - 67 - export const getPdsEndpoint = (doc: DidDocument): string | undefined => { 68 - return getServiceEndpoint(doc, '#atproto_pds', 'AtprotoPersonalDataServer'); 69 - }; 70 - 71 - export const getServiceEndpoint = ( 72 - doc: DidDocument, 73 - serviceId: string, 74 - serviceType: string, 75 - ): string | undefined => { 76 - const did = doc.id; 77 - 78 - const didServiceId = did + serviceId; 79 - const found = doc.service?.find((service) => service.id === serviceId || service.id === didServiceId); 80 - 81 - if (!found || found.type !== serviceType || typeof found.serviceEndpoint !== 'string') { 82 - return undefined; 83 - } 84 - 85 - return found.serviceEndpoint; 86 - };
+26 -78
src/api/types/plc.ts
··· 1 1 import * as v from '@badrap/valita'; 2 2 3 - import { didKeyString, didString, handleString, serviceUrlString, urlString } from './strings'; 4 - 5 - const legacyGenesisOp = v.object({ 6 - type: v.literal('create'), 7 - signingKey: didKeyString, 8 - recoveryKey: didKeyString, 9 - handle: handleString, 10 - service: serviceUrlString, 11 - prev: v.null(), 12 - sig: v.string(), 13 - }); 14 - 15 - const tombstoneOp = v.object({ 16 - type: v.literal('plc_tombstone'), 17 - prev: v.string(), 18 - sig: v.string(), 19 - }); 20 - 21 - const service = v.object({ 22 - type: v.string().assert((input) => input.length <= 256, `service type too long (max 256)`), 23 - endpoint: urlString.assert((input) => input.length <= 512, `service endpoint too long (max 512)`), 24 - }); 25 - export type Service = v.Infer<typeof service>; 26 - 27 - const updateOp = v.object({ 28 - type: v.literal('plc_operation'), 29 - prev: v.string().nullable(), 30 - sig: v.string(), 31 - rotationKeys: v.array(didKeyString).chain((input) => { 32 - const len = input.length; 33 - 34 - if (len === 0) { 35 - return v.err({ message: `missing rotation keys` }); 36 - } else if (len > 10) { 37 - return v.err({ message: `too many rotation keys (max 10)` }); 38 - } 39 - 40 - for (let i = 0; i < len; i++) { 41 - const key = input[i]; 42 - 43 - for (let j = 0; j < i; j++) { 44 - if (input[j] === key) { 45 - return v.err({ 46 - message: `duplicate rotation key`, 47 - path: [i], 48 - }); 49 - } 50 - } 51 - } 52 - 53 - return v.ok(input); 54 - }), 55 - verificationMethods: v.record(didKeyString), 56 - alsoKnownAs: v 57 - .array(urlString.assert((input) => input.length <= 256, `alsoKnownAs entry too long (max 256)`)) 58 - .assert((input) => input.length <= 10, `too many alsoKnownAs entries (max 10)`), 59 - services: v 60 - .record(service) 61 - .assert((input) => Object.keys(input).length <= 10, `too many service entries (max 10)`), 62 - }); 63 - export type PlcUpdateOp = v.Infer<typeof updateOp>; 3 + import { defs, type UnsignedOperation } from '@atcute/did-plc'; 64 4 65 - const plcOperation = v.union(legacyGenesisOp, tombstoneOp, updateOp); 66 - 67 - export const plcLogEntry = v.object({ 68 - did: didString, 69 - cid: v.string(), 70 - operation: plcOperation, 71 - nullified: v.boolean(), 72 - createdAt: v 73 - .string() 74 - .assert((input) => !Number.isNaN(new Date(input).getTime()), `must be a valid datetime string`), 75 - }); 76 - export type PlcLogEntry = v.Infer<typeof plcLogEntry>; 5 + import type { ToValidator } from '../utils/valita'; 6 + import { serviceUrlString } from './strings'; 77 7 78 - export const plcLogEntries = v.array(plcLogEntry); 8 + const _unsignedOperation = defs.unsignedOperation as ToValidator<UnsignedOperation>; 79 9 80 - export const updatePayload = updateOp.omit('type', 'prev', 'sig').extend({ 10 + export const updatePayload = _unsignedOperation.omit('type', 'prev').extend({ 81 11 services: v 82 12 .record( 83 - service.chain((input) => { 13 + defs.service.chain((input) => { 84 14 switch (input.type) { 85 15 case 'AtprotoPersonalDataServer': 86 16 case 'AtprotoLabeler': ··· 107 37 return v.ok(input); 108 38 }), 109 39 ) 110 - .assert((input) => Object.keys(input).length <= 10, `too many service entries (max 10)`), 40 + .chain((input) => { 41 + const length = Object.keys(input).length; 42 + 43 + if (length > 10) { 44 + return v.err(`too many service entries (max 10)`); 45 + } 46 + 47 + for (const id in input) { 48 + if (id.length > 32) { 49 + return v.err({ 50 + message: `service id too long (max 32 characters)`, 51 + path: [id], 52 + }); 53 + } 54 + } 55 + 56 + return v.ok(input); 57 + }), 111 58 }); 112 - export type PlcUpdatePayload = v.Infer<typeof updatePayload>; 59 + 60 + export type UpdatePayload = v.Infer<typeof updatePayload>;
-14
src/api/types/strings.ts
··· 1 1 import * as v from '@badrap/valita'; 2 2 3 - import { DID_KEY_RE, DID_RE, HANDLE_RE } from '../utils/strings'; 4 - 5 - export const didString = v 6 - .string() 7 - .assert((input): input is `did:${string}:${string}` => DID_RE.test(input), `must be a valid did`); 8 - 9 - export const didKeyString = v 10 - .string() 11 - .assert((input): input is `did:key:${string}` => DID_KEY_RE.test(input), `must be a valid did:key`); 12 - 13 - export const handleString = v.string().assert((input) => HANDLE_RE.test(input), `must be a valid handle`); 14 - 15 - export const urlString = v.string().assert((input) => URL.canParse(input), `must be a valid url`); 16 - 17 3 export const serviceUrlString = v.string().assert((input) => { 18 4 const url = URL.parse(input); 19 5
+7 -12
src/api/utils/error.ts
··· 1 - import { XRPCError } from '@atcute/client'; 2 - 3 - export const formatXRPCError = (err: XRPCError): string => { 4 - const name = err.kind; 5 - return (name ? name + ': ' : '') + err.message; 6 - }; 1 + import { ClientResponseError } from '@atcute/client'; 7 2 8 3 export const formatQueryError = (err: unknown) => { 9 - if (err instanceof XRPCError) { 10 - const kind = err.kind; 4 + if (err instanceof ClientResponseError) { 5 + const error = err.error; 11 6 12 - if (kind === 'InvalidToken' || kind === 'ExpiredToken') { 7 + if (error === 'InvalidToken' || error === 'ExpiredToken') { 13 8 return `Account session is no longer valid`; 14 9 } 15 10 16 - if (kind === 'UpstreamFailure') { 11 + if (error === 'UpstreamFailure') { 17 12 return `Server appears to be experiencing issues, try again later`; 18 13 } 19 14 20 - if (kind === 'InternalServerError') { 15 + if (error === 'InternalServerError') { 21 16 return `Server is having issues processing this request, try again later`; 22 17 } 23 18 24 - return formatXRPCError(err); 19 + return err.message; 25 20 } 26 21 27 22 if (err instanceof Error) {
+69
src/api/utils/jwt.ts
··· 1 + /** 2 + * Decodes a JWT token 3 + * @param token The token string 4 + * @returns JSON object from the token 5 + */ 6 + export const decodeJwt = (token: string): unknown => { 7 + const pos = 1; 8 + const part = token.split('.')[1]; 9 + 10 + let decoded: string; 11 + 12 + if (typeof part !== 'string') { 13 + throw new Error('invalid token: missing part ' + (pos + 1)); 14 + } 15 + 16 + try { 17 + decoded = base64UrlDecode(part); 18 + } catch (e) { 19 + throw new Error('invalid token: invalid b64 for part ' + (pos + 1) + ' (' + (e as Error).message + ')'); 20 + } 21 + 22 + try { 23 + return JSON.parse(decoded); 24 + } catch (e) { 25 + throw new Error('invalid token: invalid json for part ' + (pos + 1) + ' (' + (e as Error).message + ')'); 26 + } 27 + }; 28 + 29 + /** 30 + * Decodes a URL-safe Base64 string 31 + * @param str URL-safe Base64 that needed to be decoded 32 + * @returns The actual string 33 + */ 34 + const base64UrlDecode = (str: string): string => { 35 + let output = str.replace(/-/g, '+').replace(/_/g, '/'); 36 + 37 + switch (output.length % 4) { 38 + case 0: 39 + break; 40 + case 2: 41 + output += '=='; 42 + break; 43 + case 3: 44 + output += '='; 45 + break; 46 + default: 47 + throw new Error('base64 string is not of the correct length'); 48 + } 49 + 50 + try { 51 + return b64DecodeUnicode(output); 52 + } catch { 53 + return atob(output); 54 + } 55 + }; 56 + 57 + const b64DecodeUnicode = (str: string): string => { 58 + return decodeURIComponent( 59 + atob(str).replace(/(.)/g, (_m, p) => { 60 + let code = p.charCodeAt(0).toString(16).toUpperCase(); 61 + 62 + if (code.length < 2) { 63 + code = '0' + code; 64 + } 65 + 66 + return '%' + code; 67 + }), 68 + ); 69 + };
-50
src/api/utils/strings.ts
··· 1 - import type { At, Records } from '@atcute/client/lexicons'; 2 - 3 - import { assert } from '~/lib/utils/invariant'; 4 - 5 - export const ATURI_RE = 6 - /^at:\/\/(did:[a-zA-Z0-9._:%\-]+|[a-zA-Z0-9-.]+)\/([a-zA-Z0-9-.]+)\/([a-zA-Z0-9._~:@!$&%')(*+,;=\-]+)(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 7 - 8 - export const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/; 9 - 10 - export const DID_WEB_RE = /^did:web:([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/; 11 - 12 - export const DID_PLC_RE = /^did:plc:([a-z2-7]{24})$/; 13 - 14 - export const DID_KEY_RE = /^did:key:z[a-km-zA-HJ-NP-Z1-9]+$/; 15 - 16 - export const HANDLE_RE = /^[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]{2,})$/; 17 - 18 - export const DID_OR_HANDLE_RE = 19 - /^[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]{2,})$|^did:[a-z]+:[a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-]$/; 20 - 21 - export interface AtUri { 22 - repo: string; 23 - collection: string; 24 - rkey: string; 25 - fragment: string | undefined; 26 - } 27 - 28 - export const isDid = (value: string): value is At.DID => { 29 - return value.length >= 7 && DID_RE.test(value); 30 - }; 31 - 32 - export const isHandle = (value: string): boolean => { 33 - return value.length >= 4 && HANDLE_RE.test(value); 34 - }; 35 - 36 - export const parseAtUri = (str: string): AtUri => { 37 - const match = ATURI_RE.exec(str); 38 - assert(match !== null, `Failed to parse AT URI for ${str}`); 39 - 40 - return { 41 - repo: match[1], 42 - collection: match[2], 43 - rkey: match[3], 44 - fragment: match[4], 45 - }; 46 - }; 47 - 48 - export const makeAtUri = (repo: string, collection: keyof Records | (string & {}), rkey: string) => { 49 - return `at://${repo}/${collection}/${rkey}`; 50 - };
+26
src/api/utils/valita.ts
··· 1 + import * as v from '@badrap/valita'; 2 + 3 + export type ToValidator<T> = T extends readonly [infer Head, ...infer Rest] 4 + ? Rest extends [] 5 + ? v.TupleType<[ToValidator<Head>]> 6 + : v.TupleType<[ToValidator<Head>, ...ToValidatorTuple<Rest>]> 7 + : T extends ReadonlyArray<infer E> 8 + ? v.ArrayType<ToValidator<E>> 9 + : T extends object 10 + ? ToObjectValidator<T> 11 + : v.Type<T>; 12 + 13 + // Helper type for converting tuple types 14 + type ToValidatorTuple<T extends readonly unknown[]> = T extends readonly [infer Head, ...infer Rest] 15 + ? Rest extends [] 16 + ? [ToValidator<Head>] 17 + : [ToValidator<Head>, ...ToValidatorTuple<Rest>] 18 + : []; 19 + 20 + // Helper type for converting object types 21 + type ToObjectValidator<T extends object> = v.ObjectType< 22 + { 23 + [K in keyof T]-?: undefined extends T[K] ? v.Optional<Exclude<T[K], undefined>> : ToValidator<T[K]>; 24 + }, 25 + undefined 26 + >;
+72
src/components/accordion.tsx
··· 1 + import { createSignal, type JSX, Show } from 'solid-js'; 2 + 3 + import ChevronRightIcon from '~/components/ic-icons/baseline-chevron-right'; 4 + 5 + export interface AccordionProps { 6 + title: string; 7 + children: JSX.Element; 8 + defaultOpen?: boolean; 9 + } 10 + 11 + export const Accordion = (props: AccordionProps) => { 12 + const [isOpen, setIsOpen] = createSignal(props.defaultOpen ?? false); 13 + 14 + return ( 15 + <div class="border-b border-gray-200"> 16 + <button 17 + type="button" 18 + onClick={() => setIsOpen(!isOpen())} 19 + class="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-gray-50" 20 + > 21 + <ChevronRightIcon 22 + class={`h-5 w-5 text-gray-500 transition-transform` + (isOpen() ? ` rotate-90` : ``)} 23 + /> 24 + <span class="font-semibold">{props.title}</span> 25 + </button> 26 + 27 + <Show when={isOpen()}> 28 + <div class="pb-4 pl-12 pr-4">{props.children}</div> 29 + </Show> 30 + </div> 31 + ); 32 + }; 33 + 34 + export interface SubsectionProps { 35 + title: string; 36 + children: JSX.Element; 37 + } 38 + 39 + export const Subsection = (props: SubsectionProps) => { 40 + return ( 41 + <div class="mb-4 last:mb-0"> 42 + <h4 class="mb-3 text-sm font-semibold text-gray-600">{props.title}</h4> 43 + <div class="flex flex-col gap-3">{props.children}</div> 44 + </div> 45 + ); 46 + }; 47 + 48 + export interface StatusBadgeProps { 49 + variant: 'idle' | 'pending' | 'success' | 'error'; 50 + children: JSX.Element; 51 + } 52 + 53 + export const StatusBadge = (props: StatusBadgeProps) => { 54 + const variantStyles = () => { 55 + switch (props.variant) { 56 + case 'idle': 57 + return 'bg-gray-100 text-gray-600'; 58 + case 'pending': 59 + return 'bg-yellow-100 text-yellow-800'; 60 + case 'success': 61 + return 'bg-green-100 text-green-800'; 62 + case 'error': 63 + return 'bg-red-100 text-red-800'; 64 + } 65 + }; 66 + 67 + return ( 68 + <span class={`inline-flex items-center rounded px-2 py-0.5 text-xs font-medium ${variantStyles()}`}> 69 + {props.children} 70 + </span> 71 + ); 72 + };
+65
src/components/file-drop-zone.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + import { createDropZone, type CreateDropZoneOptions } from '~/lib/hooks/dropzone'; 4 + 5 + import Button from './inputs/button'; 6 + 7 + interface FileDropZoneProps { 8 + accept?: string; 9 + disabled?: boolean; 10 + onFiles: (files: File[]) => void; 11 + dataTypes?: CreateDropZoneOptions['dataTypes']; 12 + multiple?: boolean; 13 + children?: JSX.Element; 14 + } 15 + 16 + const FileDropZone = (props: FileDropZoneProps) => { 17 + const { ref: dropRef, isDropping } = createDropZone({ 18 + dataTypes: props.dataTypes, 19 + multiple: props.multiple ?? false, 20 + onDrop(files) { 21 + if (files) { 22 + props.onFiles(files); 23 + } 24 + }, 25 + }); 26 + 27 + const handleBrowse = () => { 28 + const input = document.createElement('input'); 29 + input.type = 'file'; 30 + if (props.accept) { 31 + input.accept = props.accept; 32 + } 33 + if (props.multiple) { 34 + input.multiple = true; 35 + } 36 + input.oninput = () => { 37 + const files = Array.from(input.files!); 38 + if (files.length > 0) { 39 + props.onFiles(files); 40 + } 41 + }; 42 + input.click(); 43 + }; 44 + 45 + return ( 46 + <fieldset 47 + ref={dropRef} 48 + disabled={props.disabled} 49 + class={ 50 + `relative grid place-items-center rounded border border-gray-300 px-6 py-12 disabled:opacity-50` + 51 + (props.disabled || !isDropping() ? ` bg-gray-100` : ` bg-green-100`) 52 + } 53 + > 54 + <div class="flex flex-col items-center gap-4"> 55 + <Button variant="outline" onClick={handleBrowse}> 56 + Browse files 57 + </Button> 58 + <p class="select-none font-medium text-gray-600">or drop your file here</p> 59 + </div> 60 + {props.children} 61 + </fieldset> 62 + ); 63 + }; 64 + 65 + export default FileDropZone;
+9
src/components/ic-icons/baseline-chevron-right.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const ChevronRightIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" viewBox="0 0 24 24"> 5 + <path fill="currentColor" d="M10 6L8.59 7.41L13.17 12l-4.58 4.59L10 18l6-6z" /> 6 + </svg> 7 + )); 8 + 9 + export default ChevronRightIcon;
+12
src/components/ic-icons/baseline-close.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const CloseIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" viewBox="0 0 24 24"> 5 + <path 6 + fill="currentColor" 7 + d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" 8 + /> 9 + </svg> 10 + )); 11 + 12 + export default CloseIcon;
+27 -12
src/components/inputs/button.tsx
··· 1 - import { JSX } from 'solid-js'; 1 + import { createMemo, type JSX } from 'solid-js'; 2 2 3 3 interface ButtonProps { 4 4 children?: JSX.Element; 5 5 disabled?: boolean; 6 - variant?: 'primary' | 'secondary'; 6 + variant?: 'primary' | 'secondary' | 'outline'; 7 7 type?: 'button' | 'submit'; 8 + href?: string; 8 9 onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 9 10 } 10 11 ··· 15 16 cn += ` bg-purple-800 text-white hover:bg-purple-700 active:bg-purple-700`; 16 17 } else if (variant === 'secondary') { 17 18 cn += ` bg-gray-200 text-black hover:bg-gray-300 active:bg-gray-300`; 19 + } else if (variant === 'outline') { 20 + cn += ` border border-gray-300 text-gray-800 hover:bg-gray-100 active:bg-gray-100`; 18 21 } 19 22 20 23 if (disabled) { ··· 25 28 }; 26 29 27 30 const Button = (props: ButtonProps) => { 28 - return ( 29 - <button 30 - type={props.type ?? 'button'} 31 - disabled={props.disabled} 32 - class={buttonStyles(props)} 33 - onClick={props.onClick} 34 - > 35 - {props.children} 36 - </button> 37 - ); 31 + const hasLink = createMemo(() => props.href !== undefined); 32 + 33 + return (() => { 34 + if (hasLink()) { 35 + return ( 36 + <a href={!props.disabled ? props.href : undefined} class={buttonStyles(props)}> 37 + {props.children} 38 + </a> 39 + ); 40 + } 41 + 42 + return ( 43 + <button 44 + type={props.type ?? 'button'} 45 + disabled={props.disabled} 46 + class={buttonStyles(props)} 47 + onClick={props.onClick} 48 + > 49 + {props.children} 50 + </button> 51 + ); 52 + }) as unknown as JSX.Element; 38 53 }; 39 54 40 55 export default Button;
+2 -3
src/components/inputs/multiline-input.tsx
··· 1 - import { createEffect, JSX } from 'solid-js'; 1 + import { createEffect, type JSX } from 'solid-js'; 2 2 3 3 import { createId } from '~/lib/hooks/id'; 4 4 5 - import { BoundInputEvent } from './_types'; 5 + import type { BoundInputEvent } from './_types'; 6 6 7 7 interface MultilineInputProps { 8 8 label: JSX.Element; ··· 39 39 name={props.name} 40 40 required={props.required} 41 41 autocomplete={props.autocomplete} 42 - // @ts-expect-error 43 42 autocorrect={props.autocorrect} 44 43 rows={22} 45 44 value={props.value}
+6 -6
src/components/inputs/radio-input.tsx
··· 1 - import { JSX } from 'solid-js'; 1 + import type { JSX } from 'solid-js'; 2 2 3 3 import { createId } from '~/lib/hooks/id'; 4 4 5 - import { BoundInputEvent } from './_types'; 5 + import type { BoundInputEvent } from './_types'; 6 6 7 7 interface RadioInputProps<T extends string> { 8 8 label: JSX.Element; ··· 10 10 name?: string; 11 11 required?: boolean; 12 12 value?: T; 13 - options: { value: NoInfer<T>; label: string }[]; 13 + options: { value: NoInfer<T>; label: string; disabled?: boolean }[]; 14 14 onChange?: (next: NoInfer<T>, event: BoundInputEvent<HTMLInputElement>) => void; 15 15 } 16 16 ··· 26 26 <span class="font-semibold text-gray-600">{props.label}</span> 27 27 </legend> 28 28 29 - {props.options.map(({ value, label }, idx) => { 29 + {props.options.map(({ value, label, disabled }, idx) => { 30 30 const optionId = fieldId + idx; 31 31 32 32 return ( 33 - <span class="flex items-center gap-3"> 33 + <fieldset disabled={disabled} class="flex items-center gap-3 disabled:opacity-50"> 34 34 <input 35 35 type="radio" 36 36 id={optionId} ··· 45 45 <label for={optionId} class="text-sm"> 46 46 {label} 47 47 </label> 48 - </span> 48 + </fieldset> 49 49 ); 50 50 })} 51 51
+2 -2
src/components/inputs/select-input.tsx
··· 1 - import { createEffect, JSX } from 'solid-js'; 1 + import { createEffect, type JSX } from 'solid-js'; 2 2 3 3 import { createId } from '~/lib/hooks/id'; 4 4 5 - import { BoundInputEvent } from './_types'; 5 + import type { BoundInputEvent } from './_types'; 6 6 7 7 interface SelectInputProps<T extends string> { 8 8 label: JSX.Element;
+5 -3
src/components/inputs/text-input.tsx
··· 1 - import { createEffect, JSX } from 'solid-js'; 1 + import { createEffect, type JSX } from 'solid-js'; 2 2 3 3 import { createId } from '~/lib/hooks/id'; 4 4 5 - import { BoundInputEvent } from './_types'; 5 + import type { BoundInputEvent } from './_types'; 6 6 7 - interface TextInputProps { 7 + export interface TextInputProps { 8 8 label: JSX.Element; 9 9 blurb?: JSX.Element; 10 10 monospace?: boolean; 11 11 type?: 'text' | 'password' | 'url' | 'email'; 12 12 name?: string; 13 13 required?: boolean; 14 + disabled?: boolean; 14 15 autocomplete?: 'off' | 'on' | 'one-time-code' | 'username'; 15 16 autocorrect?: 'off' | 'on'; 16 17 pattern?: string; ··· 55 56 id={fieldId} 56 57 name={props.name} 57 58 required={props.required} 59 + disabled={props.disabled} 58 60 autocomplete={props.autocomplete} 59 61 pattern={props.pattern} 60 62 placeholder={props.placeholder}
+1 -1
src/components/inputs/toggle-input.tsx
··· 2 2 3 3 import { createId } from '~/lib/hooks/id'; 4 4 5 - import { BoundInputEvent } from './_types'; 5 + import type { BoundInputEvent } from './_types'; 6 6 7 7 export interface ToggleInputProps { 8 8 label: string;
+22
src/components/page-header.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + interface PageHeaderProps { 4 + title: string; 5 + subtitle?: string; 6 + children?: JSX.Element; 7 + } 8 + 9 + const PageHeader = (props: PageHeaderProps) => { 10 + return ( 11 + <> 12 + <div class="p-4"> 13 + <h1 class="text-lg font-bold text-purple-800">{props.title}</h1> 14 + {props.subtitle && <p class="text-gray-600">{props.subtitle}</p>} 15 + {props.children} 16 + </div> 17 + <hr class="mx-4 border-gray-300" /> 18 + </> 19 + ); 20 + }; 21 + 22 + export default PageHeader;
+1 -1
src/components/wizard.tsx
··· 1 - import { Component, createMemo, createSignal, For, JSX } from 'solid-js'; 1 + import { type Component, createMemo, createSignal, For, type JSX } from 'solid-js'; 2 2 3 3 type EmptyObjectKeys<T> = { 4 4 [K in keyof T]: T[K] extends Record<string, never> ? K : never;
+18 -17
src/components/wizards/bluesky-login-step.tsx
··· 1 1 import { batch, createSignal, Match, Show, Switch } from 'solid-js'; 2 2 3 - import { CredentialManager, XRPCError } from '@atcute/client'; 4 - import { At } from '@atcute/client/lexicons'; 3 + import { ClientResponseError, CredentialManager } from '@atcute/client'; 4 + import { getPdsEndpoint, isAtprotoDid, type DidDocument } from '@atcute/identity'; 5 + import { isHandle, type AtprotoDid } from '@atcute/lexicons/syntax'; 5 6 6 7 import { getDidDocument } from '~/api/queries/did-doc'; 7 8 import { resolveHandleViaAppView } from '~/api/queries/handle'; 8 - import { DidDocument, getPdsEndpoint } from '~/api/types/did-doc'; 9 9 import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 10 - import { isDid } from '~/api/utils/strings'; 11 10 12 11 import { createMutation } from '~/lib/utils/mutation'; 13 12 ··· 54 53 service = service?.trim() || undefined; 55 54 56 55 if (service === undefined) { 57 - let did: At.DID; 58 - if (!isDid(identifier)) { 56 + let did: AtprotoDid; 57 + if (isAtprotoDid(identifier)) { 58 + did = identifier; 59 + } else if (isHandle(identifier)) { 59 60 did = await resolveHandleViaAppView({ handle: identifier }); 60 61 } else { 61 - did = identifier; 62 + throw new InsufficientLoginError(`Invalid identifier`); 62 63 } 63 64 64 65 const didDoc = await getDidDocument({ did }); ··· 88 89 setIsTotpRequired(false); 89 90 }); 90 91 }, 91 - onError(error) { 92 + onError(err) { 92 93 let message: string | undefined; 93 94 94 - if (error instanceof XRPCError) { 95 - if (error.kind === 'AuthFactorTokenRequired') { 95 + if (err instanceof ClientResponseError) { 96 + if (err.error === 'AuthFactorTokenRequired') { 96 97 setOtp(''); 97 98 setIsTotpRequired(true); 98 99 return; 99 100 } 100 101 101 - if (error.kind === 'AuthenticationRequired') { 102 + if (err.error === 'AuthenticationRequired') { 102 103 message = `Invalid identifier or password`; 103 - } else if (error.kind === 'AccountTakedown') { 104 + } else if (err.error === 'AccountTakedown') { 104 105 message = `Account has been taken down`; 105 - } else if (error.message.includes('Token is invalid')) { 106 + } else if (err.message.includes('Token is invalid')) { 106 107 message = `Invalid one-time confirmation code`; 107 108 setIsTotpRequired(true); 108 109 } 109 - } else if (error instanceof InsufficientLoginError) { 110 - message = error.message; 110 + } else if (err instanceof InsufficientLoginError) { 111 + message = err.message; 111 112 } 112 113 113 114 if (message !== undefined) { 114 115 setError(message); 115 116 } else { 116 - console.error(error); 117 - setError(`Something went wrong: ${error}`); 117 + console.error(err); 118 + setError(`Something went wrong: ${err}`); 118 119 } 119 120 }, 120 121 });
+2 -2
src/globals/rpc.ts
··· 1 - import { simpleFetchHandler, XRPC } from '@atcute/client'; 1 + import { Client, simpleFetchHandler } from '@atcute/client'; 2 2 3 3 const APPVIEW_URL = import.meta.env.VITE_APPVIEW_URL; 4 4 5 - export const appViewRpc = new XRPC({ handler: simpleFetchHandler({ service: APPVIEW_URL }) }); 5 + export const appViewRpc = new Client({ handler: simpleFetchHandler({ service: APPVIEW_URL }) });
+17 -44
src/lib/navigation/history.ts
··· 3 3 // Commit: 3e9dab413f4eda8d6bce565388c5ddb7aeff9f7e 4 4 // Most of the changes are just trimming it down to only include the browser 5 5 // history implementation. 6 + import { nanoid } from 'nanoid/non-secure'; 7 + 8 + import { EventEmitter } from '@mary/events'; 6 9 7 10 export type Action = 'traverse' | 'push' | 'replace' | 'update'; 8 11 ··· 111 114 let blockedPopTx: Transition | null = null; 112 115 const handlePop = () => { 113 116 if (blockedPopTx) { 114 - blockers.call(blockedPopTx); 117 + emitter.emit('block', blockedPopTx); 115 118 blockedPopTx = null; 116 119 } else { 117 120 const nextAction: Action = 'traverse'; 118 121 const nextLocation = getCurrentLocation(); 119 122 const nextIndex = nextLocation.index; 120 123 121 - if (blockers.length) { 124 + if (emitter.has('block')) { 122 125 if (nextIndex != null) { 123 126 const delta = location.index - nextIndex; 124 127 if (delta) { ··· 154 157 } 155 158 }; 156 159 157 - const listeners = createEvents<Listener>(); 158 - const blockers = createEvents<Blocker>(); 160 + const emitter = new EventEmitter<{ 161 + update: [evt: Update]; 162 + block: [tx: Transition]; 163 + }>(); 159 164 160 165 let location = getCurrentLocation(); 161 166 ··· 194 199 }; 195 200 196 201 const allowTx = (action: Action, location: Location, retry: () => void): boolean => { 197 - return !blockers.length || (blockers.call({ action, location, retry }), false); 202 + return !emitter.emit('block', { action, location, retry }); 198 203 }; 199 204 200 205 const applyTx = (nextAction: Action): void => { 201 206 location = getCurrentLocation(); 202 - listeners.call({ action: nextAction, location }); 207 + emitter.emit('update', { action: nextAction, location }); 203 208 }; 204 209 205 210 const navigate = (to: To, { replace, state }: NavigateOptions = {}): void => { ··· 263 268 return go(1); 264 269 }, 265 270 listen: (listener) => { 266 - return listeners.push(listener); 271 + return emitter.on('update', listener); 267 272 }, 268 273 block: (blocker) => { 269 - const unblock = blockers.push(blocker); 270 - 271 - if (blockers.length === 1) { 274 + if (!emitter.has('block')) { 272 275 window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); 273 276 } 274 277 278 + const unblock = emitter.on('block', blocker); 279 + 275 280 return () => { 276 281 unblock(); 277 282 278 283 // Remove the beforeunload listener so the document may 279 284 // still be salvageable in the pagehide event. 280 285 // See https://html.spec.whatwg.org/#unloading-documents 281 - if (!blockers.length) { 286 + if (!emitter.has('block')) { 282 287 window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); 283 288 } 284 289 }; ··· 293 298 event.preventDefault(); 294 299 }; 295 300 296 - interface Events<F extends (arg: any) => void> { 297 - length: number; 298 - push: (fn: F) => () => void; 299 - call: (arg: Parameters<F>[0]) => void; 300 - } 301 - 302 - const createEvents = <F extends (arg: any) => void>(): Events<F> => { 303 - const handlers: F[] = []; 304 - 305 - return { 306 - get length() { 307 - return handlers.length; 308 - }, 309 - push(fn: F) { 310 - handlers.push(fn); 311 - 312 - return () => { 313 - const index = handlers.indexOf(fn); 314 - 315 - if (index !== -1) { 316 - handlers.splice(index, 1); 317 - } 318 - }; 319 - }, 320 - call(arg) { 321 - for (let idx = 0, len = handlers.length; idx < len; idx++) { 322 - (0, handlers[idx])(arg); 323 - } 324 - }, 325 - }; 326 - }; 327 - 328 301 const createKey = () => { 329 - return crypto.randomUUID(); 302 + return nanoid(); 330 303 }; 331 304 332 305 /**
+56 -41
src/lib/navigation/router.tsx
··· 1 1 /* @refresh reload */ 2 2 import { 3 + type Accessor, 3 4 type Component, 4 5 For, 5 6 type JSX, ··· 11 12 createSignal, 12 13 getOwner, 13 14 onCleanup, 15 + untrack, 14 16 useContext, 15 17 } from 'solid-js'; 16 18 import { delegateEvents } from 'solid-js/web'; ··· 57 59 58 60 export interface MatchedRouteState extends MatchedRoute { 59 61 readonly id: string; 62 + scrollPos: { x: number; y: number } | undefined; 60 63 } 61 64 62 65 interface RouterState { ··· 68 71 interface ViewContextObject { 69 72 owner: Owner | null; 70 73 route: MatchedRouteState; 74 + isActive: () => boolean; 71 75 } 72 76 73 77 let _entry: Location; ··· 86 90 enter: boolean; 87 91 } 88 92 89 - const routerEvents = new EventEmitter<{ [key: string]: (event: RouteEvent) => void }>(); 93 + const routerEvents = new EventEmitter<{ [key: string]: [event: RouteEvent] }>(); 90 94 91 95 export { routerEvents as UNSAFE_routerEvents }; 92 96 ··· 105 109 const nextKey = matched.id || _entry.key; 106 110 107 111 const isSingle = !!matched.id; 108 - const matchedState: MatchedRouteState = { ...matched, id: nextKey }; 112 + const matchedState: MatchedRouteState = { 113 + ...matched, 114 + id: nextKey, 115 + scrollPos: undefined, 116 + }; 109 117 110 118 const next: Record<string, MatchedRouteState> = { [nextKey]: matchedState }; 111 119 ··· 118 126 } 119 127 120 128 _cleanup = createRoot((cleanup) => { 121 - createEventListener; 122 - 123 129 onCleanup( 124 130 history.listen(({ action, location: nextEntry }) => { 125 131 const currentEntry = _entry; ··· 127 133 128 134 if (action !== 'update') { 129 135 const pathname = nextEntry.pathname; 130 - let matched = matchRoute(pathname); 136 + const matched = matchRoute(pathname); 131 137 132 138 if (!matched) { 133 139 return; ··· 139 145 let singles = current.singles; 140 146 let isNew = false; 141 147 148 + const prevId = current.active; 149 + 142 150 const nextId = matched.id || nextEntry.key; 143 - const matchedState: MatchedRouteState = { ...matched, id: nextId }; 151 + const matchedState: MatchedRouteState = { 152 + ...matched, 153 + id: nextId, 154 + scrollPos: undefined, 155 + }; 144 156 145 157 let nextViews: typeof views | undefined; 146 158 ··· 163 175 } 164 176 165 177 if (!matched.id) { 166 - // Add this view, if it's already present, set `shouldCall` to true 167 178 if (!(nextId in views)) { 168 179 if (nextViews) { 169 180 nextViews[nextId] = matchedState; 181 + isNew = true; 170 182 } else { 171 183 nextViews = { ...views, [nextId]: matchedState }; 172 184 isNew = true; 173 185 } 174 186 } 175 187 } else { 176 - // Add this view, if it's already present, set `shouldCall` to true 177 188 if (!(nextId in singles)) { 178 189 singles = { ...singles, [nextId]: matchedState }; 179 190 isNew = true; ··· 184 195 views = nextViews; 185 196 } 186 197 187 - routerEvents.emit(current.active, { focus: false, enter: false }); 198 + { 199 + const prev = current.views[prevId] || current.singles[prevId]; 200 + if (prev) { 201 + prev.scrollPos = { x: window.scrollX, y: window.scrollY }; 202 + } 203 + } 204 + 205 + routerEvents.emit(prevId, { focus: false, enter: false }); 206 + 188 207 setState({ active: nextId, views: views, singles: singles }); 189 208 190 - if (!isNew) { 209 + if (isNew) { 210 + // Scroll to top if we're pushing or replacing, it's a new page. 211 + window.scrollTo(0, 0); 212 + } else { 213 + { 214 + const next = views[nextId] || singles[nextId]; 215 + if (next?.scrollPos) { 216 + const pos = next.scrollPos; 217 + window.scrollTo(pos.x, pos.y); 218 + } 219 + } 220 + 191 221 routerEvents.emit(nextId, { 192 222 focus: true, 193 223 enter: action !== 'traverse' || nextEntry.index > currentEntry.index, 194 224 }); 195 - } 196 - 197 - // Scroll to top if we're pushing or replacing, it's a new page. 198 - if (!matched.id && (action === 'push' || action === 'replace')) { 199 - window.scrollTo({ top: 0, behavior: 'instant' }); 200 225 } 201 226 } 202 227 }), ··· 235 260 } 236 261 237 262 evt.preventDefault(); 238 - 239 - if (location.pathname !== pathname || location.search !== search || location.hash !== hash) { 240 - history.navigate({ pathname, search, hash }); 241 - } 263 + history.navigate({ pathname, search, hash }); 242 264 }); 243 265 244 266 return cleanup; ··· 273 295 }; 274 296 275 297 export const onRouteEnter = (cb: () => void) => { 276 - const { route } = useViewContext(); 298 + const { route, isActive } = useViewContext(); 277 299 278 - cb(); 300 + if (untrack(isActive)) { 301 + cb(); 302 + } 303 + 279 304 onCleanup(routerEvents.on(route.id, (e) => e.enter && cb())); 305 + }; 306 + 307 + export const useIsFocused = (): Accessor<boolean> => { 308 + const { isActive } = useViewContext(); 309 + 310 + return isActive; 280 311 }; 281 312 282 313 export const createFocusEffect = (cb: () => void) => { 283 - const { route } = useViewContext(); 284 - const [active, setActive] = createSignal(true); 314 + const isFocused = useIsFocused(); 285 315 286 - onCleanup(routerEvents.on(route.id, (e) => setActive(e.focus))); 287 316 createEffect(() => { 288 - if (active()) { 289 - cb(); 317 + if (isFocused()) { 318 + createEffect(cb); 290 319 } 291 320 }); 292 321 }; ··· 305 334 const render = props.render; 306 335 307 336 const renderView = (matched: MatchedRouteState) => { 308 - const def = matched.def; 309 337 const id = matched.id; 310 338 311 339 const active = createMemo((): boolean => state().active === id); ··· 313 341 const context: ViewContextObject = { 314 342 owner: getOwner(), 315 343 route: matched, 344 + isActive: active, 316 345 }; 317 - 318 - if (def.single) { 319 - let storedHeight: number | undefined; 320 - 321 - onCleanup( 322 - routerEvents.on(id, (ev) => { 323 - if (!ev.focus) { 324 - storedHeight = document.documentElement.scrollTop; 325 - } else if (storedHeight !== undefined) { 326 - window.scrollTo({ top: storedHeight, behavior: 'instant' }); 327 - } 328 - }), 329 - ); 330 - } 331 346 332 347 return ( 333 348 <Freeze freeze={!active()}>
+104 -4
src/lib/utils/confirmation-code.ts
··· 1 - import { customAlphabet } from 'nanoid'; 1 + import { sample } from '@mary/array-fns'; 2 2 3 - const generateCode = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 10); 3 + const words = [ 4 + 'abroad', 5 + 'acorn', 6 + 'anaconda', 7 + 'anchovy', 8 + 'aorta', 9 + 'argue', 10 + 'ashy', 11 + 'astound', 12 + 'attest', 13 + 'babied', 14 + 'bobcat', 15 + 'bondless', 16 + 'bullion', 17 + 'bunny', 18 + 'celtic', 19 + 'chivalry', 20 + 'circling', 21 + 'civic', 22 + 'clobber', 23 + 'conform', 24 + 'cosmic', 25 + 'crier', 26 + 'curtly', 27 + 'depose', 28 + 'diagnosis', 29 + 'disfigure', 30 + 'drank', 31 + 'ducktail', 32 + 'eel', 33 + 'effort', 34 + 'equipment', 35 + 'eternal', 36 + 'exemplify', 37 + 'filtrate', 38 + 'fit', 39 + 'flaccid', 40 + 'fool', 41 + 'germinate', 42 + 'glade', 43 + 'graveness', 44 + 'gray', 45 + 'hydrant', 46 + 'italicize', 47 + 'landowner', 48 + 'lavender', 49 + 'mandatory', 50 + 'molecule', 51 + 'multitude', 52 + 'music', 53 + 'national', 54 + 'neatly', 55 + 'omnivore', 56 + 'other', 57 + 'overdrive', 58 + 'overhang', 59 + 'overlying', 60 + 'padded', 61 + 'pang', 62 + 'paralyses', 63 + 'partner', 64 + 'pedometer', 65 + 'plaything', 66 + 'pointy', 67 + 'prescribe', 68 + 'pueblo', 69 + 'pursuant', 70 + 'reprise', 71 + 'resilient', 72 + 'reusable', 73 + 'roster', 74 + 'scenic', 75 + 'selected', 76 + 'singer', 77 + 'slacker', 78 + 'smirk', 79 + 'smoked', 80 + 'smugly', 81 + 'startle', 82 + 'sternum', 83 + 'strut', 84 + 'subsystem', 85 + 'supper', 86 + 'swifter', 87 + 'tacking', 88 + 'traffic', 89 + 'tragedy', 90 + 'trapper', 91 + 'tummy', 92 + 'twiddle', 93 + 'unglazed', 94 + 'ungloved', 95 + 'unicorn', 96 + 'unissued', 97 + 'unmovable', 98 + 'unwary', 99 + 'uselessly', 100 + 'venus', 101 + 'vertebrae', 102 + 'wildly', 103 + 'wrecker', 104 + ]; 4 105 5 106 export const generateConfirmationCode = () => { 6 - const code = generateCode(); 7 - return `${code.slice(0, 5)}-${code.slice(5, 10)}`; 107 + return sample(words, 3).join(' '); 8 108 };
+7
src/lib/utils/mutation.ts
··· 49 49 ) & { 50 50 mutate(variables: V): void; 51 51 mutateAsync(variables: V): Promise<D>; 52 + reset(): void; 52 53 }; 53 54 54 55 type MutationFunction<D = unknown, V = unknown> = (variables: V, signal: AbortSignal) => Promise<D>; ··· 74 75 { s: MutationState.IDLE }, 75 76 { equals: (prev, next) => prev.s === next.s }, 76 77 ); 78 + 79 + const reset = () => { 80 + cleanup(); 81 + setState({ s: MutationState.IDLE }); 82 + }; 77 83 78 84 const mutate = async (variables: V): Promise<D> => { 79 85 const signal = getSignal(); ··· 139 145 }, 140 146 mutateAsync: mutate, 141 147 mutate: (variables: V) => mutate(variables).then(noop, noop), 148 + reset: reset, 142 149 } as any; 143 150 }; 144 151
+18 -6
src/lib/utils/promise-queue.ts
··· 1 + import Queue from '@mary/ds-queue'; 2 + 3 + interface QueueTask { 4 + deferred: PromiseWithResolvers<any>; 5 + fn: () => any; 6 + } 7 + 1 8 export class PromiseQueue { 2 - #queue: { deferred: PromiseWithResolvers<any>; fn: () => any }[] = []; 9 + #queue = new Queue<QueueTask>(); 3 10 4 11 #max: number; 5 12 #current = 0; ··· 11 18 add<T>(fn: () => Promise<T>): Promise<T> { 12 19 const deferred = Promise.withResolvers<T>(); 13 20 14 - this.#queue.push({ deferred, fn }); 21 + this.#queue.enqueue({ deferred, fn }); 15 22 this.#run(); 16 23 17 24 return deferred.promise; 18 25 } 19 26 20 27 async flush(): Promise<void> { 21 - while (this.#queue.length > 0) { 22 - await Promise.all(this.#queue.map((task) => task.deferred.promise)); 28 + while (this.#queue.size > 0) { 29 + // type assertion here because JSR omits the [Symbol.iterator] method declaration 30 + await Promise.all( 31 + Array.from(this.#queue as any as Iterable<QueueTask>, (task) => task.deferred.promise), 32 + ); 23 33 } 24 34 } 25 35 26 36 #run() { 27 - if (this.#queue.length > 0 && this.#current <= this.#max) { 28 - const { deferred, fn } = this.#queue.shift()!; 37 + let task: QueueTask | undefined; 38 + 39 + if (this.#current < this.#max && (task = this.#queue.dequeue()) !== undefined) { 40 + const { deferred, fn } = task; 29 41 this.#current++; 30 42 31 43 const promise = new Promise((r) => r(fn()));
+6 -7
src/lib/utils/search-params.ts
··· 1 1 import { batch, createSignal } from 'solid-js'; 2 2 3 - import { At } from '@atcute/client/lexicons'; 3 + import { isDid, isHandle } from '@atcute/lexicons/syntax'; 4 4 5 - import { DID_OR_HANDLE_RE, DID_RE, HANDLE_RE } from '~/api/utils/strings'; 6 - import { UnwrapArray } from '~/api/utils/types'; 5 + import type { UnwrapArray } from '~/api/utils/types'; 7 6 8 7 export interface ParamParser<T> { 9 8 parse: (value: string | string[] | null) => T | null; ··· 223 222 224 223 export const asDID = createParser({ 225 224 parse(value) { 226 - if (typeof value === 'string' && DID_RE.test(value)) { 227 - return value as At.DID; 225 + if (isDid(value)) { 226 + return value; 228 227 } 229 228 230 229 return null; ··· 236 235 237 236 export const asHandle = createParser({ 238 237 parse(value) { 239 - if (typeof value === 'string' && HANDLE_RE.test(value)) { 238 + if (isHandle(value)) { 240 239 return value; 241 240 } 242 241 ··· 249 248 250 249 export const asIdentifier = createParser({ 251 250 parse(value) { 252 - if (typeof value === 'string' && DID_OR_HANDLE_RE.test(value)) { 251 + if (typeof value === 'string' && (isDid(value) || isHandle(value))) { 253 252 return value; 254 253 } 255 254
+17
src/lib/utils/stream.ts
··· 1 + export async function* iterateStream<T>(stream: ReadableStream<T>) { 2 + const reader = stream.getReader(); 3 + 4 + try { 5 + while (true) { 6 + const { done, value } = await reader.read(); 7 + 8 + if (done) { 9 + return; 10 + } 11 + 12 + yield value; 13 + } 14 + } finally { 15 + reader.releaseLock(); 16 + } 17 + }
+3
src/main.tsx
··· 22 22 if (Symbol.dispose === undefined) { 23 23 Object.defineProperty(Symbol, 'dispose', { value: Symbol.for(`Symbol.dispose`) }); 24 24 } 25 + if (Symbol.asyncDispose === undefined) { 26 + Object.defineProperty(Symbol, 'asyncDispose', { value: Symbol.for(`Symbol.asyncDispose`) }); 27 + } 25 28 26 29 render(App, document.body);
+15 -2
src/routes.ts
··· 22 22 path: '/crypto-generate', 23 23 component: lazy(() => import('./views/crypto/crypto-generate')), 24 24 }, 25 + { 26 + path: '/crypto-info', 27 + component: lazy(() => import('./views/crypto/crypto-info')), 28 + }, 25 29 26 30 { 27 31 path: '/did-lookup', ··· 41 45 component: lazy(() => import('./views/repository/repo-export')), 42 46 }, 43 47 { 44 - path: '/car-unpack', 45 - component: lazy(() => import('./views/repository/car-unpack')), 48 + path: '/repo-archive-unpack', 49 + component: lazy(() => import('./views/repository/repo-archive-unpack')), 50 + }, 51 + { 52 + path: '/repo-archive-explore', 53 + component: lazy(() => import('./views/repository/repo-archive-explore/page')), 54 + }, 55 + 56 + { 57 + path: '/account-migrate', 58 + component: lazy(() => import('./views/account/account-migrate/page')), 46 59 }, 47 60 48 61 {
+49
src/views/account/account-migrate/context.tsx
··· 1 + import { createContext, createSignal, useContext, type JSX } from 'solid-js'; 2 + 3 + import type { CredentialManager } from '@atcute/client'; 4 + import type { DidDocument } from '@atcute/identity'; 5 + import type { AtprotoDid, Did } from '@atcute/lexicons/syntax'; 6 + 7 + export interface SourceAccount { 8 + did: AtprotoDid; 9 + didDoc: DidDocument; 10 + pdsUrl: string; 11 + manager: CredentialManager | null; 12 + } 13 + 14 + export interface DestinationAccount { 15 + pdsUrl: string; 16 + serviceDid: Did; 17 + manager: CredentialManager | null; 18 + } 19 + 20 + export interface MigrationContextValue { 21 + source: () => SourceAccount | null; 22 + setSource: (account: SourceAccount | null) => void; 23 + destination: () => DestinationAccount | null; 24 + setDestination: (account: DestinationAccount | null) => void; 25 + } 26 + 27 + const MigrationContext = createContext<MigrationContextValue>(); 28 + 29 + export const MigrationProvider = (props: { children: JSX.Element }) => { 30 + const [source, setSource] = createSignal<SourceAccount | null>(null); 31 + const [destination, setDestination] = createSignal<DestinationAccount | null>(null); 32 + 33 + const value: MigrationContextValue = { 34 + source, 35 + setSource, 36 + destination, 37 + setDestination, 38 + }; 39 + 40 + return <MigrationContext.Provider value={value}>{props.children}</MigrationContext.Provider>; 41 + }; 42 + 43 + export const useMigration = (): MigrationContextValue => { 44 + const context = useContext(MigrationContext); 45 + if (!context) { 46 + throw new Error('useMigration must be used within a MigrationProvider'); 47 + } 48 + return context; 49 + };
+54
src/views/account/account-migrate/page.tsx
··· 1 + import { createEffect, createSignal, onCleanup } from 'solid-js'; 2 + 3 + import { history } from '~/globals/navigation'; 4 + 5 + import { useTitle } from '~/lib/navigation/router'; 6 + 7 + import PageHeader from '~/components/page-header'; 8 + 9 + import { MigrationProvider } from './context'; 10 + 11 + import SourceAccountSection from './sections/source-account'; 12 + import DestinationAccountSection from './sections/destination-account'; 13 + import RepositorySection from './sections/repository'; 14 + import BlobsSection from './sections/blobs'; 15 + import PreferencesSection from './sections/preferences'; 16 + import IdentitySection from './sections/identity'; 17 + import AccountStatusSection from './sections/account-status'; 18 + 19 + const AccountMigratePage = () => { 20 + const [hasStarted, setHasStarted] = createSignal(false); 21 + 22 + createEffect(() => { 23 + if (hasStarted()) { 24 + const cleanup = history.block((tx) => { 25 + if (window.confirm(`You have a migration in progress. Leave this page?`)) { 26 + cleanup(); 27 + tx.retry(); 28 + } 29 + }); 30 + 31 + onCleanup(cleanup); 32 + } 33 + }); 34 + 35 + useTitle(() => `Migrate account โ€” boat`); 36 + 37 + return ( 38 + <MigrationProvider> 39 + <PageHeader title="Migrate account" subtitle="Move your account data to another server" /> 40 + 41 + <div class="flex flex-col"> 42 + <SourceAccountSection onStarted={() => setHasStarted(true)} /> 43 + <DestinationAccountSection /> 44 + <RepositorySection /> 45 + <BlobsSection /> 46 + <PreferencesSection /> 47 + <IdentitySection /> 48 + <AccountStatusSection /> 49 + </div> 50 + </MigrationProvider> 51 + ); 52 + }; 53 + 54 + export default AccountMigratePage;
+207
src/views/account/account-migrate/sections/account-status.tsx
··· 1 + import { Show } from 'solid-js'; 2 + 3 + import { Client, type CredentialManager, ok } from '@atcute/client'; 4 + 5 + import { createMutation } from '~/lib/utils/mutation'; 6 + 7 + import { Accordion, StatusBadge, Subsection } from '~/components/accordion'; 8 + import Button from '~/components/inputs/button'; 9 + 10 + import { useMigration } from '../context'; 11 + 12 + interface AccountStatus { 13 + activated: boolean; 14 + validDid: boolean; 15 + repoCommit: string; 16 + repoRev: string; 17 + repoBlocks: number; 18 + indexedRecords: number; 19 + privateStateValues: number; 20 + expectedBlobs: number; 21 + importedBlobs: number; 22 + } 23 + 24 + const AccountStatusSection = () => { 25 + const { source, destination } = useMigration(); 26 + 27 + const checkSourceMutation = createMutation({ 28 + async mutationFn({ manager }: { manager: CredentialManager }) { 29 + const sourceClient = new Client({ handler: manager }); 30 + return await ok(sourceClient.get('com.atproto.server.checkAccountStatus')) as AccountStatus; 31 + }, 32 + onError(err) { 33 + console.error(err); 34 + }, 35 + }); 36 + 37 + const checkDestMutation = createMutation({ 38 + async mutationFn({ manager }: { manager: CredentialManager }) { 39 + const destClient = new Client({ handler: manager }); 40 + return await ok(destClient.get('com.atproto.server.checkAccountStatus')) as AccountStatus; 41 + }, 42 + onError(err) { 43 + console.error(err); 44 + }, 45 + }); 46 + 47 + const activateMutation = createMutation({ 48 + async mutationFn({ manager }: { manager: CredentialManager }) { 49 + const destClient = new Client({ handler: manager }); 50 + await ok(destClient.post('com.atproto.server.activateAccount', { as: null })); 51 + }, 52 + onSuccess() { 53 + const dest = destination(); 54 + if (dest?.manager) { 55 + checkDestMutation.mutate({ manager: dest.manager }); 56 + } 57 + }, 58 + onError(err) { 59 + console.error(err); 60 + }, 61 + }); 62 + 63 + const deactivateMutation = createMutation({ 64 + async mutationFn({ manager }: { manager: CredentialManager }) { 65 + if (!confirm('Are you sure you want to deactivate your source account? This will prevent the old PDS from serving your data.')) { 66 + throw new Error('Cancelled'); 67 + } 68 + const sourceClient = new Client({ handler: manager }); 69 + await ok(sourceClient.post('com.atproto.server.deactivateAccount', { as: null, input: {} })); 70 + }, 71 + onSuccess() { 72 + const src = source(); 73 + if (src?.manager) { 74 + checkSourceMutation.mutate({ manager: src.manager }); 75 + } 76 + }, 77 + onError(err) { 78 + if (err instanceof Error && err.message === 'Cancelled') return; 79 + console.error(err); 80 + }, 81 + }); 82 + 83 + const renderStatus = (status: AccountStatus) => ( 84 + <div class="space-y-1 text-sm"> 85 + <p> 86 + <span class="text-gray-500">Status:</span>{' '} 87 + <StatusBadge variant={status.activated ? 'success' : 'idle'}> 88 + {status.activated ? 'Active' : 'Deactivated'} 89 + </StatusBadge> 90 + </p> 91 + <p> 92 + <span class="text-gray-500">Records:</span>{' '} 93 + <span class="font-mono">{status.indexedRecords}</span> 94 + </p> 95 + <p> 96 + <span class="text-gray-500">Blobs:</span>{' '} 97 + <span class="font-mono">{status.importedBlobs}/{status.expectedBlobs}</span> 98 + </p> 99 + <p> 100 + <span class="text-gray-500">Repo blocks:</span>{' '} 101 + <span class="font-mono">{status.repoBlocks}</span> 102 + </p> 103 + </div> 104 + ); 105 + 106 + return ( 107 + <Accordion title="Account Status"> 108 + <Subsection title="Source account"> 109 + <Show 110 + when={source()?.manager} 111 + fallback={<p class="text-sm text-gray-500">Sign in to source account first.</p>} 112 + > 113 + {(manager) => ( 114 + <> 115 + <div class="flex items-center gap-3"> 116 + <Button 117 + variant="outline" 118 + onClick={() => checkSourceMutation.mutate({ manager: manager() })} 119 + disabled={checkSourceMutation.isPending} 120 + > 121 + {checkSourceMutation.isPending ? 'Checking...' : 'Check status'} 122 + </Button> 123 + </div> 124 + 125 + <Show when={checkSourceMutation.isError}> 126 + <p class="text-sm text-red-600">{`${checkSourceMutation.error}`}</p> 127 + </Show> 128 + 129 + <Show when={checkSourceMutation.data}> 130 + {(status) => ( 131 + <> 132 + {renderStatus(status())} 133 + 134 + <Show when={status().activated}> 135 + <div class="mt-3"> 136 + <Button 137 + variant="secondary" 138 + onClick={() => deactivateMutation.mutate({ manager: manager() })} 139 + disabled={deactivateMutation.isPending} 140 + > 141 + {deactivateMutation.isPending ? 'Deactivating...' : 'Deactivate source account'} 142 + </Button> 143 + </div> 144 + </Show> 145 + </> 146 + )} 147 + </Show> 148 + </> 149 + )} 150 + </Show> 151 + </Subsection> 152 + 153 + <Subsection title="Destination account"> 154 + <Show 155 + when={destination()?.manager} 156 + fallback={<p class="text-sm text-gray-500">Sign in to destination account first.</p>} 157 + > 158 + {(manager) => ( 159 + <> 160 + <div class="flex items-center gap-3"> 161 + <Button 162 + variant="outline" 163 + onClick={() => checkDestMutation.mutate({ manager: manager() })} 164 + disabled={checkDestMutation.isPending} 165 + > 166 + {checkDestMutation.isPending ? 'Checking...' : 'Check status'} 167 + </Button> 168 + </div> 169 + 170 + <Show when={checkDestMutation.isError}> 171 + <p class="text-sm text-red-600">{`${checkDestMutation.error}`}</p> 172 + </Show> 173 + 174 + <Show when={checkDestMutation.data}> 175 + {(status) => ( 176 + <> 177 + {renderStatus(status())} 178 + 179 + <Show when={!status().activated}> 180 + <div class="mt-3"> 181 + <Button 182 + onClick={() => activateMutation.mutate({ manager: manager() })} 183 + disabled={activateMutation.isPending} 184 + > 185 + {activateMutation.isPending ? 'Activating...' : 'Activate destination account'} 186 + </Button> 187 + </div> 188 + </Show> 189 + </> 190 + )} 191 + </Show> 192 + </> 193 + )} 194 + </Show> 195 + </Subsection> 196 + 197 + <Show when={activateMutation.isError || deactivateMutation.isError}> 198 + <p class="text-sm text-red-600"> 199 + {activateMutation.isError ? `Failed to activate: ${activateMutation.error}` : ''} 200 + {deactivateMutation.isError ? `Failed to deactivate: ${deactivateMutation.error}` : ''} 201 + </p> 202 + </Show> 203 + </Accordion> 204 + ); 205 + }; 206 + 207 + export default AccountStatusSection;
+455
src/views/account/account-migrate/sections/blobs.tsx
··· 1 + import { showOpenFilePicker, showSaveFilePicker } from 'native-file-system-adapter'; 2 + import { createSignal, For, Show } from 'solid-js'; 3 + 4 + import { Client, ClientResponseError, type CredentialManager, ok, simpleFetchHandler } from '@atcute/client'; 5 + import { untar, writeTarEntry } from '@mary/tar'; 6 + 7 + import { createMutation } from '~/lib/utils/mutation'; 8 + 9 + import { Accordion, StatusBadge, Subsection } from '~/components/accordion'; 10 + import Button from '~/components/inputs/button'; 11 + 12 + import { useMigration, type SourceAccount } from '../context'; 13 + 14 + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 15 + 16 + const BlobsSection = () => { 17 + const { source, destination } = useMigration(); 18 + 19 + // Progress state (kept separate since mutations don't handle incremental updates) 20 + const [exportProgress, setExportProgress] = createSignal<string>(); 21 + const [importProgress, setImportProgress] = createSignal<string>(); 22 + 23 + const exportMutation = createMutation({ 24 + async mutationFn({ source }: { source: SourceAccount }) { 25 + const sourceClient = new Client({ handler: simpleFetchHandler({ service: source.pdsUrl }) }); 26 + 27 + setExportProgress('Retrieving list of blobs...'); 28 + 29 + // Get list of all blobs 30 + let blobs: string[] = []; 31 + let cursor: string | undefined; 32 + do { 33 + const data = await ok( 34 + sourceClient.get('com.atproto.sync.listBlobs', { 35 + params: { did: source.did, cursor, limit: 1_000 }, 36 + }), 37 + ); 38 + cursor = data.cursor; 39 + blobs = blobs.concat(data.cids); 40 + setExportProgress(`Retrieving list of blobs (found ${blobs.length})`); 41 + } while (cursor !== undefined); 42 + 43 + if (blobs.length === 0) { 44 + return { count: 0, cancelled: false }; 45 + } 46 + 47 + setExportProgress('Waiting for file picker...'); 48 + 49 + const fd = await showSaveFilePicker({ 50 + suggestedName: `blobs-${source.did}-${new Date().toISOString()}.tar`, 51 + // @ts-expect-error: ponyfill doesn't have the full typings 52 + id: 'blob-export', 53 + startIn: 'downloads', 54 + types: [ 55 + { 56 + description: 'Tarball archive', 57 + accept: { 'application/tar': ['.tar'] }, 58 + }, 59 + ], 60 + }).catch((err) => { 61 + if (err instanceof DOMException && err.name === 'AbortError') { 62 + return undefined; 63 + } 64 + throw err; 65 + }); 66 + 67 + if (!fd) { 68 + return { count: 0, cancelled: true }; 69 + } 70 + 71 + const writable = await fd.createWritable(); 72 + 73 + let downloaded = 0; 74 + for (const cid of blobs) { 75 + setExportProgress(`Downloading blobs (${downloaded}/${blobs.length})`); 76 + 77 + const downloadBlob = async (): Promise<Uint8Array | undefined> => { 78 + let attempts = 0; 79 + while (true) { 80 + if (attempts > 0) await sleep(2_000); 81 + attempts++; 82 + 83 + try { 84 + const response = await sourceClient.get('com.atproto.sync.getBlob', { 85 + as: 'bytes', 86 + params: { did: source.did, cid }, 87 + }); 88 + 89 + if (response.ok) { 90 + return response.data; 91 + } 92 + 93 + if (response.status === 400 && response.data.message === 'Blob not found') { 94 + return undefined; 95 + } 96 + 97 + if (response.status === 429) { 98 + await sleep(10_000); 99 + } 100 + 101 + if (attempts < 3) continue; 102 + throw new ClientResponseError(response); 103 + } catch (err) { 104 + if (attempts < 3) continue; 105 + throw err; 106 + } 107 + } 108 + }; 109 + 110 + const data = await downloadBlob(); 111 + if (data !== undefined) { 112 + const entry = writeTarEntry({ filename: `blobs/${cid}`, data }); 113 + await writable.write(entry); 114 + } 115 + 116 + downloaded++; 117 + } 118 + 119 + await writable.close(); 120 + return { count: blobs.length, cancelled: false }; 121 + }, 122 + onError(err) { 123 + console.error(err); 124 + }, 125 + onSettled() { 126 + setExportProgress(); 127 + }, 128 + }); 129 + 130 + const importFromFileMutation = createMutation({ 131 + async mutationFn({ destManager }: { destManager: CredentialManager }) { 132 + setImportProgress('Waiting for file picker...'); 133 + 134 + const [fd] = await showOpenFilePicker({ 135 + // @ts-expect-error: ponyfill doesn't have the full typings 136 + id: 'blob-import', 137 + types: [ 138 + { 139 + description: 'Tarball archive', 140 + accept: { 'application/tar': ['.tar'] }, 141 + }, 142 + ], 143 + }).catch((err) => { 144 + if (err instanceof DOMException && err.name === 'AbortError') { 145 + return [undefined]; 146 + } 147 + throw err; 148 + }); 149 + 150 + if (!fd) { 151 + return { uploaded: 0, failed: 0, cancelled: true }; 152 + } 153 + 154 + setImportProgress('Reading archive...'); 155 + const file = await fd.getFile(); 156 + 157 + const destClient = new Client({ handler: destManager }); 158 + 159 + let uploaded = 0; 160 + let failed = 0; 161 + 162 + for await (const entry of untar(file.stream())) { 163 + if (entry.type !== 'file') continue; 164 + 165 + const filename = entry.name; 166 + // Extract CID from path like "blobs/bafk..." 167 + const cid = filename.split('/').pop(); 168 + if (!cid) continue; 169 + 170 + setImportProgress(`Uploading blobs (${uploaded} uploaded, ${failed} failed)`); 171 + 172 + try { 173 + const data = await entry.bytes(); 174 + await destClient.post('com.atproto.repo.uploadBlob', { 175 + input: data, 176 + headers: { 177 + 'content-type': 'application/octet-stream', 178 + }, 179 + }); 180 + uploaded++; 181 + } catch (err) { 182 + console.error(`Failed to upload blob ${cid}:`, err); 183 + failed++; 184 + } 185 + } 186 + 187 + return { uploaded, failed, cancelled: false }; 188 + }, 189 + onError(err) { 190 + console.error(err); 191 + }, 192 + onSettled() { 193 + setImportProgress(); 194 + }, 195 + }); 196 + 197 + const importFromSourceMutation = createMutation({ 198 + async mutationFn({ source, destManager }: { source: SourceAccount; destManager: CredentialManager }) { 199 + setImportProgress('Checking for missing blobs...'); 200 + 201 + const sourceClient = new Client({ handler: simpleFetchHandler({ service: source.pdsUrl }) }); 202 + const destClient = new Client({ handler: destManager }); 203 + 204 + let uploaded = 0; 205 + let failed = 0; 206 + let cursor: string | undefined; 207 + 208 + do { 209 + const data = await ok( 210 + destClient.get('com.atproto.repo.listMissingBlobs', { 211 + params: { cursor, limit: 100 }, 212 + }), 213 + ); 214 + cursor = data.cursor; 215 + 216 + for (const blob of data.blobs) { 217 + setImportProgress(`Uploading missing blobs (${uploaded} uploaded, ${failed} failed)`); 218 + 219 + try { 220 + const response = await sourceClient.get('com.atproto.sync.getBlob', { 221 + as: 'stream', 222 + params: { did: source.did, cid: blob.cid }, 223 + }); 224 + 225 + if (!response.ok) { 226 + failed++; 227 + continue; 228 + } 229 + 230 + const contentType = response.headers.get('content-type') || 'application/octet-stream'; 231 + 232 + await destClient.post('com.atproto.repo.uploadBlob', { 233 + input: response.data, 234 + headers: { 235 + 'content-type': contentType, 236 + }, 237 + }); 238 + 239 + uploaded++; 240 + } catch (err) { 241 + console.error(`Failed to transfer blob ${blob.cid}:`, err); 242 + failed++; 243 + } 244 + } 245 + } while (cursor !== undefined); 246 + 247 + return { uploaded, failed }; 248 + }, 249 + onError(err) { 250 + console.error(err); 251 + }, 252 + onSettled() { 253 + setImportProgress(); 254 + }, 255 + }); 256 + 257 + const checkStatusMutation = createMutation({ 258 + async mutationFn({ destManager }: { destManager: CredentialManager }) { 259 + const destClient = new Client({ handler: destManager }); 260 + const status = await ok(destClient.get('com.atproto.server.checkAccountStatus')); 261 + 262 + let missingBlobs: string[] = []; 263 + 264 + // Get list of missing blobs if any 265 + if (status.expectedBlobs > status.importedBlobs) { 266 + let cursor: string | undefined; 267 + do { 268 + const data = await ok( 269 + destClient.get('com.atproto.repo.listMissingBlobs', { 270 + params: { cursor, limit: 100 }, 271 + }), 272 + ); 273 + cursor = data.cursor; 274 + missingBlobs.push(...data.blobs.map((b) => b.cid)); 275 + } while (cursor !== undefined); 276 + } 277 + 278 + return { 279 + expected: status.expectedBlobs, 280 + imported: status.importedBlobs, 281 + missingBlobs, 282 + }; 283 + }, 284 + onError(err) { 285 + console.error(err); 286 + }, 287 + }); 288 + 289 + const isImporting = () => importFromFileMutation.isPending || importFromSourceMutation.isPending; 290 + 291 + const getExportStatusText = () => { 292 + const data = exportMutation.data; 293 + if (data?.cancelled) return undefined; 294 + if (data?.count === 0) return 'No blobs to export'; 295 + if (data) return `Exported ${data.count} blobs`; 296 + return exportProgress(); 297 + }; 298 + 299 + const getImportStatusText = () => { 300 + const fileData = importFromFileMutation.data; 301 + const sourceData = importFromSourceMutation.data; 302 + 303 + if (fileData && !fileData.cancelled) { 304 + return ( 305 + `Uploaded ${fileData.uploaded} blobs` + (fileData.failed > 0 ? ` (${fileData.failed} failed)` : '') 306 + ); 307 + } 308 + if (sourceData) { 309 + if (sourceData.uploaded === 0 && sourceData.failed === 0) return 'No missing blobs'; 310 + return ( 311 + `Uploaded ${sourceData.uploaded} blobs` + 312 + (sourceData.failed > 0 ? ` (${sourceData.failed} failed)` : '') 313 + ); 314 + } 315 + return importProgress(); 316 + }; 317 + 318 + const getImportError = () => importFromFileMutation.error || importFromSourceMutation.error; 319 + 320 + return ( 321 + <Accordion title="Blobs"> 322 + <Subsection title="Export from source"> 323 + <p class="text-sm text-gray-600">Download all blobs as a tarball for backup or manual import.</p> 324 + 325 + <Show when={source()} fallback={<p class="text-sm text-gray-500">Resolve source account first.</p>}> 326 + {(src) => ( 327 + <> 328 + <div class="flex items-center gap-3"> 329 + <Button 330 + onClick={() => exportMutation.mutate({ source: src() })} 331 + disabled={exportMutation.isPending} 332 + > 333 + {exportMutation.isPending ? 'Exporting...' : 'Export to file'} 334 + </Button> 335 + <Show when={getExportStatusText()}> 336 + {(text) => <span class="text-sm text-gray-600">{text()}</span>} 337 + </Show> 338 + </div> 339 + 340 + <Show when={exportMutation.error}> 341 + {(err) => <p class="text-sm text-red-600">{`${err()}`}</p>} 342 + </Show> 343 + </> 344 + )} 345 + </Show> 346 + </Subsection> 347 + 348 + <Subsection title="Import to destination"> 349 + <p class="text-sm text-gray-600">Upload blobs from a tarball or transfer directly from source.</p> 350 + 351 + <Show 352 + when={destination()?.manager} 353 + fallback={<p class="text-sm text-gray-500">Sign in to destination account first.</p>} 354 + > 355 + {(destManager) => ( 356 + <> 357 + <div class="flex flex-wrap items-center gap-3"> 358 + <Button 359 + onClick={() => importFromFileMutation.mutate({ destManager: destManager() })} 360 + disabled={isImporting()} 361 + > 362 + {isImporting() ? 'Importing...' : 'Import from file'} 363 + </Button> 364 + 365 + <Show when={source()}> 366 + {(src) => ( 367 + <Button 368 + variant="secondary" 369 + onClick={() => 370 + importFromSourceMutation.mutate({ source: src(), destManager: destManager() }) 371 + } 372 + disabled={isImporting()} 373 + > 374 + Transfer from source 375 + </Button> 376 + )} 377 + </Show> 378 + </div> 379 + 380 + <Show when={getImportStatusText()}> 381 + {(text) => <span class="text-sm text-gray-600">{text()}</span>} 382 + </Show> 383 + 384 + <Show when={getImportError()}>{(err) => <p class="text-sm text-red-600">{`${err()}`}</p>}</Show> 385 + </> 386 + )} 387 + </Show> 388 + </Subsection> 389 + 390 + <Subsection title="Status"> 391 + <Show 392 + when={destination()?.manager} 393 + fallback={<p class="text-sm text-gray-500">Sign in to destination account first.</p>} 394 + > 395 + {(destManager) => ( 396 + <> 397 + <div class="flex items-center gap-3"> 398 + <Button 399 + variant="outline" 400 + onClick={() => checkStatusMutation.mutate({ destManager: destManager() })} 401 + disabled={checkStatusMutation.isPending} 402 + > 403 + {checkStatusMutation.isPending ? 'Checking...' : 'Check status'} 404 + </Button> 405 + 406 + <Show when={checkStatusMutation.data}> 407 + {(status) => ( 408 + <span class="text-sm"> 409 + <StatusBadge variant={status().imported === status().expected ? 'success' : 'pending'}> 410 + {status().imported}/{status().expected} blobs 411 + </StatusBadge> 412 + </span> 413 + )} 414 + </Show> 415 + </div> 416 + 417 + <Show when={checkStatusMutation.data?.missingBlobs.length}> 418 + {(count) => ( 419 + <div class="mt-2 rounded border border-yellow-300 bg-yellow-50 p-3"> 420 + <p class="mb-2 text-sm font-medium text-yellow-800">{count()} missing blobs</p> 421 + 422 + <Show when={source()}> 423 + {(src) => ( 424 + <Button 425 + variant="secondary" 426 + onClick={() => 427 + importFromSourceMutation.mutate({ source: src(), destManager: destManager() }) 428 + } 429 + disabled={isImporting()} 430 + > 431 + Transfer missing from source 432 + </Button> 433 + )} 434 + </Show> 435 + 436 + <details class="mt-2"> 437 + <summary class="cursor-pointer text-sm text-yellow-700">Show CIDs</summary> 438 + <div class="mt-1 max-h-32 overflow-auto font-mono text-xs"> 439 + <For each={checkStatusMutation.data?.missingBlobs}> 440 + {(cid) => <div class="truncate">{cid}</div>} 441 + </For> 442 + </div> 443 + </details> 444 + </div> 445 + )} 446 + </Show> 447 + </> 448 + )} 449 + </Show> 450 + </Subsection> 451 + </Accordion> 452 + ); 453 + }; 454 + 455 + export default BlobsSection;
+437
src/views/account/account-migrate/sections/destination-account.tsx
··· 1 + import { createSignal, Show } from 'solid-js'; 2 + 3 + import { 4 + type AtpAccessJwt, 5 + Client, 6 + ClientResponseError, 7 + CredentialManager, 8 + ok, 9 + simpleFetchHandler, 10 + } from '@atcute/client'; 11 + import type { Did, Handle } from '@atcute/lexicons/syntax'; 12 + 13 + import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 14 + import { decodeJwt } from '~/api/utils/jwt'; 15 + import { isServiceUrlString } from '~/api/types/strings'; 16 + 17 + import { createMutation } from '~/lib/utils/mutation'; 18 + 19 + import { Accordion, StatusBadge, Subsection } from '~/components/accordion'; 20 + import Button from '~/components/inputs/button'; 21 + import TextInput from '~/components/inputs/text-input'; 22 + 23 + import { useMigration } from '../context'; 24 + 25 + class InsufficientLoginError extends Error {} 26 + 27 + const DestinationAccountSection = () => { 28 + const { source, destination, setDestination } = useMigration(); 29 + 30 + // Connect state 31 + const [pdsUrl, setPdsUrl] = createSignal(''); 32 + const [connectError, setConnectError] = createSignal<string>(); 33 + 34 + // Create account state 35 + const [newHandle, setNewHandle] = createSignal(''); 36 + const [newEmail, setNewEmail] = createSignal(''); 37 + const [newPassword, setNewPassword] = createSignal(''); 38 + const [inviteCode, setInviteCode] = createSignal(''); 39 + const [createError, setCreateError] = createSignal<string>(); 40 + 41 + // Login state 42 + const [loginPassword, setLoginPassword] = createSignal(''); 43 + const [loginOtp, setLoginOtp] = createSignal(''); 44 + const [isLoginTotpRequired, setIsLoginTotpRequired] = createSignal(false); 45 + const [loginError, setLoginError] = createSignal<string>(); 46 + 47 + const connectMutation = createMutation({ 48 + async mutationFn({ pdsUrl }: { pdsUrl: string }) { 49 + const destClient = new Client({ handler: simpleFetchHandler({ service: pdsUrl }) }); 50 + const desc = await ok(destClient.get('com.atproto.server.describeServer')); 51 + 52 + return { serviceDid: desc.did }; 53 + }, 54 + onMutate() { 55 + setConnectError(); 56 + }, 57 + onSuccess({ serviceDid }) { 58 + setDestination({ pdsUrl: pdsUrl(), serviceDid, manager: null }); 59 + }, 60 + onError(err) { 61 + console.error(err); 62 + setConnectError(`Failed to connect: ${err}`); 63 + }, 64 + }); 65 + 66 + const createAccountMutation = createMutation({ 67 + async mutationFn({ 68 + sourceDid, 69 + sourceManager, 70 + destPdsUrl, 71 + destServiceDid, 72 + handle, 73 + email, 74 + password, 75 + inviteCode, 76 + }: { 77 + sourceDid: Did; 78 + sourceManager: CredentialManager; 79 + destPdsUrl: string; 80 + destServiceDid: string; 81 + handle: Handle; 82 + email: string; 83 + password: string; 84 + inviteCode: string; 85 + }) { 86 + // Get service auth token from old PDS 87 + const sourceClient = new Client({ handler: sourceManager }); 88 + const authResp = await ok( 89 + sourceClient.get('com.atproto.server.getServiceAuth', { 90 + params: { 91 + aud: destServiceDid as Did, 92 + lxm: 'com.atproto.server.createAccount', 93 + }, 94 + }), 95 + ); 96 + const serviceJwt = authResp.token; 97 + 98 + // Create account on new PDS with service auth 99 + const destClient = new Client({ handler: simpleFetchHandler({ service: destPdsUrl }) }); 100 + const createResp = await destClient.post('com.atproto.server.createAccount', { 101 + headers: { Authorization: `Bearer ${serviceJwt}` }, 102 + input: { 103 + did: sourceDid, 104 + handle: handle, 105 + email: email, 106 + password: password, 107 + inviteCode: inviteCode || undefined, 108 + }, 109 + }); 110 + 111 + if (!createResp.ok) { 112 + throw new ClientResponseError(createResp); 113 + } 114 + 115 + if (createResp.data.did !== sourceDid) { 116 + throw new Error(`Created account has different DID: ${createResp.data.did}`); 117 + } 118 + 119 + // Login to the new account 120 + const manager = new CredentialManager({ service: destPdsUrl }); 121 + await manager.login({ 122 + identifier: sourceDid, 123 + password: password, 124 + }); 125 + 126 + return manager; 127 + }, 128 + onMutate() { 129 + setCreateError(); 130 + }, 131 + onSuccess(manager) { 132 + setDestination({ ...destination()!, manager }); 133 + setNewPassword(''); 134 + }, 135 + onError(err) { 136 + if (err instanceof ClientResponseError) { 137 + if (err.error === 'InvalidInviteCode') { 138 + setCreateError(`Invalid invite code`); 139 + return; 140 + } 141 + if (err.error === 'HandleNotAvailable') { 142 + setCreateError(`Handle is not available`); 143 + return; 144 + } 145 + if (err.description) { 146 + setCreateError(err.description); 147 + return; 148 + } 149 + } 150 + console.error(err); 151 + setCreateError(`${err}`); 152 + }, 153 + }); 154 + 155 + const loginMutation = createMutation({ 156 + async mutationFn({ 157 + pdsUrl, 158 + did, 159 + password, 160 + otp, 161 + }: { 162 + pdsUrl: string; 163 + did: string; 164 + password: string; 165 + otp: string; 166 + }) { 167 + const manager = new CredentialManager({ service: pdsUrl }); 168 + const session = await manager.login({ 169 + identifier: did, 170 + password: password, 171 + code: formatTotpCode(otp), 172 + }); 173 + 174 + const decoded = decodeJwt(session.accessJwt) as AtpAccessJwt; 175 + if (decoded.scope !== 'com.atproto.access') { 176 + throw new InsufficientLoginError(`You need to sign in with a main password, not an app password`); 177 + } 178 + 179 + return manager; 180 + }, 181 + onMutate() { 182 + setLoginError(); 183 + }, 184 + onSuccess(manager) { 185 + setDestination({ ...destination()!, manager }); 186 + setLoginPassword(''); 187 + setLoginOtp(''); 188 + setIsLoginTotpRequired(false); 189 + }, 190 + onError(err) { 191 + if (err instanceof ClientResponseError) { 192 + if (err.error === 'AuthFactorTokenRequired') { 193 + setLoginOtp(''); 194 + setIsLoginTotpRequired(true); 195 + return; 196 + } 197 + if (err.error === 'AuthenticationRequired') { 198 + setLoginError(`Invalid identifier or password`); 199 + return; 200 + } 201 + if (err.description?.includes('Token is invalid')) { 202 + setLoginError(`Invalid one-time confirmation code`); 203 + setIsLoginTotpRequired(true); 204 + return; 205 + } 206 + } 207 + if (err instanceof InsufficientLoginError) { 208 + setLoginError(err.message); 209 + return; 210 + } 211 + console.error(err); 212 + setLoginError(`${err}`); 213 + }, 214 + }); 215 + 216 + const isConnected = () => destination() !== null; 217 + const isAuthenticated = () => destination()?.manager != null; 218 + const canCreateAccount = () => source()?.manager != null; 219 + 220 + return ( 221 + <Accordion title="Destination Account"> 222 + <Subsection title="Connect to PDS"> 223 + <Show when={!isConnected()}> 224 + <form 225 + onSubmit={(ev) => { 226 + ev.preventDefault(); 227 + connectMutation.mutate({ pdsUrl: pdsUrl() }); 228 + }} 229 + class="flex flex-col gap-3" 230 + > 231 + <TextInput 232 + label="PDS URL" 233 + type="url" 234 + placeholder="https://pds.example.com" 235 + value={pdsUrl()} 236 + required 237 + onChange={(text, event) => { 238 + setPdsUrl(text); 239 + const input = event.currentTarget; 240 + if (text !== '' && !isServiceUrlString(text)) { 241 + input.setCustomValidity('Must be a valid URL'); 242 + } else { 243 + input.setCustomValidity(''); 244 + } 245 + }} 246 + /> 247 + 248 + <Show when={connectError()}> 249 + <p class="text-sm text-red-600">{connectError()}</p> 250 + </Show> 251 + 252 + <div> 253 + <Button type="submit" disabled={connectMutation.isPending}> 254 + {connectMutation.isPending ? 'Connecting...' : 'Connect'} 255 + </Button> 256 + </div> 257 + </form> 258 + </Show> 259 + 260 + <Show when={isConnected()}> 261 + <div class="flex flex-col gap-2 text-sm"> 262 + <p> 263 + <span class="text-gray-500">URL:</span>{' '} 264 + <span class="font-mono">{destination()!.pdsUrl}</span> 265 + </p> 266 + <p> 267 + <span class="text-gray-500">Service DID:</span>{' '} 268 + <span class="font-mono">{destination()!.serviceDid}</span> 269 + </p> 270 + <div class="mt-1"> 271 + <button 272 + type="button" 273 + onClick={() => setDestination(null)} 274 + class="text-sm text-purple-800 hover:underline" 275 + > 276 + Change PDS 277 + </button> 278 + </div> 279 + </div> 280 + </Show> 281 + </Subsection> 282 + 283 + <Show when={isConnected() && !isAuthenticated()}> 284 + <Subsection title="Create new account"> 285 + <Show when={!canCreateAccount()}> 286 + <p class="text-sm text-gray-600"> 287 + You need to authenticate to your source account first to create an account on the 288 + destination PDS. 289 + </p> 290 + </Show> 291 + 292 + <Show when={canCreateAccount()}> 293 + <form 294 + onSubmit={(ev) => { 295 + ev.preventDefault(); 296 + const src = source()!; 297 + const dest = destination()!; 298 + createAccountMutation.mutate({ 299 + sourceDid: src.did, 300 + sourceManager: src.manager!, 301 + destPdsUrl: dest.pdsUrl, 302 + destServiceDid: dest.serviceDid, 303 + handle: newHandle() as Handle, 304 + email: newEmail(), 305 + password: newPassword(), 306 + inviteCode: inviteCode(), 307 + }); 308 + }} 309 + class="flex flex-col gap-3" 310 + > 311 + <TextInput 312 + label="Handle" 313 + placeholder="alice.pds.example.com" 314 + value={newHandle()} 315 + required 316 + onChange={setNewHandle} 317 + /> 318 + 319 + <TextInput 320 + label="Email" 321 + type="email" 322 + placeholder="alice@example.com" 323 + value={newEmail()} 324 + required 325 + onChange={setNewEmail} 326 + /> 327 + 328 + <TextInput 329 + label="Password" 330 + type="password" 331 + value={newPassword()} 332 + required 333 + onChange={setNewPassword} 334 + /> 335 + 336 + <TextInput 337 + label="Invite code (if required)" 338 + placeholder="pds-example-com-xxxxx" 339 + value={inviteCode()} 340 + onChange={setInviteCode} 341 + /> 342 + 343 + <Show when={createError()}> 344 + <p class="text-sm text-red-600">{createError()}</p> 345 + </Show> 346 + 347 + <div> 348 + <Button type="submit" disabled={createAccountMutation.isPending}> 349 + {createAccountMutation.isPending ? 'Creating...' : 'Create account'} 350 + </Button> 351 + </div> 352 + </form> 353 + </Show> 354 + </Subsection> 355 + 356 + <Subsection title="Or login to existing account"> 357 + <p class="mb-2 text-sm text-gray-600"> 358 + If you already have a deactivated account on the destination PDS. 359 + </p> 360 + 361 + <Show when={!source()}> 362 + <p class="text-sm text-gray-600"> 363 + Resolve your source account first so we know which DID to use. 364 + </p> 365 + </Show> 366 + 367 + <Show when={source()}> 368 + <form 369 + onSubmit={(ev) => { 370 + ev.preventDefault(); 371 + const src = source()!; 372 + const dest = destination()!; 373 + loginMutation.mutate({ 374 + pdsUrl: dest.pdsUrl, 375 + did: src.did, 376 + password: loginPassword(), 377 + otp: loginOtp(), 378 + }); 379 + }} 380 + class="flex flex-col gap-3" 381 + > 382 + <TextInput 383 + label="Password" 384 + type="password" 385 + value={loginPassword()} 386 + required 387 + onChange={setLoginPassword} 388 + /> 389 + 390 + <Show when={isLoginTotpRequired()}> 391 + <TextInput 392 + label="One-time confirmation code" 393 + blurb="A code has been sent to your email address." 394 + type="text" 395 + autocomplete="one-time-code" 396 + pattern={TOTP_RE.source} 397 + placeholder="AAAAA-BBBBB" 398 + value={loginOtp()} 399 + required 400 + onChange={setLoginOtp} 401 + monospace 402 + /> 403 + </Show> 404 + 405 + <Show when={loginError()}> 406 + <p class="text-sm text-red-600">{loginError()}</p> 407 + </Show> 408 + 409 + <div> 410 + <Button type="submit" disabled={loginMutation.isPending}> 411 + {loginMutation.isPending ? 'Signing in...' : 'Sign in'} 412 + </Button> 413 + </div> 414 + </form> 415 + </Show> 416 + </Subsection> 417 + </Show> 418 + 419 + <Show when={isAuthenticated()}> 420 + <Subsection title="Account status"> 421 + <div class="flex items-center gap-2"> 422 + <StatusBadge variant="success">Signed in</StatusBadge> 423 + <button 424 + type="button" 425 + onClick={() => setDestination({ ...destination()!, manager: null })} 426 + class="text-sm text-purple-800 hover:underline" 427 + > 428 + Sign out 429 + </button> 430 + </div> 431 + </Subsection> 432 + </Show> 433 + </Accordion> 434 + ); 435 + }; 436 + 437 + export default DestinationAccountSection;
+545
src/views/account/account-migrate/sections/identity.tsx
··· 1 + import { createSignal, For, Index, Show } from 'solid-js'; 2 + 3 + import { Client, ClientResponseError, type CredentialManager, ok } from '@atcute/client'; 4 + import { type DidKeyString, Secp256k1PrivateKeyExportable } from '@atcute/crypto'; 5 + import type { Did } from '@atcute/lexicons/syntax'; 6 + 7 + import { getPlcAuditLogs } from '~/api/queries/plc'; 8 + import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 9 + 10 + import { createMutation } from '~/lib/utils/mutation'; 11 + 12 + import { Accordion, StatusBadge, Subsection } from '~/components/accordion'; 13 + import Button from '~/components/inputs/button'; 14 + import TextInput from '~/components/inputs/text-input'; 15 + import ToggleInput from '~/components/inputs/toggle-input'; 16 + 17 + import { getPlcPayload } from '~/views/identity/plc-applicator/plc-utils'; 18 + 19 + import { useMigration } from '../context'; 20 + 21 + interface RecommendedCredentials { 22 + alsoKnownAs?: string[]; 23 + rotationKeys?: string[]; 24 + verificationMethods?: Record<string, unknown>; 25 + services?: Record<string, unknown>; 26 + } 27 + 28 + interface GeneratedKeypair { 29 + publicDidKey: DidKeyString; 30 + privateHex: string; 31 + privateMultikey: string; 32 + } 33 + 34 + const IdentitySection = () => { 35 + const { source, destination } = useMigration(); 36 + 37 + // Rotation key state 38 + const [useGeneratedKey, setUseGeneratedKey] = createSignal(false); 39 + const [customKeys, setCustomKeys] = createSignal<string[]>([]); 40 + const [plcToken, setPlcToken] = createSignal(''); 41 + 42 + const requestTokenMutation = createMutation({ 43 + async mutationFn({ manager }: { manager: CredentialManager }) { 44 + const client = new Client({ handler: manager }); 45 + await ok(client.post('com.atproto.identity.requestPlcOperationSignature', { as: null })); 46 + }, 47 + onError(err) { 48 + console.error(err); 49 + }, 50 + }); 51 + 52 + const loadCredentialsMutation = createMutation({ 53 + async mutationFn({ manager }: { manager: CredentialManager }) { 54 + const client = new Client({ handler: manager }); 55 + return (await ok( 56 + client.get('com.atproto.identity.getRecommendedDidCredentials', {}), 57 + )) as RecommendedCredentials; 58 + }, 59 + onError(err) { 60 + console.error(err); 61 + }, 62 + }); 63 + 64 + // Analyze current rotation keys to find user-controlled keys that should be preserved 65 + const analyzeRotationKeysMutation = createMutation({ 66 + async mutationFn({ did, sourceManager }: { did: Did<'plc'>; sourceManager: CredentialManager }, signal) { 67 + // Get current rotation keys from PLC audit log 68 + const auditLogs = await getPlcAuditLogs({ did, signal }); 69 + const latestEntry = auditLogs[auditLogs.length - 1]; 70 + const currentPayload = getPlcPayload(latestEntry); 71 + const currentRotationKeys = currentPayload.rotationKeys ?? []; 72 + 73 + // Get source PDS's recommended credentials to identify PDS-controlled keys 74 + const sourceClient = new Client({ handler: sourceManager }); 75 + const sourcePdsCredentials = (await ok( 76 + sourceClient.get('com.atproto.identity.getRecommendedDidCredentials', {}), 77 + )) as RecommendedCredentials; 78 + const sourcePdsKeys = new Set(sourcePdsCredentials.rotationKeys ?? []); 79 + 80 + // Keys in current doc that aren't from source PDS are user-controlled 81 + const userControlledKeys = currentRotationKeys.filter((key) => !sourcePdsKeys.has(key)); 82 + 83 + return { 84 + currentRotationKeys, 85 + sourcePdsKeys: sourcePdsCredentials.rotationKeys ?? [], 86 + userControlledKeys, 87 + }; 88 + }, 89 + onSuccess(data) { 90 + // Pre-populate custom keys with user-controlled keys 91 + if (data.userControlledKeys.length > 0) { 92 + setCustomKeys(data.userControlledKeys); 93 + } 94 + }, 95 + onError(err) { 96 + console.error(err); 97 + }, 98 + }); 99 + 100 + const generateKeyMutation = createMutation({ 101 + async mutationFn() { 102 + const keypair = await Secp256k1PrivateKeyExportable.createKeypair(); 103 + const [publicDidKey, privateHex, privateMultikey] = await Promise.all([ 104 + keypair.exportPublicKey('did'), 105 + keypair.exportPrivateKey('rawHex'), 106 + keypair.exportPrivateKey('multikey'), 107 + ]); 108 + return { publicDidKey, privateHex, privateMultikey } as GeneratedKeypair; 109 + }, 110 + onError(err) { 111 + console.error(err); 112 + }, 113 + }); 114 + 115 + const signAndSubmitMutation = createMutation({ 116 + async mutationFn({ 117 + sourceManager, 118 + destManager, 119 + token, 120 + credentials, 121 + generatedKey, 122 + customKeys, 123 + }: { 124 + sourceManager: CredentialManager; 125 + destManager: CredentialManager; 126 + token: string; 127 + credentials: RecommendedCredentials; 128 + generatedKey?: GeneratedKeypair; 129 + customKeys: string[]; 130 + }) { 131 + const sourceClient = new Client({ handler: sourceManager }); 132 + const destClient = new Client({ handler: destManager }); 133 + 134 + // Prepend user keys to PDS-provided keys (so user keys appear first for recovery) 135 + const pdsRotationKeys = credentials.rotationKeys ?? []; 136 + const userKeys: string[] = []; 137 + if (generatedKey) { 138 + userKeys.push(generatedKey.publicDidKey); 139 + } 140 + userKeys.push(...customKeys.filter((k) => k.trim())); 141 + const rotationKeys = [...userKeys, ...pdsRotationKeys]; 142 + 143 + // Sign the PLC operation on the source PDS 144 + const signage = await ok( 145 + sourceClient.post('com.atproto.identity.signPlcOperation', { 146 + input: { 147 + token: formatTotpCode(token), 148 + alsoKnownAs: credentials.alsoKnownAs, 149 + rotationKeys: rotationKeys, 150 + services: credentials.services, 151 + verificationMethods: credentials.verificationMethods, 152 + }, 153 + }), 154 + ); 155 + 156 + // Submit via the destination PDS 157 + await ok( 158 + destClient.post('com.atproto.identity.submitPlcOperation', { 159 + as: null, 160 + input: { 161 + operation: signage.operation, 162 + }, 163 + }), 164 + ); 165 + }, 166 + onSuccess() { 167 + setPlcToken(''); 168 + }, 169 + onError(err) { 170 + console.error(err); 171 + }, 172 + }); 173 + 174 + // Calculate rotation key counts 175 + const pdsKeyCount = () => loadCredentialsMutation.data?.rotationKeys?.length ?? 0; 176 + const totalKeyCount = () => { 177 + const custom = customKeys().filter((k) => k.trim()).length; 178 + const generated = useGeneratedKey() && generateKeyMutation.data ? 1 : 0; 179 + return pdsKeyCount() + custom + generated; 180 + }; 181 + const canAddCustomKey = () => totalKeyCount() < 5; 182 + const isOverLimit = () => totalKeyCount() > 5; 183 + 184 + const addCustomKey = () => { 185 + if (canAddCustomKey()) { 186 + setCustomKeys([...customKeys(), '']); 187 + } 188 + }; 189 + 190 + const removeCustomKey = (index: number) => { 191 + setCustomKeys(customKeys().filter((_, i) => i !== index)); 192 + }; 193 + 194 + const updateCustomKey = (index: number, value: string) => { 195 + setCustomKeys(customKeys().map((k, i) => (i === index ? value : k))); 196 + }; 197 + 198 + const canSignAndSubmit = () => { 199 + const src = source(); 200 + const dest = destination(); 201 + const creds = loadCredentialsMutation.data; 202 + const token = plcToken().trim(); 203 + 204 + return !!(src?.manager && dest?.manager && creds && token && !isOverLimit()); 205 + }; 206 + 207 + const handleSignAndSubmit = () => { 208 + const src = source(); 209 + const dest = destination(); 210 + const creds = loadCredentialsMutation.data; 211 + const token = plcToken().trim(); 212 + 213 + if (!src?.manager || !dest?.manager || !creds || !token || isOverLimit()) return; 214 + 215 + signAndSubmitMutation.mutate({ 216 + sourceManager: src.manager, 217 + destManager: dest.manager, 218 + token, 219 + credentials: creds, 220 + generatedKey: useGeneratedKey() ? generateKeyMutation.data : undefined, 221 + customKeys: customKeys(), 222 + }); 223 + }; 224 + 225 + const getSubmitErrorMessage = () => { 226 + const err = signAndSubmitMutation.error; 227 + if (err instanceof ClientResponseError) { 228 + if (err.error === 'InvalidToken' || err.error === 'ExpiredToken') { 229 + return 'Confirmation code has expired or is invalid'; 230 + } 231 + } 232 + return `${err}`; 233 + }; 234 + 235 + return ( 236 + <Accordion title="Identity (PLC)"> 237 + <div class="mb-4 rounded border border-yellow-300 bg-yellow-50 p-3"> 238 + <p class="text-sm font-medium text-yellow-800"> 239 + This updates your DID document to point to the new PDS. This is the critical step that makes the 240 + migration official. 241 + </p> 242 + </div> 243 + 244 + <Subsection title="1. Preview new credentials"> 245 + <p class="text-sm text-gray-600">View what your DID document will look like after the migration.</p> 246 + 247 + <Show 248 + when={destination()?.manager} 249 + fallback={<p class="text-sm text-gray-500">Sign in to destination account first.</p>} 250 + > 251 + {(manager) => ( 252 + <> 253 + <div class="flex items-center gap-3"> 254 + <Button 255 + variant="outline" 256 + onClick={() => loadCredentialsMutation.mutate({ manager: manager() })} 257 + disabled={loadCredentialsMutation.isPending} 258 + > 259 + {loadCredentialsMutation.isPending ? 'Loading...' : 'Load credentials'} 260 + </Button> 261 + 262 + <Show when={loadCredentialsMutation.isSuccess}> 263 + <StatusBadge variant="success">Loaded</StatusBadge> 264 + </Show> 265 + </div> 266 + 267 + <Show when={loadCredentialsMutation.isError}> 268 + <p class="text-sm text-red-600">{`${loadCredentialsMutation.error}`}</p> 269 + </Show> 270 + 271 + <Show when={loadCredentialsMutation.data}> 272 + {(creds) => ( 273 + <> 274 + <div class="mt-2 text-sm"> 275 + <p class="text-gray-500"> 276 + Destination PDS rotation keys ({creds().rotationKeys?.length ?? 0}/5): 277 + </p> 278 + <div class="mt-1 flex flex-col gap-1"> 279 + <For each={creds().rotationKeys ?? []}> 280 + {(key) => <code class="block truncate text-xs text-gray-700">{key}</code>} 281 + </For> 282 + </div> 283 + </div> 284 + 285 + <Show when={source()?.manager && source()}> 286 + {(src) => ( 287 + <div class="mt-3 rounded border border-blue-200 bg-blue-50 p-3"> 288 + <div class="flex items-center justify-between"> 289 + <p class="text-sm font-medium text-blue-800">Analyze existing rotation keys</p> 290 + <Button 291 + variant="outline" 292 + onClick={() => 293 + analyzeRotationKeysMutation.mutate({ 294 + did: src().did as Did<'plc'>, 295 + sourceManager: src().manager!, 296 + }) 297 + } 298 + disabled={analyzeRotationKeysMutation.isPending} 299 + > 300 + {analyzeRotationKeysMutation.isPending ? 'Analyzing...' : 'Analyze'} 301 + </Button> 302 + </div> 303 + <p class="mt-1 text-xs text-blue-600"> 304 + Check if you have any user-controlled rotation keys that should be preserved 305 + during migration. 306 + </p> 307 + 308 + <Show when={analyzeRotationKeysMutation.error}> 309 + <p class="mt-2 text-sm text-red-600">{`${analyzeRotationKeysMutation.error}`}</p> 310 + </Show> 311 + 312 + <Show when={analyzeRotationKeysMutation.data}> 313 + {(analysis) => ( 314 + <div class="mt-2 text-sm"> 315 + <Show 316 + when={analysis().userControlledKeys.length > 0} 317 + fallback={ 318 + <p class="text-blue-700"> 319 + No user-controlled rotation keys found. Your current keys are all 320 + managed by your source PDS. 321 + </p> 322 + } 323 + > 324 + <p class="font-medium text-blue-800"> 325 + Found {analysis().userControlledKeys.length} user-controlled key(s) to 326 + preserve: 327 + </p> 328 + <div class="mt-1 flex flex-col gap-1"> 329 + <For each={analysis().userControlledKeys}> 330 + {(key) => ( 331 + <code class="block truncate text-xs text-blue-700">{key}</code> 332 + )} 333 + </For> 334 + </div> 335 + <p class="mt-2 text-xs text-blue-600"> 336 + These keys have been added to the custom keys section below. 337 + </p> 338 + </Show> 339 + </div> 340 + )} 341 + </Show> 342 + </div> 343 + )} 344 + </Show> 345 + 346 + <details class="mt-2"> 347 + <summary class="cursor-pointer text-sm text-gray-600">View full credentials</summary> 348 + <pre class="mt-2 max-h-48 overflow-auto rounded border border-gray-200 bg-gray-50 p-2 font-mono text-xs"> 349 + {JSON.stringify(creds(), null, 2)} 350 + </pre> 351 + </details> 352 + </> 353 + )} 354 + </Show> 355 + </> 356 + )} 357 + </Show> 358 + </Subsection> 359 + 360 + <Subsection title="2. Rotation keys (optional)"> 361 + <p class="text-sm text-gray-600"> 362 + Add a rotation key to recover your account if your new PDS goes rogue. This will be prepended to the 363 + PDS rotation keys shown above. 364 + </p> 365 + 366 + <ToggleInput 367 + label="Generate a new rotation key" 368 + checked={useGeneratedKey()} 369 + onChange={(checked) => { 370 + setUseGeneratedKey(checked); 371 + // Auto-generate if checked and no key exists yet 372 + if (checked && !generateKeyMutation.data && !generateKeyMutation.isPending) { 373 + generateKeyMutation.mutate(); 374 + } 375 + }} 376 + /> 377 + 378 + <Show when={useGeneratedKey() && generateKeyMutation.isPending}> 379 + <p class="mt-2 text-sm text-gray-500">Generating key...</p> 380 + </Show> 381 + 382 + <Show when={useGeneratedKey() && generateKeyMutation.isError}> 383 + <p class="mt-2 text-sm text-red-600">{`${generateKeyMutation.error}`}</p> 384 + </Show> 385 + 386 + <Show when={useGeneratedKey() && generateKeyMutation.data}> 387 + {(keypair) => ( 388 + <div class="rounded border border-green-300 bg-green-50 p-3"> 389 + <p class="mb-2 text-sm font-semibold text-green-800">Save your rotation key private key!</p> 390 + <p class="mb-3 text-xs text-green-700"> 391 + Store this securely. You'll need it to recover your account if your PDS becomes unavailable or 392 + malicious. 393 + </p> 394 + 395 + <div class="flex flex-col gap-2 text-sm"> 396 + <div> 397 + <p class="font-medium text-gray-600">Public key (did:key)</p> 398 + <p class="break-all font-mono text-xs">{keypair().publicDidKey}</p> 399 + </div> 400 + <div> 401 + <p class="font-medium text-gray-600">Private key (hex)</p> 402 + <p class="break-all font-mono text-xs">{keypair().privateHex}</p> 403 + </div> 404 + <div> 405 + <p class="font-medium text-gray-600">Private key (multikey)</p> 406 + <p class="break-all font-mono text-xs">{keypair().privateMultikey}</p> 407 + </div> 408 + </div> 409 + </div> 410 + )} 411 + </Show> 412 + 413 + <div class="rounded border border-gray-200 bg-gray-50 p-3"> 414 + <p class="mb-2 text-sm font-medium text-gray-700">Custom rotation keys</p> 415 + <p class="mb-3 text-xs text-gray-500"> 416 + Add existing rotation keys (did:key format) you already control. 417 + </p> 418 + 419 + <Index each={customKeys()}> 420 + {(key, index) => ( 421 + <div class="mb-2 flex items-center gap-2"> 422 + <TextInput 423 + label="" 424 + placeholder="did:key:z..." 425 + monospace 426 + autocomplete="off" 427 + value={key()} 428 + onChange={(value) => updateCustomKey(index, value)} 429 + /> 430 + <button 431 + type="button" 432 + class="shrink-0 rounded px-2 py-1 text-sm text-red-600 hover:bg-red-50" 433 + onClick={() => removeCustomKey(index)} 434 + > 435 + Remove 436 + </button> 437 + </div> 438 + )} 439 + </Index> 440 + 441 + <Button variant="outline" onClick={addCustomKey} disabled={!canAddCustomKey()}> 442 + Add rotation key 443 + </Button> 444 + 445 + <Show when={isOverLimit()}> 446 + <p class="mt-2 text-sm text-red-600"> 447 + Too many rotation keys. PLC documents can only have up to 5 rotation keys total. 448 + </p> 449 + </Show> 450 + 451 + <p class="mt-2 text-xs text-gray-500"> 452 + Total keys: {totalKeyCount()}/5 (PDS: {pdsKeyCount()} 453 + {useGeneratedKey() && generateKeyMutation.data ? ', generated: 1' : ''} 454 + {customKeys().filter((k) => k.trim()).length > 0 455 + ? `, custom: ${customKeys().filter((k) => k.trim()).length}` 456 + : ''} 457 + ) 458 + </p> 459 + </div> 460 + </Subsection> 461 + 462 + <Subsection title="3. Request operation signature"> 463 + <p class="text-sm text-gray-600">Request a confirmation token via email from your source PDS.</p> 464 + 465 + <Show 466 + when={source()?.manager} 467 + fallback={<p class="text-sm text-gray-500">Sign in to source account first.</p>} 468 + > 469 + {(manager) => ( 470 + <> 471 + <div class="flex items-center gap-3"> 472 + <Button 473 + onClick={() => requestTokenMutation.mutate({ manager: manager() })} 474 + disabled={requestTokenMutation.isPending} 475 + > 476 + {requestTokenMutation.isPending ? 'Requesting...' : 'Request token'} 477 + </Button> 478 + 479 + <Show when={requestTokenMutation.isSuccess}> 480 + <StatusBadge variant="success">Email sent</StatusBadge> 481 + </Show> 482 + </div> 483 + 484 + <Show when={requestTokenMutation.isError}> 485 + <p class="text-sm text-red-600">{`${requestTokenMutation.error}`}</p> 486 + </Show> 487 + 488 + <Show when={requestTokenMutation.isSuccess}> 489 + <p class="text-sm text-gray-600">Check your email inbox for the confirmation code.</p> 490 + </Show> 491 + </> 492 + )} 493 + </Show> 494 + </Subsection> 495 + 496 + <Subsection title="4. Sign and submit"> 497 + <p class="text-sm text-gray-600">Enter the confirmation code and submit the PLC operation.</p> 498 + 499 + <Show when={!source()?.manager || !destination()?.manager}> 500 + <p class="text-sm text-gray-500">Sign in to both source and destination accounts first.</p> 501 + </Show> 502 + 503 + <Show when={!loadCredentialsMutation.data}> 504 + <p class="text-sm text-gray-500">Load credentials first.</p> 505 + </Show> 506 + 507 + <Show when={useGeneratedKey() && !generateKeyMutation.data}> 508 + <p class="text-sm text-gray-500">Generate your rotation key first.</p> 509 + </Show> 510 + 511 + <Show when={source()?.manager && destination()?.manager && loadCredentialsMutation.data}> 512 + <TextInput 513 + label="Confirmation code from email" 514 + type="text" 515 + autocomplete="one-time-code" 516 + pattern={TOTP_RE.source} 517 + placeholder="AAAAA-BBBBB" 518 + value={plcToken()} 519 + onChange={setPlcToken} 520 + monospace 521 + /> 522 + 523 + <div class="flex items-center gap-3"> 524 + <Button 525 + onClick={handleSignAndSubmit} 526 + disabled={signAndSubmitMutation.isPending || !canSignAndSubmit()} 527 + > 528 + {signAndSubmitMutation.isPending ? 'Submitting...' : 'Sign and submit'} 529 + </Button> 530 + 531 + <Show when={signAndSubmitMutation.isSuccess}> 532 + <StatusBadge variant="success">Identity updated successfully</StatusBadge> 533 + </Show> 534 + </div> 535 + 536 + <Show when={signAndSubmitMutation.isError}> 537 + <p class="text-sm text-red-600">{getSubmitErrorMessage()}</p> 538 + </Show> 539 + </Show> 540 + </Subsection> 541 + </Accordion> 542 + ); 543 + }; 544 + 545 + export default IdentitySection;
+180
src/views/account/account-migrate/sections/preferences.tsx
··· 1 + import { showSaveFilePicker } from 'native-file-system-adapter'; 2 + import { createSignal, Show } from 'solid-js'; 3 + 4 + import { Client, type CredentialManager, ok } from '@atcute/client'; 5 + 6 + import { createMutation } from '~/lib/utils/mutation'; 7 + 8 + import { Accordion, StatusBadge, Subsection } from '~/components/accordion'; 9 + import Button from '~/components/inputs/button'; 10 + import MultilineInput from '~/components/inputs/multiline-input'; 11 + 12 + import { useMigration } from '../context'; 13 + 14 + const PreferencesSection = () => { 15 + const { source, destination } = useMigration(); 16 + 17 + const [prefsInput, setPrefsInput] = createSignal(''); 18 + 19 + const exportMutation = createMutation({ 20 + async mutationFn({ sourceManager }: { sourceManager: CredentialManager }) { 21 + const sourceClient = new Client({ handler: sourceManager }); 22 + const prefs = await ok(sourceClient.get('app.bsky.actor.getPreferences', { params: {} })); 23 + return JSON.stringify(prefs, null, 2); 24 + }, 25 + onSuccess(json) { 26 + setPrefsInput(json); 27 + }, 28 + onError(err) { 29 + console.error(err); 30 + }, 31 + }); 32 + 33 + const downloadPrefs = async () => { 34 + const prefs = exportMutation.data; 35 + if (!prefs) return; 36 + 37 + try { 38 + const fd = await showSaveFilePicker({ 39 + suggestedName: `preferences-${source()?.did}-${new Date().toISOString()}.json`, 40 + // @ts-expect-error: ponyfill doesn't have the full typings 41 + id: 'prefs-export', 42 + startIn: 'downloads', 43 + types: [ 44 + { 45 + description: 'JSON file', 46 + accept: { 'application/json': ['.json'] }, 47 + }, 48 + ], 49 + }).catch((err) => { 50 + if (err instanceof DOMException && err.name === 'AbortError') { 51 + return undefined; 52 + } 53 + throw err; 54 + }); 55 + 56 + if (!fd) return; 57 + 58 + const writable = await fd.createWritable(); 59 + await writable.write(prefs); 60 + await writable.close(); 61 + } catch (err) { 62 + console.error(err); 63 + } 64 + }; 65 + 66 + const importMutation = createMutation({ 67 + async mutationFn({ destManager, input }: { destManager: CredentialManager; input: string }) { 68 + const prefs = JSON.parse(input); 69 + 70 + // Validate that it has a preferences array 71 + if (!prefs.preferences || !Array.isArray(prefs.preferences)) { 72 + throw new Error('Invalid preferences format: missing preferences array'); 73 + } 74 + 75 + const destClient = new Client({ handler: destManager }); 76 + await destClient.post('app.bsky.actor.putPreferences', { 77 + as: null, 78 + input: prefs, 79 + }); 80 + }, 81 + onError(err) { 82 + console.error(err); 83 + }, 84 + }); 85 + 86 + const getImportErrorMessage = () => { 87 + const err = importMutation.error; 88 + if (err instanceof SyntaxError) { 89 + return 'Invalid JSON format'; 90 + } 91 + return `${err}`; 92 + }; 93 + 94 + return ( 95 + <Accordion title="Preferences"> 96 + <Subsection title="Export from source"> 97 + <p class="text-sm text-gray-600"> 98 + Export your Bluesky preferences (muted words, content filters, saved feeds, etc). 99 + </p> 100 + 101 + <Show 102 + when={source()?.manager} 103 + fallback={<p class="text-sm text-gray-500">Sign in to source account first.</p>} 104 + > 105 + {(sourceManager) => ( 106 + <> 107 + <div class="flex items-center gap-3"> 108 + <Button 109 + onClick={() => exportMutation.mutate({ sourceManager: sourceManager() })} 110 + disabled={exportMutation.isPending} 111 + > 112 + {exportMutation.isPending ? 'Exporting...' : 'Export preferences'} 113 + </Button> 114 + 115 + <Show when={exportMutation.data}> 116 + <Button variant="secondary" onClick={downloadPrefs}> 117 + Download as file 118 + </Button> 119 + </Show> 120 + </div> 121 + 122 + <Show when={exportMutation.error}> 123 + {(err) => <p class="text-sm text-red-600">{`${err()}`}</p>} 124 + </Show> 125 + 126 + <Show when={exportMutation.data}> 127 + {(prefs) => ( 128 + <details class="mt-2"> 129 + <summary class="cursor-pointer text-sm text-gray-600"> 130 + View exported preferences 131 + </summary> 132 + <pre class="mt-2 max-h-48 overflow-auto rounded border border-gray-200 bg-gray-50 p-2 font-mono text-xs"> 133 + {prefs()} 134 + </pre> 135 + </details> 136 + )} 137 + </Show> 138 + </> 139 + )} 140 + </Show> 141 + </Subsection> 142 + 143 + <Subsection title="Import to destination"> 144 + <p class="text-sm text-gray-600">Paste preferences JSON or use the exported data above.</p> 145 + 146 + <Show 147 + when={destination()?.manager} 148 + fallback={<p class="text-sm text-gray-500">Sign in to destination account first.</p>} 149 + > 150 + {(destManager) => ( 151 + <> 152 + <MultilineInput label="Preferences JSON" value={prefsInput()} onChange={setPrefsInput} /> 153 + 154 + <div class="flex items-center gap-3"> 155 + <Button 156 + onClick={() => 157 + importMutation.mutate({ destManager: destManager(), input: prefsInput().trim() }) 158 + } 159 + disabled={importMutation.isPending || !prefsInput().trim()} 160 + > 161 + {importMutation.isPending ? 'Importing...' : 'Import preferences'} 162 + </Button> 163 + 164 + <Show when={importMutation.isSuccess}> 165 + <StatusBadge variant="success">Preferences imported successfully</StatusBadge> 166 + </Show> 167 + </div> 168 + 169 + <Show when={importMutation.error}> 170 + <p class="text-sm text-red-600">{getImportErrorMessage()}</p> 171 + </Show> 172 + </> 173 + )} 174 + </Show> 175 + </Subsection> 176 + </Accordion> 177 + ); 178 + }; 179 + 180 + export default PreferencesSection;
+291
src/views/account/account-migrate/sections/repository.tsx
··· 1 + import { showOpenFilePicker, showSaveFilePicker } from 'native-file-system-adapter'; 2 + import { createSignal, Show } from 'solid-js'; 3 + 4 + import { Client, type CredentialManager, ok, simpleFetchHandler } from '@atcute/client'; 5 + import type { Did } from '@atcute/lexicons/syntax'; 6 + 7 + import { formatBytes } from '~/lib/utils/intl/bytes'; 8 + import { createMutation } from '~/lib/utils/mutation'; 9 + import { iterateStream } from '~/lib/utils/stream'; 10 + 11 + import { Accordion, StatusBadge, Subsection } from '~/components/accordion'; 12 + import Button from '~/components/inputs/button'; 13 + 14 + import { useMigration } from '../context'; 15 + 16 + const RepositorySection = () => { 17 + const { source, destination } = useMigration(); 18 + 19 + // Export state 20 + const [exportStatus, setExportStatus] = createSignal<string>(); 21 + 22 + // Import state 23 + const [importStatus, setImportStatus] = createSignal<string>(); 24 + const [importedRecords, setImportedRecords] = createSignal<number>(); 25 + 26 + const exportMutation = createMutation({ 27 + async mutationFn({ pdsUrl, did }: { pdsUrl: string; did: Did }) { 28 + setExportStatus('Waiting for file picker...'); 29 + 30 + const fd = await showSaveFilePicker({ 31 + suggestedName: `repo-${did}-${new Date().toISOString()}.car`, 32 + // @ts-expect-error: ponyfill doesn't have the full typings 33 + id: 'repo-export', 34 + startIn: 'downloads', 35 + types: [ 36 + { 37 + description: 'CAR archive file', 38 + accept: { 'application/vnd.ipld.car': ['.car'] }, 39 + }, 40 + ], 41 + }).catch((err) => { 42 + if (err instanceof DOMException && err.name === 'AbortError') { 43 + return undefined; 44 + } 45 + throw err; 46 + }); 47 + 48 + if (!fd) { 49 + setExportStatus(); 50 + return null; 51 + } 52 + 53 + const writable = await fd.createWritable(); 54 + 55 + setExportStatus('Downloading repository...'); 56 + 57 + const sourceClient = new Client({ handler: simpleFetchHandler({ service: pdsUrl }) }); 58 + const response = await sourceClient.get('com.atproto.sync.getRepo', { 59 + as: 'stream', 60 + params: { did }, 61 + }); 62 + 63 + if (!response.ok) { 64 + throw new Error(`Failed to download repository: ${response.status}`); 65 + } 66 + 67 + let size = 0; 68 + for await (const chunk of iterateStream(response.data)) { 69 + size += chunk.length; 70 + await writable.write(chunk); 71 + setExportStatus(`Downloading repository... (${formatBytes(size)})`); 72 + } 73 + 74 + await writable.close(); 75 + setExportStatus(`Exported ${formatBytes(size)}`); 76 + return size; 77 + }, 78 + onMutate() { 79 + setExportStatus(); 80 + }, 81 + onError(err) { 82 + console.error(err); 83 + setExportStatus(); 84 + }, 85 + }); 86 + 87 + const importFromFileMutation = createMutation({ 88 + async mutationFn({ manager }: { manager: CredentialManager }) { 89 + setImportStatus('Waiting for file picker...'); 90 + 91 + const [fd] = await showOpenFilePicker({ 92 + // @ts-expect-error: ponyfill doesn't have the full typings 93 + id: 'repo-import', 94 + types: [ 95 + { 96 + description: 'CAR archive file', 97 + accept: { 'application/vnd.ipld.car': ['.car'] }, 98 + }, 99 + ], 100 + }).catch((err) => { 101 + if (err instanceof DOMException && err.name === 'AbortError') { 102 + return [undefined]; 103 + } 104 + throw err; 105 + }); 106 + 107 + if (!fd) { 108 + setImportStatus(); 109 + return null; 110 + } 111 + 112 + const file = await fd.getFile(); 113 + 114 + setImportStatus(`Uploading repository (${formatBytes(file.size)})...`); 115 + 116 + const destClient = new Client({ handler: manager }); 117 + const importResp = await destClient.post('com.atproto.repo.importRepo', { 118 + as: null, 119 + input: file, 120 + headers: { 121 + 'content-type': 'application/vnd.ipld.car', 122 + }, 123 + }); 124 + 125 + if (!importResp.ok) { 126 + throw new Error(`Failed to import repository: ${importResp.status}`); 127 + } 128 + 129 + // Check account status to get record count 130 + const status = await ok(destClient.get('com.atproto.server.checkAccountStatus', {})); 131 + setImportedRecords(status.indexedRecords); 132 + 133 + setImportStatus(`Imported successfully`); 134 + return status.indexedRecords; 135 + }, 136 + onMutate() { 137 + setImportStatus(); 138 + setImportedRecords(); 139 + }, 140 + onError(err) { 141 + console.error(err); 142 + setImportStatus(); 143 + }, 144 + }); 145 + 146 + const importFromSourceMutation = createMutation({ 147 + async mutationFn({ 148 + sourcePdsUrl, 149 + sourceDid, 150 + destManager, 151 + }: { 152 + sourcePdsUrl: string; 153 + sourceDid: Did; 154 + destManager: CredentialManager; 155 + }) { 156 + setImportStatus('Downloading from source PDS...'); 157 + 158 + const sourceClient = new Client({ handler: simpleFetchHandler({ service: sourcePdsUrl }) }); 159 + const response = await sourceClient.get('com.atproto.sync.getRepo', { 160 + as: 'bytes', 161 + params: { did: sourceDid }, 162 + }); 163 + 164 + if (!response.ok) { 165 + throw new Error(`Failed to download repository: ${response.status}`); 166 + } 167 + 168 + setImportStatus(`Uploading to destination (${formatBytes(response.data.length)})...`); 169 + 170 + const destClient = new Client({ handler: destManager }); 171 + const importResp = await destClient.post('com.atproto.repo.importRepo', { 172 + as: null, 173 + input: response.data, 174 + headers: { 175 + 'content-type': 'application/vnd.ipld.car', 176 + }, 177 + }); 178 + 179 + if (!importResp.ok) { 180 + throw new Error(`Failed to import repository: ${importResp.status}`); 181 + } 182 + 183 + // Check account status to get record count 184 + const status = await ok(destClient.get('com.atproto.server.checkAccountStatus', {})); 185 + setImportedRecords(status.indexedRecords); 186 + 187 + setImportStatus(`Imported successfully`); 188 + return status.indexedRecords; 189 + }, 190 + onMutate() { 191 + setImportStatus(); 192 + setImportedRecords(); 193 + }, 194 + onError(err) { 195 + console.error(err); 196 + setImportStatus(); 197 + }, 198 + }); 199 + 200 + const isExporting = () => exportMutation.isPending; 201 + const isImporting = () => importFromFileMutation.isPending || importFromSourceMutation.isPending; 202 + 203 + return ( 204 + <Accordion title="Repository"> 205 + <Subsection title="Export from source"> 206 + <p class="text-sm text-gray-600"> 207 + Download the repository as a CAR file for backup or manual import. 208 + </p> 209 + 210 + <Show when={source()} fallback={<p class="text-sm text-gray-500">Resolve source account first.</p>}> 211 + {(src) => ( 212 + <> 213 + <div class="flex items-center gap-3"> 214 + <Button 215 + onClick={() => exportMutation.mutate({ pdsUrl: src().pdsUrl, did: src().did })} 216 + disabled={isExporting()} 217 + > 218 + {isExporting() ? 'Exporting...' : 'Export to file'} 219 + </Button> 220 + <Show when={exportStatus()}> 221 + <span class="text-sm text-gray-600">{exportStatus()}</span> 222 + </Show> 223 + </div> 224 + 225 + <Show when={exportMutation.isError}> 226 + <p class="text-sm text-red-600">{`${exportMutation.error}`}</p> 227 + </Show> 228 + </> 229 + )} 230 + </Show> 231 + </Subsection> 232 + 233 + <Subsection title="Import to destination"> 234 + <p class="text-sm text-gray-600">Upload a repository CAR file or transfer directly from source.</p> 235 + 236 + <Show 237 + when={destination()?.manager} 238 + fallback={<p class="text-sm text-gray-500">Sign in to destination account first.</p>} 239 + > 240 + {(manager) => ( 241 + <> 242 + <div class="flex flex-wrap items-center gap-3"> 243 + <Button 244 + onClick={() => importFromFileMutation.mutate({ manager: manager() })} 245 + disabled={isImporting()} 246 + > 247 + {isImporting() ? 'Importing...' : 'Import from file'} 248 + </Button> 249 + 250 + <Show when={source()}> 251 + {(src) => ( 252 + <Button 253 + variant="secondary" 254 + onClick={() => 255 + importFromSourceMutation.mutate({ 256 + sourcePdsUrl: src().pdsUrl, 257 + sourceDid: src().did, 258 + destManager: manager(), 259 + }) 260 + } 261 + disabled={isImporting()} 262 + > 263 + Transfer from source 264 + </Button> 265 + )} 266 + </Show> 267 + </div> 268 + 269 + <Show when={importStatus()}> 270 + <div class="flex items-center gap-2"> 271 + <span class="text-sm text-gray-600">{importStatus()}</span> 272 + <Show when={importedRecords() !== undefined}> 273 + <StatusBadge variant="success">{importedRecords()} records</StatusBadge> 274 + </Show> 275 + </div> 276 + </Show> 277 + 278 + <Show when={importFromFileMutation.isError || importFromSourceMutation.isError}> 279 + <p class="text-sm text-red-600"> 280 + {`${importFromFileMutation.error || importFromSourceMutation.error}`} 281 + </p> 282 + </Show> 283 + </> 284 + )} 285 + </Show> 286 + </Subsection> 287 + </Accordion> 288 + ); 289 + }; 290 + 291 + export default RepositorySection;
+265
src/views/account/account-migrate/sections/source-account.tsx
··· 1 + import { createSignal, Show } from 'solid-js'; 2 + 3 + import { type AtpAccessJwt, ClientResponseError, CredentialManager } from '@atcute/client'; 4 + import { getPdsEndpoint, isAtprotoDid } from '@atcute/identity'; 5 + import { isHandle, type AtprotoDid } from '@atcute/lexicons/syntax'; 6 + 7 + import { getDidDocument } from '~/api/queries/did-doc'; 8 + import { resolveHandleViaAppView } from '~/api/queries/handle'; 9 + import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 10 + import { decodeJwt } from '~/api/utils/jwt'; 11 + 12 + import { createMutation } from '~/lib/utils/mutation'; 13 + 14 + import { Accordion, StatusBadge, Subsection } from '~/components/accordion'; 15 + import Button from '~/components/inputs/button'; 16 + import TextInput from '~/components/inputs/text-input'; 17 + 18 + import { useMigration } from '../context'; 19 + 20 + interface SourceAccountSectionProps { 21 + onStarted?: () => void; 22 + } 23 + 24 + class InsufficientLoginError extends Error {} 25 + 26 + const SourceAccountSection = (props: SourceAccountSectionProps) => { 27 + const { source, setSource } = useMigration(); 28 + 29 + // Resolve state 30 + const [identifier, setIdentifier] = createSignal(''); 31 + const [resolveError, setResolveError] = createSignal<string>(); 32 + 33 + // Auth state 34 + const [password, setPassword] = createSignal(''); 35 + const [otp, setOtp] = createSignal(''); 36 + const [isTotpRequired, setIsTotpRequired] = createSignal(false); 37 + const [authError, setAuthError] = createSignal<string>(); 38 + 39 + const resolveMutation = createMutation({ 40 + async mutationFn({ identifier }: { identifier: string }) { 41 + let did: AtprotoDid; 42 + if (isAtprotoDid(identifier)) { 43 + did = identifier; 44 + } else if (isHandle(identifier)) { 45 + did = await resolveHandleViaAppView({ handle: identifier }); 46 + } else { 47 + throw new Error(`${identifier} is not a valid DID or handle`); 48 + } 49 + 50 + const didDoc = await getDidDocument({ did }); 51 + const pdsUrl = getPdsEndpoint(didDoc); 52 + 53 + if (!pdsUrl) { 54 + throw new Error(`No PDS endpoint found in DID document`); 55 + } 56 + 57 + return { did, didDoc, pdsUrl }; 58 + }, 59 + onMutate() { 60 + setResolveError(); 61 + }, 62 + onSuccess({ did, didDoc, pdsUrl }) { 63 + setSource({ did, didDoc, pdsUrl, manager: null }); 64 + props.onStarted?.(); 65 + }, 66 + onError(err) { 67 + if (err instanceof ClientResponseError) { 68 + if (err.error === 'InvalidRequest' && err.description?.includes('resolve handle')) { 69 + setResolveError(`Can't resolve handle, is it typed correctly?`); 70 + return; 71 + } 72 + } 73 + console.error(err); 74 + setResolveError(`${err}`); 75 + }, 76 + }); 77 + 78 + const authMutation = createMutation({ 79 + async mutationFn({ pdsUrl, did, password, otp }: { pdsUrl: string; did: string; password: string; otp: string }) { 80 + const manager = new CredentialManager({ service: pdsUrl }); 81 + const session = await manager.login({ 82 + identifier: did, 83 + password: password, 84 + code: formatTotpCode(otp), 85 + }); 86 + 87 + const decoded = decodeJwt(session.accessJwt) as AtpAccessJwt; 88 + if (decoded.scope !== 'com.atproto.access') { 89 + throw new InsufficientLoginError(`You need to sign in with a main password, not an app password`); 90 + } 91 + 92 + return manager; 93 + }, 94 + onMutate() { 95 + setAuthError(); 96 + }, 97 + onSuccess(manager) { 98 + setSource({ ...source()!, manager }); 99 + setPassword(''); 100 + setOtp(''); 101 + setIsTotpRequired(false); 102 + }, 103 + onError(err) { 104 + if (err instanceof ClientResponseError) { 105 + if (err.error === 'AuthFactorTokenRequired') { 106 + setOtp(''); 107 + setIsTotpRequired(true); 108 + return; 109 + } 110 + if (err.error === 'AuthenticationRequired') { 111 + setAuthError(`Invalid identifier or password`); 112 + return; 113 + } 114 + if (err.error === 'AccountTakedown') { 115 + setAuthError(`Account has been taken down`); 116 + return; 117 + } 118 + if (err.description?.includes('Token is invalid')) { 119 + setAuthError(`Invalid one-time confirmation code`); 120 + setIsTotpRequired(true); 121 + return; 122 + } 123 + } 124 + if (err instanceof InsufficientLoginError) { 125 + setAuthError(err.message); 126 + return; 127 + } 128 + console.error(err); 129 + setAuthError(`${err}`); 130 + }, 131 + }); 132 + 133 + const isResolved = () => source() !== null; 134 + const isAuthenticated = () => source()?.manager != null; 135 + 136 + return ( 137 + <Accordion title="Source Account" defaultOpen> 138 + <Subsection title="Resolve identity"> 139 + <Show when={!isResolved()}> 140 + <form 141 + onSubmit={(ev) => { 142 + ev.preventDefault(); 143 + resolveMutation.mutate({ identifier: identifier() }); 144 + }} 145 + class="flex flex-col gap-3" 146 + > 147 + <TextInput 148 + label="Handle or DID" 149 + placeholder="alice.bsky.social" 150 + value={identifier()} 151 + required 152 + autofocus 153 + onChange={setIdentifier} 154 + /> 155 + 156 + <Show when={resolveError()}> 157 + <p class="text-sm text-red-600">{resolveError()}</p> 158 + </Show> 159 + 160 + <div> 161 + <Button type="submit" disabled={resolveMutation.isPending}> 162 + {resolveMutation.isPending ? 'Resolving...' : 'Resolve'} 163 + </Button> 164 + </div> 165 + </form> 166 + </Show> 167 + 168 + <Show when={isResolved()}> 169 + <div class="flex flex-col gap-2 text-sm"> 170 + <p> 171 + <span class="text-gray-500">DID:</span>{' '} 172 + <span class="font-mono">{source()!.did}</span> 173 + </p> 174 + <p> 175 + <span class="text-gray-500">PDS:</span>{' '} 176 + <span class="font-mono">{source()!.pdsUrl}</span> 177 + </p> 178 + <div class="mt-1"> 179 + <button 180 + type="button" 181 + onClick={() => setSource(null)} 182 + class="text-sm text-purple-800 hover:underline" 183 + > 184 + Change account 185 + </button> 186 + </div> 187 + </div> 188 + </Show> 189 + </Subsection> 190 + 191 + <Show when={isResolved()}> 192 + <Subsection title="Authenticate"> 193 + <p class="text-sm text-gray-600"> 194 + Authentication is required for some operations like exporting preferences or signing PLC operations. 195 + </p> 196 + 197 + <Show when={!isAuthenticated()}> 198 + <form 199 + onSubmit={(ev) => { 200 + ev.preventDefault(); 201 + const src = source()!; 202 + authMutation.mutate({ 203 + pdsUrl: src.pdsUrl, 204 + did: src.did, 205 + password: password(), 206 + otp: otp(), 207 + }); 208 + }} 209 + class="flex flex-col gap-3" 210 + > 211 + <TextInput 212 + label="Main password" 213 + blurb="Your credentials stay entirely within your browser." 214 + type="password" 215 + value={password()} 216 + required 217 + onChange={setPassword} 218 + /> 219 + 220 + <Show when={isTotpRequired()}> 221 + <TextInput 222 + label="One-time confirmation code" 223 + blurb="A code has been sent to your email address." 224 + type="text" 225 + autocomplete="one-time-code" 226 + pattern={TOTP_RE.source} 227 + placeholder="AAAAA-BBBBB" 228 + value={otp()} 229 + required 230 + onChange={setOtp} 231 + monospace 232 + /> 233 + </Show> 234 + 235 + <Show when={authError()}> 236 + <p class="text-sm text-red-600">{authError()}</p> 237 + </Show> 238 + 239 + <div> 240 + <Button type="submit" disabled={authMutation.isPending}> 241 + {authMutation.isPending ? 'Signing in...' : 'Sign in'} 242 + </Button> 243 + </div> 244 + </form> 245 + </Show> 246 + 247 + <Show when={isAuthenticated()}> 248 + <div class="flex items-center gap-2"> 249 + <StatusBadge variant="success">Signed in</StatusBadge> 250 + <button 251 + type="button" 252 + onClick={() => setSource({ ...source()!, manager: null })} 253 + class="text-sm text-purple-800 hover:underline" 254 + > 255 + Sign out 256 + </button> 257 + </div> 258 + </Show> 259 + </Subsection> 260 + </Show> 261 + </Accordion> 262 + ); 263 + }; 264 + 265 + export default SourceAccountSection;
+58 -41
src/views/blob/blob-export.tsx
··· 1 1 import { FileSystemWritableFileStream, showSaveFilePicker } from 'native-file-system-adapter'; 2 2 import { createSignal } from 'solid-js'; 3 3 4 - import { simpleFetchHandler, XRPC, XRPCError } from '@atcute/client'; 5 - import { At } from '@atcute/client/lexicons'; 4 + import { Client, ClientResponseError, ok, simpleFetchHandler } from '@atcute/client'; 5 + import { getPdsEndpoint, isAtprotoDid } from '@atcute/identity'; 6 + import { isHandle, type AtprotoDid } from '@atcute/lexicons/syntax'; 6 7 import { writeTarEntry } from '@mary/tar'; 7 8 8 9 import { getDidDocument } from '~/api/queries/did-doc'; 9 10 import { resolveHandleViaAppView, resolveHandleViaPds } from '~/api/queries/handle'; 10 - import { getPdsEndpoint } from '~/api/types/did-doc'; 11 11 import { isServiceUrlString } from '~/api/types/strings'; 12 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 13 12 14 13 import { useTitle } from '~/lib/navigation/router'; 15 14 import { makeAbortable } from '~/lib/utils/abortable'; ··· 18 17 import Button from '~/components/inputs/button'; 19 18 import TextInput from '~/components/inputs/text-input'; 20 19 import Logger, { createLogger } from '~/components/logger'; 20 + import PageHeader from '~/components/page-header'; 21 21 22 22 const BlobExportPage = () => { 23 23 const logger = createLogger(); ··· 36 36 }) => { 37 37 logger.info(`Starting export for ${identifier}`); 38 38 39 - let did: At.DID; 40 - if (isDid(identifier)) { 39 + let did: AtprotoDid; 40 + if (isAtprotoDid(identifier)) { 41 41 did = identifier; 42 - } else if (service) { 43 - did = await resolveHandleViaPds({ service, handle: identifier, signal }); 44 - logger.log(`Resolved handle to ${did}`); 42 + } else if (isHandle(identifier)) { 43 + if (service) { 44 + did = await resolveHandleViaPds({ service, handle: identifier, signal }); 45 + logger.log(`Resolved handle to ${did}`); 46 + } else { 47 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 48 + logger.log(`Resolved handle to ${did}`); 49 + } 45 50 } else { 46 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 47 - logger.log(`Resolved handle to ${did}`); 51 + logger.error(`Invalid identifier`); 52 + return; 48 53 } 49 54 50 55 if (!service) { ··· 61 66 service = endpoint; 62 67 } 63 68 64 - const rpc = new XRPC({ handler: simpleFetchHandler({ service }) }); 69 + // const rpc = new XRPC({ handler: simpleFetchHandler({ service }) }); 70 + const client = new Client({ handler: simpleFetchHandler({ service }) }); 65 71 66 72 // Grab a list of blobs 67 73 let blobs: string[] = []; ··· 70 76 71 77 let cursor: string | undefined; 72 78 do { 73 - const { data } = await rpc.get('com.atproto.sync.listBlobs', { 74 - signal, 75 - params: { did, cursor, limit: 1_000 }, 76 - }); 79 + const data = await ok( 80 + client.get('com.atproto.sync.listBlobs', { 81 + signal, 82 + params: { did, cursor, limit: 1_000 }, 83 + }), 84 + ); 77 85 78 86 cursor = data.cursor; 79 87 blobs = blobs.concat(data.cids); ··· 147 155 attempts++; 148 156 149 157 try { 150 - const { data } = await rpc.get('com.atproto.sync.getBlob', { 158 + const response = await client.get('com.atproto.sync.getBlob', { 151 159 signal, 160 + as: 'bytes', 152 161 params: { did, cid }, 153 162 }); 154 163 155 - return data; 156 - } catch (err) { 157 - if (attempts > 3) { 158 - throw err; 164 + if (response.ok) { 165 + return response.data; 159 166 } 160 167 161 - if (err instanceof XRPCError) { 162 - if (err.status === 400) { 163 - if (err.message === 'Blob not found') { 164 - console.warn(`Blob ${cid} not found`); 165 - return; 166 - } 167 - } else if (err.status === 429) { 168 - const reset = err.headers?.['ratelimit-reset']; 168 + if (response.status === 400) { 169 + // If the PDS says it can't find the blob, stop right here. 170 + if (response.data.message === 'Blob not found') { 171 + logger.warn(`Blob ${cid} not found`); 172 + return undefined; 173 + } 174 + } else if (response.status === 429) { 175 + // Not exposed by CORS, hoping that someday it will 176 + const reset = response.headers.get('ratelimit-reset'); 169 177 170 - if (reset !== undefined) { 171 - logger.warn(`Ratelimit exceeded when downloading ${cid}, waiting`); 178 + logger.warn(`Ratelimit exceeded when downloading ${cid}, waiting`); 172 179 173 - const refreshAt = +reset * 1_000; 174 - const delta = refreshAt - Date.now(); 180 + if (reset !== null) { 181 + const refreshAt = +reset * 1_000; 182 + const delta = refreshAt - Date.now(); 175 183 176 - await sleep(delta); 177 - } 184 + await sleep(delta); 185 + } else { 186 + await sleep(10_000); 178 187 } 179 188 } 189 + 190 + if (attempts < 3) { 191 + continue; 192 + } 193 + 194 + throw new ClientResponseError(response); 195 + } catch (err) { 196 + // Network errors, etc 197 + if (attempts < 3) { 198 + continue; 199 + } 200 + 201 + throw err; 180 202 } 181 203 } 182 204 }; ··· 208 230 209 231 return ( 210 232 <> 211 - <div class="p-4"> 212 - <h1 class="text-lg font-bold text-purple-800">Export blobs</h1> 213 - <p class="text-gray-600">Download all blobs from an account into a tarball</p> 214 - </div> 215 - <hr class="mx-4 border-gray-300" /> 233 + <PageHeader title="Export blobs" subtitle="Download all blobs from an account into a tarball" /> 216 234 217 235 <form 218 236 onSubmit={(ev) => { ··· 263 281 type="text" 264 282 name="ident" 265 283 autocomplete="username" 266 - pattern={/* @once */ DID_OR_HANDLE_RE.source} 267 284 placeholder="paul.bsky.social" 268 285 autofocus 269 286 />
+10 -11
src/views/bluesky/threadgate-applicator/page.tsx
··· 1 1 import { createEffect, createSignal, onCleanup } from 'solid-js'; 2 2 3 + import { AppBskyFeedDefs, AppBskyFeedThreadgate } from '@atcute/bluesky'; 3 4 import { CredentialManager } from '@atcute/client'; 4 - import { AppBskyFeedDefs, AppBskyFeedThreadgate } from '@atcute/client/lexicons'; 5 + import type { DidDocument } from '@atcute/identity'; 5 6 6 - import { DidDocument } from '~/api/types/did-doc'; 7 - import { UnwrapArray } from '~/api/utils/types'; 7 + import type { UnwrapArray } from '~/api/utils/types'; 8 8 9 9 import { history } from '~/globals/navigation'; 10 10 11 11 import { useTitle } from '~/lib/navigation/router'; 12 12 13 + import PageHeader from '~/components/page-header'; 13 14 import { Wizard } from '~/components/wizard'; 14 15 15 16 import Step1_HandleInput from './steps/step1_handle-input'; ··· 18 19 import Step4_Confirmation from './steps/step4_confirmation'; 19 20 import Step5_Finished from './steps/step5_finished'; 20 21 21 - export interface ThreadgateState 22 - extends Pick<AppBskyFeedThreadgate.Record, 'allow' | 'hiddenReplies' | 'createdAt'> { 22 + export interface ThreadgateState extends Pick< 23 + AppBskyFeedThreadgate.Main, 24 + 'allow' | 'hiddenReplies' | 'createdAt' 25 + > { 23 26 uri: string; 24 27 } 25 28 26 - export type ThreadgateRule = UnwrapArray<AppBskyFeedThreadgate.Record['allow']>; 29 + export type ThreadgateRule = UnwrapArray<AppBskyFeedThreadgate.Main['allow']>; 27 30 28 31 export interface ThreadItem { 29 32 post: AppBskyFeedDefs.PostView; ··· 78 81 79 82 return ( 80 83 <> 81 - <div class="p-4"> 82 - <h1 class="text-lg font-bold text-purple-800">Retroactive thread gating</h1> 83 - <p class="text-gray-600">Set reply permissions on all of your past Bluesky posts</p> 84 - </div> 85 - <hr class="mx-4 border-gray-300" /> 84 + <PageHeader title="Retroactive thread gating" subtitle="Set reply permissions on all of your past Bluesky posts" /> 86 85 87 86 <Wizard<ThreadgateApplicatorConstraints> 88 87 initialStep="Step1_HandleInput"
+23 -18
src/views/bluesky/threadgate-applicator/steps/step1_handle-input.tsx
··· 1 1 import { createSignal } from 'solid-js'; 2 2 3 - import type { AppBskyFeedThreadgate, At } from '@atcute/client/lexicons'; 3 + import type { AppBskyFeedThreadgate } from '@atcute/bluesky'; 4 + import { ok } from '@atcute/client'; 5 + import { isAtprotoDid } from '@atcute/identity'; 6 + import { isHandle, type AtprotoDid } from '@atcute/lexicons/syntax'; 4 7 5 8 import { getDidDocument } from '~/api/queries/did-doc'; 6 9 import { resolveHandleViaAppView } from '~/api/queries/handle'; 7 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 8 10 9 11 import { appViewRpc } from '~/globals/rpc'; 10 12 ··· 12 14 13 15 import Button from '~/components/inputs/button'; 14 16 import TextInput from '~/components/inputs/text-input'; 15 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 17 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 16 18 17 - import { ThreadgateApplicatorConstraints, ThreadgateState, ThreadItem } from '../page'; 19 + import type { ThreadgateApplicatorConstraints, ThreadgateState, ThreadItem } from '../page'; 18 20 import { sortThreadgateState } from '../utils'; 19 21 20 22 class NoThreadsError extends Error {} ··· 32 34 async mutationFn({ identifier }: { identifier: string }, signal) { 33 35 setStatus(`Resolving identity`); 34 36 35 - let did: At.DID; 36 - if (isDid(identifier)) { 37 + let did: AtprotoDid; 38 + if (isAtprotoDid(identifier)) { 37 39 did = identifier; 40 + } else if (isHandle(identifier)) { 41 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 38 42 } else { 39 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 43 + throw new Error(`Invalid identifier`); 40 44 } 41 45 42 46 const didDoc = await getDidDocument({ did, signal }); ··· 47 51 48 52 let cursor: string | undefined; 49 53 do { 50 - const { data } = await appViewRpc.get('app.bsky.feed.getAuthorFeed', { 51 - signal, 52 - params: { 53 - actor: did, 54 - filter: 'posts_no_replies', 55 - limit: 100, 56 - cursor, 57 - }, 58 - }); 54 + const data = await ok( 55 + appViewRpc.get('app.bsky.feed.getAuthorFeed', { 56 + signal, 57 + params: { 58 + actor: did, 59 + filter: 'posts_no_replies', 60 + limit: 100, 61 + cursor, 62 + }, 63 + }), 64 + ); 59 65 60 66 cursor = data.cursor; 61 67 ··· 80 86 let threadgate: ThreadgateState | null = null; 81 87 82 88 if (tg?.record) { 83 - const record = tg.record as AppBskyFeedThreadgate.Record; 89 + const record = tg.record as AppBskyFeedThreadgate.Main; 84 90 85 91 const allow = record?.allow; 86 92 const hiddenReplies = record?.hiddenReplies; ··· 150 156 placeholder="paul.bsky.social" 151 157 value={identifier()} 152 158 required 153 - pattern={/* @once */ DID_OR_HANDLE_RE.source} 154 159 autofocus={isActive()} 155 160 onChange={setIdentifier} 156 161 />
+19 -26
src/views/bluesky/threadgate-applicator/steps/step2_rules-input.tsx
··· 1 1 import { batch, createMemo, createSignal, For, Show } from 'solid-js'; 2 2 3 - import { AppBskyFeedThreadgate, Brand } from '@atcute/client/lexicons'; 4 - 5 - import { UnwrapArray } from '~/api/utils/types'; 3 + import { AppBskyFeedThreadgate } from '@atcute/bluesky'; 4 + import { ok } from '@atcute/client'; 5 + import type { $type } from '@atcute/lexicons'; 6 6 7 7 import { appViewRpc } from '~/globals/rpc'; 8 8 ··· 11 11 import { createQuery } from '~/lib/utils/query'; 12 12 13 13 import RadioInput from '~/components/inputs/radio-input'; 14 - import { Stage, StageActions, WizardStepProps } from '~/components/wizard'; 14 + import { Stage, StageActions, type WizardStepProps } from '~/components/wizard'; 15 15 16 16 import CircularProgressView from '~/components/circular-progress-view'; 17 17 import Button from '~/components/inputs/button'; 18 18 import ToggleInput from '~/components/inputs/toggle-input'; 19 19 20 - import { ThreadgateApplicatorConstraints } from '../page'; 20 + import type { ThreadgateApplicatorConstraints, ThreadgateRule } from '../page'; 21 21 import { sortThreadgateAllow } from '../utils'; 22 22 23 23 const enum FilterType { ··· 30 30 NO_ONE = 'no_one', 31 31 CUSTOM = 'custom', 32 32 } 33 - 34 - type ThreadRule = UnwrapArray<AppBskyFeedThreadgate.Record['allow']>; 35 33 36 34 const Step2_RulesInput = ({ 37 35 data, ··· 41 39 }: WizardStepProps<ThreadgateApplicatorConstraints, 'Step2_RulesInput'>) => { 42 40 const [filter, setFilter] = createSignal(FilterType.MISSING_ONLY); 43 41 44 - const [threadRules, _setThreadRules] = createSignal<ThreadRule[] | undefined>([ 42 + const [threadRules, _setThreadRules] = createSignal<ThreadgateRule[] | undefined>([ 45 43 { $type: 'app.bsky.feed.threadgate#followingRule' }, 46 44 { $type: 'app.bsky.feed.threadgate#mentionRule' }, 47 45 ]); ··· 64 62 () => data.profile.didDoc.id, 65 63 async (did, signal) => { 66 64 const lists = await accumulate(async (cursor) => { 67 - const { data } = await appViewRpc.get('app.bsky.graph.getLists', { 68 - signal, 69 - params: { 70 - actor: did, 71 - cursor, 72 - limit: 100, 73 - }, 74 - }); 65 + const data = await ok( 66 + appViewRpc.get('app.bsky.graph.getLists', { 67 + signal, 68 + params: { 69 + actor: did, 70 + cursor, 71 + limit: 100, 72 + }, 73 + }), 74 + ); 75 75 76 76 return { 77 77 cursor: data.cursor, ··· 121 121 ); 122 122 }); 123 123 124 - const hasThreadRule = (predicate: ThreadRule): boolean => { 124 + const hasThreadRule = (predicate: ThreadgateRule): boolean => { 125 125 return !!threadRules()?.find((rule) => dequal(rule, predicate)); 126 126 }; 127 127 128 - const setCustomThreadRules = (next: ThreadRule[] | undefined) => { 128 + const setCustomThreadRules = (next: ThreadgateRule[] | undefined) => { 129 129 batch(() => { 130 130 _setThreadRules(next); 131 131 setThreadRulesPreset(ThreadRulePreset.CUSTOM); ··· 226 226 } 227 227 > 228 228 {(list) => { 229 - const rule: Brand.Union<AppBskyFeedThreadgate.ListRule> = { 229 + const rule: $type.enforce<AppBskyFeedThreadgate.ListRule> = { 230 230 $type: 'app.bsky.feed.threadgate#listRule', 231 231 list: list.uri, 232 232 }; ··· 255 255 blurb={ 256 256 <> 257 257 <span>This will apply to {filteredThreads().length} threads. </span> 258 - {/* <button 259 - type="button" 260 - hidden={filteredThreads().length < 1} 261 - class="font-medium text-purple-800 hover:underline" 262 - > 263 - View 264 - </button> */} 265 258 </> 266 259 } 267 260 value={filter()}
+2 -2
src/views/bluesky/threadgate-applicator/steps/step3_authentication.tsx
··· 2 2 3 3 import { CredentialManager } from '@atcute/client'; 4 4 5 - import { WizardStepProps } from '~/components/wizard'; 5 + import type { WizardStepProps } from '~/components/wizard'; 6 6 import BlueskyLoginStep from '~/components/wizards/bluesky-login-step'; 7 7 8 - import { ThreadgateApplicatorConstraints } from '../page'; 8 + import type { ThreadgateApplicatorConstraints } from '../page'; 9 9 10 10 const Step3_Authentication = ({ 11 11 data,
+71 -52
src/views/bluesky/threadgate-applicator/steps/step4_confirmation.tsx
··· 1 1 import { createSignal, Show } from 'solid-js'; 2 2 3 - import { XRPC, XRPCError } from '@atcute/client'; 4 - import { AppBskyFeedThreadgate, ComAtprotoRepoApplyWrites } from '@atcute/client/lexicons'; 3 + import type { ComAtprotoRepoApplyWrites } from '@atcute/atproto'; 4 + import type { AppBskyFeedThreadgate } from '@atcute/bluesky'; 5 + import { Client, ClientResponseError } from '@atcute/client'; 6 + import { parseCanonicalResourceUri } from '@atcute/lexicons'; 5 7 import { chunked } from '@mary/array-fns'; 6 8 7 9 import { dequal } from '~/lib/utils/dequal'; ··· 9 11 10 12 import Button from '~/components/inputs/button'; 11 13 import ToggleInput from '~/components/inputs/toggle-input'; 12 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 13 - 14 - import { parseAtUri } from '~/api/utils/strings'; 15 14 import Logger, { createLogger } from '~/components/logger'; 16 - import { ThreadgateApplicatorConstraints } from '../page'; 15 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 16 + 17 + import type { ThreadgateApplicatorConstraints } from '../page'; 17 18 18 19 const Step4_Confirmation = ({ 19 20 data, ··· 33 34 logger.log(`Preparing writes`); 34 35 35 36 const rules = data.rules; 36 - const writes: ComAtprotoRepoApplyWrites.Input['writes'] = []; 37 + const writes: ComAtprotoRepoApplyWrites.$input['writes'] = []; 37 38 38 39 const now = new Date().toISOString(); 39 40 for (const { post, threadgate } of data.threads) { 40 41 if (threadgate === null) { 41 42 if (rules !== undefined) { 42 - const { rkey } = parseAtUri(post.uri); 43 + const postUri = parseCanonicalResourceUri(post.uri); 44 + if (!postUri.ok) { 45 + throw new Error(`failed to parse ${post.uri}`); 46 + } 43 47 44 - const record: AppBskyFeedThreadgate.Record = { 48 + const record: AppBskyFeedThreadgate.Main = { 45 49 $type: 'app.bsky.feed.threadgate', 46 50 createdAt: now, 47 51 post: post.uri, ··· 52 56 writes.push({ 53 57 $type: 'com.atproto.repo.applyWrites#create', 54 58 collection: 'app.bsky.feed.threadgate', 55 - rkey: rkey, 59 + rkey: postUri.value.rkey, 56 60 value: record, 57 61 }); 58 62 } 59 63 } else { 60 64 if (rules === undefined && !threadgate.hiddenReplies?.length) { 61 - const { rkey } = parseAtUri(threadgate.uri); 65 + const threadgateUri = parseCanonicalResourceUri(threadgate.uri); 66 + if (!threadgateUri.ok) { 67 + throw new Error(`failed to parse ${threadgate.uri}`); 68 + } 62 69 63 70 writes.push({ 64 71 $type: 'com.atproto.repo.applyWrites#delete', 65 72 collection: 'app.bsky.feed.threadgate', 66 - rkey: rkey, 73 + rkey: threadgateUri.value.rkey, 67 74 }); 68 75 } else if (!dequal(threadgate.allow, rules)) { 69 - const { rkey } = parseAtUri(threadgate.uri); 76 + const threadgateUri = parseCanonicalResourceUri(threadgate.uri); 77 + if (!threadgateUri.ok) { 78 + throw new Error(`failed to parse ${threadgate.uri}`); 79 + } 70 80 71 - const record: AppBskyFeedThreadgate.Record = { 81 + const record: AppBskyFeedThreadgate.Main = { 72 82 $type: 'app.bsky.feed.threadgate', 73 83 createdAt: threadgate.createdAt, 74 84 post: post.uri, ··· 79 89 writes.push({ 80 90 $type: 'com.atproto.repo.applyWrites#update', 81 91 collection: 'app.bsky.feed.threadgate', 82 - rkey: rkey, 92 + rkey: threadgateUri.value.rkey, 83 93 value: record, 84 94 }); 85 95 } ··· 89 99 logger.log(`${writes.length} write operations to apply`); 90 100 91 101 const did = data.profile.didDoc.id; 92 - const rpc = new XRPC({ handler: data.manager }); 93 - 94 - const RATELIMIT_POINT_LIMIT = 150 * 3; 102 + const client = new Client({ handler: data.manager }); 95 103 96 104 { 97 105 using progress = logger.progress(`Applying writes`); 98 106 99 107 let written = 0; 100 108 for (const chunk of chunked(writes, 200)) { 101 - try { 102 - const { headers } = await rpc.call('com.atproto.repo.applyWrites', { 103 - data: { 104 - repo: did, 105 - writes: chunk, 106 - }, 107 - }); 109 + let attempts = 0; 108 110 109 - written += chunk.length; 110 - progress.update(`Applying writes (${written} applied)`); 111 + while (true) { 112 + if (attempts > 0) { 113 + await sleep(2_000); 114 + } 111 115 112 - if ('ratelimit-remaining' in headers) { 113 - const remaining = +headers['ratelimit-remaining']; 114 - const reset = +headers['ratelimit-reset'] * 1_000; 116 + attempts++; 115 117 116 - if (remaining < RATELIMIT_POINT_LIMIT) { 117 - // add some delay to be sure 118 - const delta = reset - Date.now() + 5_000; 119 - using _progress = logger.progress(`Reached ratelimit, waiting ${delta}ms`); 118 + try { 119 + const response = await client.post('com.atproto.repo.applyWrites', { 120 + input: { 121 + repo: did, 122 + writes: chunk, 123 + }, 124 + }); 120 125 121 - await new Promise((resolve) => setTimeout(resolve, delta)); 126 + if (response.ok) { 127 + written += chunk.length; 128 + progress.update(`Applying writes (${written} applied)`); 129 + break; 122 130 } 123 - } 124 - } catch (err) { 125 - if (!(err instanceof XRPCError) || err.kind !== 'RateLimitExceeded') { 126 - throw err; 127 - } 131 + 132 + if (response.status === 429) { 133 + // not exposed by CORS, hoping that someday it will 134 + const reset = response.headers.get('ratelimit-reset'); 135 + 136 + using _progress = logger.progress(`Ratelimited, waiting`); 137 + 138 + if (reset !== null) { 139 + const refreshAt = +reset * 1_000; 140 + const delta = refreshAt - Date.now(); 128 141 129 - const headers = err.headers; 130 - if ('ratelimit-remaining' in headers) { 131 - const remaining = +headers['ratelimit-remaining']; 132 - const reset = +headers['ratelimit-reset'] * 1_000; 142 + await sleep(delta); 143 + } else { 144 + await sleep(10_000); 145 + } 146 + } 133 147 134 - if (remaining < RATELIMIT_POINT_LIMIT) { 135 - // add some delay to be sure 136 - const delta = reset - Date.now() + 5_000; 137 - using _progress = logger.progress(`Reached ratelimit, waiting ${delta}ms`); 148 + if (attempts < 3) { 149 + continue; 150 + } 138 151 139 - await new Promise((resolve) => setTimeout(resolve, delta)); 152 + throw new ClientResponseError(response); 153 + } catch (err) { 154 + // Network errors, etc 155 + if (attempts < 3) { 156 + continue; 140 157 } 141 - } else { 142 - using _progress = logger.progress(`Reached ratelimit, waiting 1 minute`); 143 158 144 - await new Promise((resolve) => setTimeout(resolve, 60 * 1_000)); 159 + throw err; 145 160 } 146 161 } 147 162 } ··· 201 216 }; 202 217 203 218 export default Step4_Confirmation; 219 + 220 + const sleep = (ms: number): Promise<void> => { 221 + return new Promise((resolve) => setTimeout(resolve, ms)); 222 + };
+5 -4
src/views/bluesky/threadgate-applicator/steps/step5_finished.tsx
··· 1 - import { Stage, WizardStepProps } from '~/components/wizard'; 1 + import { Stage, type WizardStepProps } from '~/components/wizard'; 2 2 3 - import { ThreadgateApplicatorConstraints } from '../page'; 3 + import type { ThreadgateApplicatorConstraints } from '../page'; 4 4 5 5 export const Step5_Finished = ({}: WizardStepProps<ThreadgateApplicatorConstraints, 'Step5_Finished'>) => { 6 6 return ( 7 7 <Stage title="All done!"> 8 8 <div> 9 - <p class="text-pretty">Thread gating option has been applied.</p> 9 + <p class="text-pretty">Thread gating has been applied.</p> 10 10 11 11 <p class="mt-3 text-pretty"> 12 - You can close this page, or reload the page if you intend on doing another submission. 12 + You can revoke the app password and close this page now, or reload the page if you intend on 13 + changing your mind. 13 14 </p> 14 15 </div> 15 16 </Stage>
+1 -1
src/views/bluesky/threadgate-applicator/utils.ts
··· 1 - import { ThreadgateState } from './page'; 1 + import type { ThreadgateState } from './page'; 2 2 3 3 const collator = new Intl.Collator('en'); 4 4
+3 -6
src/views/crypto/crypto-generate.tsx
··· 6 6 7 7 import Button from '~/components/inputs/button'; 8 8 import RadioInput from '~/components/inputs/radio-input'; 9 + import PageHeader from '~/components/page-header'; 9 10 10 11 type KeyType = 'p256' | 'secp256k1'; 11 12 ··· 26 27 27 28 return ( 28 29 <> 29 - <div class="p-4"> 30 - <h1 class="text-lg font-bold text-purple-800">Generate secret keys</h1> 31 - <p class="text-gray-600">Create a new secp256k1/nistp256 keypair</p> 32 - </div> 33 - <hr class="mx-4 border-gray-300" /> 30 + <PageHeader title="Generate secret keys" subtitle="Create a new secp256k1/nistp256 keypair" /> 34 31 35 32 <form 36 33 onSubmit={async (ev) => { ··· 54 51 ]); 55 52 56 53 const result: KeypairResult = { 57 - type: keypair.type, 54 + type: keypair.type as KeyType, 58 55 publicDidKey, 59 56 privateHex, 60 57 privateMultikey,
+255
src/views/crypto/crypto-info.tsx
··· 1 + import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'; 2 + 3 + import { 4 + type DidKeyString, 5 + P256PrivateKeyExportable, 6 + P256PublicKey, 7 + parseDidKey, 8 + parsePrivateMultikey, 9 + parsePublicMultikey, 10 + Secp256k1PrivateKeyExportable, 11 + Secp256k1PublicKey, 12 + } from '@atcute/crypto'; 13 + import { fromBase16 } from '@atcute/multibase'; 14 + 15 + import { useTitle } from '~/lib/navigation/router'; 16 + 17 + import Button from '~/components/inputs/button'; 18 + import RadioInput from '~/components/inputs/radio-input'; 19 + import TextInput from '~/components/inputs/text-input'; 20 + import PageHeader from '~/components/page-header'; 21 + 22 + type KeyType = 'p256' | 'secp256k1'; 23 + type KeyFormat = 'did:key' | 'multikey' | 'hex'; 24 + 25 + interface KeyInfo { 26 + keyType: KeyType; 27 + isPrivate: boolean; 28 + inputFormat: KeyFormat; 29 + publicDidKey: DidKeyString; 30 + publicMultikey: string; 31 + privateHex?: string; 32 + privateMultikey?: string; 33 + } 34 + 35 + const DID_KEY_REGEX = /^did:key:z[a-km-zA-HJ-NP-Z1-9]+$/; 36 + const MULTIKEY_REGEX = /^z[a-km-zA-HJ-NP-Z1-9]+$/; 37 + const HEX_REGEX = /^[0-9a-f]+$/; 38 + 39 + const CryptoInfoPage = () => { 40 + const [input, setInput] = createSignal(''); 41 + const [hexKeyType, setHexKeyType] = createSignal<KeyType>(); 42 + const [result, setResult] = createSignal<KeyInfo>(); 43 + const [error, setError] = createSignal<string>(); 44 + 45 + const detectedFormat = createMemo((): KeyFormat | undefined => { 46 + const $input = input().trim(); 47 + 48 + if (DID_KEY_REGEX.test($input)) { 49 + return 'did:key'; 50 + } 51 + if (MULTIKEY_REGEX.test($input)) { 52 + return 'multikey'; 53 + } 54 + if (HEX_REGEX.test($input)) { 55 + return 'hex'; 56 + } 57 + }); 58 + 59 + const canSubmit = createMemo(() => { 60 + const format = detectedFormat(); 61 + if (!format) { 62 + return false; 63 + } 64 + if (format === 'hex' && !hexKeyType()) { 65 + return false; 66 + } 67 + return true; 68 + }); 69 + 70 + useTitle(() => `View crypto key info โ€” boat`); 71 + 72 + return ( 73 + <> 74 + <PageHeader title="View crypto key info" subtitle="Show basic metadata about a public or private key" /> 75 + 76 + <form 77 + onSubmit={async (ev) => { 78 + ev.preventDefault(); 79 + 80 + const $input = input().trim(); 81 + const format = detectedFormat(); 82 + 83 + setResult(); 84 + setError(); 85 + 86 + try { 87 + let info: KeyInfo; 88 + 89 + if (format === 'did:key') { 90 + const parsed = parseDidKey($input); 91 + const pubKey = 92 + parsed.type === 'p256' 93 + ? await P256PublicKey.importRaw(parsed.publicKeyBytes) 94 + : await Secp256k1PublicKey.importRaw(parsed.publicKeyBytes); 95 + 96 + info = { 97 + keyType: parsed.type, 98 + isPrivate: false, 99 + inputFormat: 'did:key', 100 + publicDidKey: await pubKey.exportPublicKey('did'), 101 + publicMultikey: await pubKey.exportPublicKey('multikey'), 102 + }; 103 + } else if (format === 'multikey') { 104 + // try parsing as private key first 105 + try { 106 + const parsed = parsePrivateMultikey($input); 107 + const privKey = 108 + parsed.type === 'p256' 109 + ? await P256PrivateKeyExportable.importRaw(parsed.privateKeyBytes) 110 + : await Secp256k1PrivateKeyExportable.importRaw(parsed.privateKeyBytes); 111 + 112 + info = { 113 + keyType: parsed.type, 114 + isPrivate: true, 115 + inputFormat: 'multikey', 116 + publicDidKey: await privKey.exportPublicKey('did'), 117 + publicMultikey: await privKey.exportPublicKey('multikey'), 118 + privateHex: await privKey.exportPrivateKey('rawHex'), 119 + privateMultikey: await privKey.exportPrivateKey('multikey'), 120 + }; 121 + } catch { 122 + // try parsing as public key 123 + const parsed = parsePublicMultikey($input); 124 + const pubKey = 125 + parsed.type === 'p256' 126 + ? await P256PublicKey.importRaw(parsed.publicKeyBytes) 127 + : await Secp256k1PublicKey.importRaw(parsed.publicKeyBytes); 128 + 129 + info = { 130 + keyType: parsed.type, 131 + isPrivate: false, 132 + inputFormat: 'multikey', 133 + publicDidKey: await pubKey.exportPublicKey('did'), 134 + publicMultikey: await pubKey.exportPublicKey('multikey'), 135 + }; 136 + } 137 + } else if (format === 'hex') { 138 + const keyType = hexKeyType()!; 139 + const privateKeyBytes = fromBase16($input); 140 + 141 + const privKey = 142 + keyType === 'p256' 143 + ? await P256PrivateKeyExportable.importRaw(privateKeyBytes) 144 + : await Secp256k1PrivateKeyExportable.importRaw(privateKeyBytes); 145 + 146 + info = { 147 + keyType: keyType, 148 + isPrivate: true, 149 + inputFormat: 'hex', 150 + publicDidKey: await privKey.exportPublicKey('did'), 151 + publicMultikey: await privKey.exportPublicKey('multikey'), 152 + privateHex: await privKey.exportPrivateKey('rawHex'), 153 + privateMultikey: await privKey.exportPrivateKey('multikey'), 154 + }; 155 + } else { 156 + throw new Error('Unknown key format'); 157 + } 158 + 159 + setResult(info); 160 + } catch (err) { 161 + console.error(err); 162 + setError(`Failed to parse key: ${err}`); 163 + } 164 + }} 165 + class="flex flex-col gap-4 p-4" 166 + > 167 + <TextInput 168 + label="Public or private key" 169 + blurb="Accepts did:key, multikey, or hex format" 170 + monospace 171 + autocomplete="off" 172 + autocorrect="off" 173 + placeholder="did:key:z... or z... or a5973930f9d348..." 174 + value={input()} 175 + required 176 + onChange={setInput} 177 + /> 178 + 179 + <Show when={detectedFormat() === 'hex'}> 180 + <RadioInput 181 + label="This is a..." 182 + value={hexKeyType()} 183 + required 184 + options={[ 185 + { value: 'secp256k1', label: `ES256K (secp256k1) private key` }, 186 + { value: 'p256', label: `ES256 (p256) private key` }, 187 + ]} 188 + onChange={setHexKeyType} 189 + /> 190 + </Show> 191 + 192 + <div> 193 + <Button type="submit" disabled={!canSubmit()}> 194 + Inspect 195 + </Button> 196 + </div> 197 + </form> 198 + 199 + <hr class="mx-4 border-gray-300" /> 200 + 201 + <Switch> 202 + <Match when={error()}> 203 + <div class="p-4 text-red-600">{error()}</div> 204 + </Match> 205 + 206 + <Match when={result()} keyed> 207 + {(info) => ( 208 + <div class="flex flex-col gap-6 break-words p-4 text-gray-900"> 209 + <div> 210 + <p class="font-semibold text-gray-600">Key type</p> 211 + <span> 212 + {/* @once */ info.keyType === 'p256' 213 + ? 'ES256 (p256)' 214 + : 'ES256K (secp256k1)'}{' '} 215 + {/* @once */ info.isPrivate ? 'private' : 'public'} key 216 + </span> 217 + </div> 218 + 219 + <div> 220 + <p class="font-semibold text-gray-600">Input encoding</p> 221 + <span>{/* @once */ info.inputFormat}</span> 222 + </div> 223 + 224 + <div> 225 + <p class="font-semibold text-gray-600">Public key (did:key)</p> 226 + <span class="font-mono">{/* @once */ info.publicDidKey}</span> 227 + </div> 228 + 229 + <div> 230 + <p class="font-semibold text-gray-600">Public key (multikey)</p> 231 + <span class="font-mono">{/* @once */ info.publicMultikey}</span> 232 + </div> 233 + 234 + <Show when={info.privateHex}> 235 + <div> 236 + <p class="font-semibold text-gray-600">Private key (hex)</p> 237 + <span class="font-mono">{/* @once */ info.privateHex}</span> 238 + </div> 239 + </Show> 240 + 241 + <Show when={info.privateMultikey}> 242 + <div> 243 + <p class="font-semibold text-gray-600">Private key (multikey)</p> 244 + <span class="font-mono">{/* @once */ info.privateMultikey}</span> 245 + </div> 246 + </Show> 247 + </div> 248 + )} 249 + </Match> 250 + </Switch> 251 + </> 252 + ); 253 + }; 254 + 255 + export default CryptoInfoPage;
+11 -13
src/views/frontpage.tsx
··· 1 - import { Component, ComponentProps } from 'solid-js'; 1 + import type { Component, ComponentProps } from 'solid-js'; 2 2 3 3 import { useTitle } from '~/lib/navigation/router'; 4 + 5 + import PageHeader from '~/components/page-header'; 4 6 5 7 import HistoryIcon from '~/components/ic-icons/baseline-history'; 6 8 import KeyIcon from '~/components/ic-icons/baseline-key'; ··· 61 63 icon: ArchiveOutlinedIcon, 62 64 }, 63 65 { 64 - name: `Unpack CAR file`, 66 + name: `Unpack archive`, 65 67 description: `Extract a repository archive into a tarball`, 66 - href: '/car-unpack', 68 + href: '/repo-archive-unpack', 67 69 icon: DirectionsCarOutlinedIcon, 68 70 }, 69 71 { 70 - name: `Repository explorer`, 71 - description: `Explore an account's public records`, 72 - href: null, 72 + name: `Explore archive`, 73 + description: `Explore a repository archive`, 74 + href: '/repo-archive-explore', 73 75 icon: ExploreOutlinedIcon, 74 76 }, 75 77 ], ··· 102 104 { 103 105 name: `Migrate account`, 104 106 description: `Move your account data to another server`, 105 - href: null, 107 + href: '/account-migrate', 106 108 icon: MoveUpOutlinedIcon, 107 109 }, 108 110 ], ··· 119 121 { 120 122 name: `View crypto key info`, 121 123 description: `Show basic metadata about a public or private key`, 122 - href: null, 124 + href: `/crypto-info`, 123 125 icon: KeyVisualizerIcon, 124 126 }, 125 127 ], ··· 170 172 171 173 return ( 172 174 <> 173 - <div class="p-4"> 174 - <h1 class="text-lg font-bold text-purple-800">boat</h1> 175 - <p class="text-gray-600">handy online tools for AT Protocol</p> 176 - </div> 177 - <hr class="mx-4 border-gray-300" /> 175 + <PageHeader title="boat" subtitle="handy online tools for AT Protocol" /> 178 176 179 177 <div class="flex grow flex-col pb-2">{nodes}</div> 180 178
+24 -38
src/views/identity/did-lookup.tsx
··· 1 1 import { Match, Switch } from 'solid-js'; 2 2 3 - import { At } from '@atcute/client/lexicons'; 3 + import { isAtprotoDid } from '@atcute/identity'; 4 + import { isHandle, type AtprotoDid, type Did, type Handle } from '@atcute/lexicons/syntax'; 4 5 5 6 import { getDidDocument } from '~/api/queries/did-doc'; 6 7 import { resolveHandleViaAppView } from '~/api/queries/handle'; 7 8 import { isServiceUrlString } from '~/api/types/strings'; 8 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 9 9 10 10 import { useTitle } from '~/lib/navigation/router'; 11 11 import { createQuery } from '~/lib/utils/query'; ··· 15 15 import ErrorView from '~/components/error-view'; 16 16 import Button from '~/components/inputs/button'; 17 17 import TextInput from '~/components/inputs/text-input'; 18 + import PageHeader from '~/components/page-header'; 18 19 19 20 const DidLookupPage = () => { 20 21 const [params, setParams] = useSearchParams({ ··· 24 25 const query = createQuery( 25 26 () => params.q, 26 27 async (identifier, signal) => { 27 - let did: At.DID; 28 - if (isDid(identifier)) { 28 + let did: AtprotoDid; 29 + if (isAtprotoDid(identifier)) { 29 30 did = identifier; 31 + } else if (isHandle(identifier)) { 32 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 30 33 } else { 31 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 34 + throw new Error(`Invalid identifier`); 32 35 } 33 36 34 37 const doc = await getDidDocument({ did, signal }); ··· 44 47 45 48 return ( 46 49 <> 47 - <div class="p-4"> 48 - <h1 class="text-lg font-bold text-purple-800">View identity info</h1> 49 - <p class="text-gray-600">Look up an account's DID document</p> 50 - </div> 51 - <hr class="mx-4 border-gray-300" /> 50 + <PageHeader title="View identity info" subtitle="Look up an account's DID document" /> 52 51 53 52 <form 54 53 onSubmit={(ev) => { 55 54 const formData = new FormData(ev.currentTarget); 56 55 ev.preventDefault(); 57 56 58 - const ident = formData.get('ident') as string; 57 + const ident = formData.get('ident') as Did | Handle; 59 58 setParams({ q: ident }); 60 59 }} 61 60 class="m-4 flex flex-col gap-4" ··· 65 64 type="text" 66 65 name="ident" 67 66 autocomplete="username" 68 - pattern={/* @once */ DID_OR_HANDLE_RE.source} 69 67 placeholder="paul.bsky.social" 70 68 autofocus 71 69 /> ··· 100 98 <div> 101 99 <p class="font-semibold text-gray-600">Identifies as</p> 102 100 <ol class="list-disc pl-4"> 103 - {doc.alsoKnownAs.map((ident) => ( 101 + {doc.alsoKnownAs?.map((ident) => ( 104 102 <li>{ident}</li> 105 103 ))} 106 104 </ol> ··· 109 107 <div> 110 108 <p class="font-semibold text-gray-600">Services</p> 111 109 <ol class="list-disc pl-4"> 112 - {doc.service.map(({ id, type, serviceEndpoint }, idx) => { 110 + {doc.service?.map(({ id, type, serviceEndpoint }, idx) => { 113 111 const isString = typeof serviceEndpoint === 'string'; 114 112 const isURL = isString && URL.canParse('' + serviceEndpoint); 115 113 const isServiceUrl = isString && isServiceUrlString(serviceEndpoint); ··· 132 130 133 131 <div class="mt-2 flex flex-wrap gap-2 empty:hidden"> 134 132 {isPDS && isServiceUrl && ( 135 - <button 136 - disabled 137 - class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50" 138 - > 133 + <Button variant="outline" disabled> 139 134 View PDS info 140 - </button> 135 + </Button> 141 136 )} 142 137 143 138 {isPDS && isServiceUrl && ( 144 - <button 145 - disabled 146 - class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50" 147 - > 139 + <Button variant="outline" disabled> 148 140 Explore account repository 149 - </button> 141 + </Button> 150 142 )} 151 143 152 144 {isLabeler && isServiceUrl && ( 153 - <button 154 - disabled 155 - class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50" 156 - > 145 + <Button variant="outline" disabled> 157 146 View emitted labels 158 - </button> 147 + </Button> 159 148 )} 160 149 </div> 161 150 </li> ··· 167 156 <div> 168 157 <p class="font-semibold text-gray-600">Verification methods</p> 169 158 <ol class="list-disc pl-4"> 170 - {doc.verificationMethod.map(({ id, type, publicKeyMultibase }, idx) => { 159 + {doc.verificationMethod?.map(({ id, type, publicKeyMultibase }, idx) => { 171 160 return ( 172 161 <li class={idx !== 0 ? `mt-3` : ``}> 173 162 <p class="font-medium">{id.replace(doc.id, '')}</p> ··· 184 173 </div> 185 174 186 175 <div class="flex flex-wrap gap-4 p-4 pt-2"> 187 - <button 176 + <Button 177 + variant="outline" 188 178 onClick={() => { 189 179 navigator.clipboard.writeText(JSON.stringify(doc, null, 2)); 190 180 }} 191 - class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100" 192 181 > 193 182 Copy DID document 194 - </button> 183 + </Button> 195 184 196 185 {isDidPlc && ( 197 - <a 198 - href={`/plc-oplogs?q=${params.q!}`} 199 - class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100" 200 - > 186 + <Button variant="outline" href={`/plc-oplogs?q=${params.q!}`}> 201 187 View PLC operation logs 202 - </a> 188 + </Button> 203 189 )} 204 190 </div> 205 191 </>
+15 -18
src/views/identity/plc-applicator/page.tsx
··· 1 1 import { createEffect, createSignal, onCleanup } from 'solid-js'; 2 2 3 + import type { ComAtprotoIdentityGetRecommendedDidCredentials } from '@atcute/atproto'; 3 4 import type { CredentialManager } from '@atcute/client'; 4 - import type { ComAtprotoIdentityGetRecommendedDidCredentials } from '@atcute/client/lexicons'; 5 5 import type { P256PrivateKey, Secp256k1PrivateKey } from '@atcute/crypto'; 6 + import type { CompatibleOperation, IndexedEntry, IndexedEntryWithSigner } from '@atcute/did-plc'; 7 + import type { DidDocument } from '@atcute/identity'; 8 + import type { Did } from '@atcute/lexicons/syntax'; 6 9 7 - import type { DidDocument } from '~/api/types/did-doc'; 8 - import type { PlcUpdatePayload } from '~/api/types/plc'; 10 + import type { UpdatePayload } from '~/api/types/plc'; 9 11 10 12 import { history } from '~/globals/navigation'; 11 13 12 14 import { useTitle } from '~/lib/navigation/router'; 13 15 16 + import PageHeader from '~/components/page-header'; 14 17 import { Wizard } from '~/components/wizard'; 15 18 16 - import type { DetailedPlcEntry } from './plc-utils'; 17 - 18 19 import Step1_HandleInput from './steps/step1_handle-input'; 19 20 import Step2_PdsAuthentication from './steps/step2_pds-authentication'; 20 21 import Step2_PrivateKeyInput from './steps/step2_private-key-input'; ··· 26 27 27 28 export interface PlcInformation { 28 29 didDoc: DidDocument; 29 - logs: DetailedPlcEntry[]; 30 + logs: IndexedEntryWithSigner[]; 30 31 } 31 32 32 33 export interface PdsSigningMethod { 33 34 type: 'pds'; 34 35 manager: CredentialManager; 35 - recommendedDidDoc: ComAtprotoIdentityGetRecommendedDidCredentials.Output; 36 + recommendedDidDoc: ComAtprotoIdentityGetRecommendedDidCredentials.$output; 36 37 } 37 38 38 39 export type Keypair = P256PrivateKey | Secp256k1PrivateKey; 39 40 export interface PrivateKeySigningMethod { 40 41 type: 'private_key'; 41 42 keypair: Keypair; 42 - didPublicKey: string; 43 + didPublicKey: Did<'key'>; 43 44 } 44 45 45 46 export type SigningMethod = PdsSigningMethod | PrivateKeySigningMethod; ··· 62 63 Step4_PayloadInput: { 63 64 info: PlcInformation; 64 65 method: SigningMethod; 65 - base: DetailedPlcEntry; 66 + base: IndexedEntry<CompatibleOperation>; 66 67 }; 67 68 68 69 Step5_PdsConfirmation: { 69 70 info: PlcInformation; 70 71 method: PdsSigningMethod; 71 - base: DetailedPlcEntry; 72 - payload: PlcUpdatePayload; 72 + base: IndexedEntry<CompatibleOperation>; 73 + payload: UpdatePayload; 73 74 }; 74 75 Step5_PrivateKeyConfirmation: { 75 76 info: PlcInformation; 76 77 method: PrivateKeySigningMethod; 77 - base: DetailedPlcEntry; 78 - payload: PlcUpdatePayload; 78 + base: IndexedEntry<CompatibleOperation>; 79 + payload: UpdatePayload; 79 80 }; 80 81 81 82 Step6_Finished: {}; ··· 101 102 102 103 return ( 103 104 <> 104 - <div class="p-4"> 105 - <h1 class="text-lg font-bold text-purple-800">Apply PLC operations</h1> 106 - <p class="text-gray-600">Submit operations to your did:plc identity</p> 107 - </div> 108 - <hr class="mx-4 border-gray-300" /> 105 + <PageHeader title="Apply PLC operations" subtitle="Submit operations to your did:plc identity" /> 109 106 110 107 <Wizard<PlcApplicatorConstraints> 111 108 initialStep="Step1_HandleInput"
+3 -96
src/views/identity/plc-applicator/plc-utils.ts
··· 1 - import * as CBOR from '@atcute/cbor'; 2 - import { verifySigWithDidKey } from '@atcute/crypto'; 3 - import { fromBase64Url } from '@atcute/multibase'; 1 + import type { IndexedEntry } from '@atcute/did-plc'; 4 2 5 - import { PlcLogEntry, PlcUpdatePayload } from '~/api/types/plc'; 6 - import { UnwrapArray } from '~/api/utils/types'; 3 + import type { UpdatePayload } from '~/api/types/plc'; 7 4 8 5 import { assert } from '~/lib/utils/invariant'; 9 6 10 - export const getPlcPayload = (entry: PlcLogEntry): PlcUpdatePayload => { 7 + export const getPlcPayload = (entry: IndexedEntry): UpdatePayload => { 11 8 const op = entry.operation; 12 9 assert(op.type === 'plc_operation' || op.type === 'create'); 13 10 ··· 36 33 37 34 assert(false); 38 35 }; 39 - 40 - export const getPlcKeying = async (logs: PlcLogEntry[]) => { 41 - logs = logs.filter((entry) => !entry.nullified); 42 - 43 - const length = logs.length; 44 - const promises = logs.map(async (entry, idx) => { 45 - const operation = entry.operation; 46 - if (operation.type === 'plc_tombstone') { 47 - return; 48 - } 49 - 50 - // If it's not the last entry, check if the next entry ahead of this one 51 - // was made within the last 72 hours. 52 - if (idx !== length - 1) { 53 - const next = logs[idx + 1]!; 54 - const date = new Date(next.createdAt); 55 - const diff = Date.now() - date.getTime(); 56 - 57 - if (diff / (1_000 * 60 * 60) > 72) { 58 - return; 59 - } 60 - } 61 - 62 - /** keys that potentially signed this operation */ 63 - let signers: `did:key:${string}`[] | undefined; 64 - if (operation.prev === null) { 65 - if (operation.type === 'create') { 66 - signers = [operation.recoveryKey, operation.signingKey]; 67 - } else if (operation.type === 'plc_operation') { 68 - signers = operation.rotationKeys; 69 - } 70 - } else { 71 - const prev = logs[idx - 1]; 72 - assert(prev !== undefined, `missing previous entry from ${entry.createdAt}`); 73 - assert(prev.cid === operation.prev, `prev cid mismatch on ${entry.createdAt}`); 74 - 75 - const prevOp = prev.operation; 76 - 77 - if (prevOp.type === 'create') { 78 - signers = [prevOp.recoveryKey, prevOp.signingKey]; 79 - } else if (prevOp.type === 'plc_operation') { 80 - signers = prevOp.rotationKeys; 81 - } 82 - } 83 - 84 - assert(signers !== undefined, `no signers found for ${entry.createdAt}`); 85 - 86 - const opBytes = CBOR.encode({ ...operation, sig: undefined }); 87 - const sigBytes = fromBase64Url(operation.sig); 88 - 89 - /** key that signed this operation */ 90 - let signedBy: string | undefined; 91 - for (const key of signers) { 92 - const valid = await verifySigWithDidKey(key, sigBytes, opBytes); 93 - if (valid) { 94 - signedBy = key; 95 - break; 96 - } 97 - } 98 - 99 - assert(signedBy !== undefined, `no valid signer for ${entry.createdAt}`); 100 - 101 - return { 102 - ...entry, 103 - signers, 104 - signedBy, 105 - }; 106 - }); 107 - 108 - const fulfilled = await Promise.all(promises); 109 - return fulfilled.filter((entry) => entry !== undefined); 110 - }; 111 - 112 - type DetailedEntries = Awaited<ReturnType<typeof getPlcKeying>>; 113 - export type DetailedPlcEntry = UnwrapArray<DetailedEntries>; 114 - 115 - export const getCurrentSignersFromEntry = (entry: PlcLogEntry): string[] => { 116 - const operation = entry.operation; 117 - 118 - /** keys that can sign the next operation */ 119 - let nextSigners: string[] | undefined; 120 - if (operation.type === 'create') { 121 - nextSigners = [operation.recoveryKey, operation.signingKey]; 122 - } else if (operation.type === 'plc_operation') { 123 - nextSigners = operation.rotationKeys; 124 - } 125 - 126 - assert(nextSigners !== undefined, `no signers found for ${entry.createdAt}`); 127 - return nextSigners; 128 - };
+27 -22
src/views/identity/plc-applicator/steps/step1_handle-input.tsx
··· 1 1 import { createSignal } from 'solid-js'; 2 2 3 - import { XRPCError } from '@atcute/client'; 4 - import { At } from '@atcute/client/lexicons'; 3 + import { ClientResponseError } from '@atcute/client'; 4 + import { processIndexedEntryLog } from '@atcute/did-plc'; 5 + import { isPlcDid } from '@atcute/identity'; 6 + import { isHandle, type Did } from '@atcute/lexicons/syntax'; 5 7 6 8 import { getDidDocument } from '~/api/queries/did-doc'; 7 9 import { resolveHandleViaAppView } from '~/api/queries/handle'; 8 10 import { getPlcAuditLogs } from '~/api/queries/plc'; 9 - import { DID_OR_HANDLE_RE, DID_PLC_RE, isDid } from '~/api/utils/strings'; 10 11 11 12 import { createMutation } from '~/lib/utils/mutation'; 12 13 13 14 import Button from '~/components/inputs/button'; 14 15 import RadioInput from '~/components/inputs/radio-input'; 15 16 import TextInput from '~/components/inputs/text-input'; 16 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 17 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 17 18 18 - import { type PlcInformation, PlcApplicatorConstraints } from '../page'; 19 - import { getPlcKeying } from '../plc-utils'; 19 + import type { PlcApplicatorConstraints, PlcInformation } from '../page'; 20 20 21 21 type Method = 'pds' | 'key'; 22 22 ··· 38 38 39 39 const mutation = createMutation({ 40 40 async mutationFn({ identifier }: MutationVariables): Promise<PlcInformation> { 41 - let did: At.DID; 42 - if (isDid(identifier)) { 41 + let did: Did<'plc'>; 42 + if (isPlcDid(identifier)) { 43 43 did = identifier; 44 + } else if (isHandle(identifier)) { 45 + const resolved = await resolveHandleViaAppView({ handle: identifier }); 46 + if (!isPlcDid(resolved)) { 47 + throw new DidIsNotPlcError(`${resolved} does not resolve to a did:plc`); 48 + } 49 + 50 + did = resolved; 44 51 } else { 45 - did = await resolveHandleViaAppView({ handle: identifier }); 46 - } 47 - 48 - if (!DID_PLC_RE.test(did)) { 49 - throw new DidIsNotPlcError(`"${did}" is not did:plc`); 52 + throw new DidIsNotPlcError(`${identifier} is not a valid did:plc or handle`); 50 53 } 51 54 52 55 const [didDoc, logs] = await Promise.all([getDidDocument({ did }), getPlcAuditLogs({ did })]); 56 + const { canonical } = await processIndexedEntryLog(did, logs); 53 57 54 58 return { 55 - didDoc, 56 - logs: await getPlcKeying(logs), 59 + didDoc: didDoc, 60 + logs: canonical, 57 61 }; 58 62 }, 59 63 onMutate() { ··· 66 70 onNext('Step2_PrivateKeyInput', { info }); 67 71 } 68 72 }, 69 - onError(error) { 73 + onError(err) { 70 74 let message: string | undefined; 71 75 72 - if (error instanceof XRPCError) { 73 - if (error.kind === 'InvalidRequest' && error.message.includes('resolve handle')) { 76 + if (err instanceof ClientResponseError) { 77 + if (err.error === 'InvalidRequest' && err.description?.includes('resolve handle')) { 74 78 message = `Can't seem to resolve handle, is it typed correctly?`; 75 79 } 76 - } else if (error instanceof DidIsNotPlcError) { 77 - message = error.message; 80 + } else if (err instanceof DidIsNotPlcError) { 81 + message = err.message; 78 82 } 79 83 80 84 if (message !== undefined) { 81 85 setError(message); 82 86 } else { 83 - setError(`Something went wrong: ${error}`); 87 + console.error(err); 88 + 89 + setError(`Something went wrong: ${err}`); 84 90 } 85 91 }, 86 92 }); ··· 101 107 placeholder="paul.bsky.social" 102 108 value={identifier()} 103 109 required 104 - pattern={/* @once */ DID_OR_HANDLE_RE.source} 105 110 autofocus={isActive()} 106 111 onChange={setIdentifier} 107 112 />
+18 -21
src/views/identity/plc-applicator/steps/step2_pds-authentication.tsx
··· 1 1 import { createSignal, Match, Show, Switch } from 'solid-js'; 2 2 3 - import { AtpAccessJwt, CredentialManager, XRPC, XRPCError } from '@atcute/client'; 4 - import { decodeJwt } from '@atcute/client/utils/jwt'; 3 + import { type AtpAccessJwt, Client, ClientResponseError, CredentialManager, ok } from '@atcute/client'; 4 + import { getPdsEndpoint } from '@atcute/identity'; 5 5 6 - import { getPdsEndpoint } from '~/api/types/did-doc'; 7 - import { TOTP_RE, formatTotpCode } from '~/api/utils/auth'; 6 + import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 7 + import { decodeJwt } from '~/api/utils/jwt'; 8 8 9 9 import { createMutation } from '~/lib/utils/mutation'; 10 10 11 11 import Button from '~/components/inputs/button'; 12 12 import TextInput from '~/components/inputs/text-input'; 13 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 13 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 14 14 15 - import { PlcApplicatorConstraints } from '../page'; 15 + import type { PlcApplicatorConstraints } from '../page'; 16 16 17 17 class InsufficientLoginError extends Error {} 18 18 ··· 59 59 setPassword(''); 60 60 setIsTotpRequired(false); 61 61 }, 62 - onError(error) { 62 + onError(err) { 63 63 let message: string | undefined; 64 64 65 - if (error instanceof XRPCError) { 66 - if (error.kind === 'AuthFactorTokenRequired') { 65 + if (err instanceof ClientResponseError) { 66 + if (err.error === 'AuthFactorTokenRequired') { 67 67 setOtp(''); 68 68 setIsTotpRequired(true); 69 69 return; 70 70 } 71 71 72 - if (error.kind === 'AuthenticationRequired') { 72 + if (err.error === 'AuthenticationRequired') { 73 73 message = `Invalid identifier or password`; 74 - } else if (error.kind === 'AccountTakedown') { 74 + } else if (err.error === 'AccountTakedown') { 75 75 message = `Account has been taken down`; 76 - } else if (error.message.includes('Token is invalid')) { 76 + } else if (err.description?.includes('Token is invalid')) { 77 77 message = `Invalid one-time confirmation code`; 78 78 setIsTotpRequired(true); 79 79 } 80 - } else if (error instanceof InsufficientLoginError) { 81 - message = error.message; 80 + } else if (err instanceof InsufficientLoginError) { 81 + message = err.message; 82 82 } 83 83 84 84 if (message !== undefined) { 85 85 setError(message); 86 86 } else { 87 - console.error(error); 88 - setError(`Something went wrong: ${error}`); 87 + console.error(err); 88 + setError(`Something went wrong: ${err}`); 89 89 } 90 90 }, 91 91 }); 92 92 93 93 const dispatchMutation = createMutation({ 94 94 async mutationFn({ manager }: { manager: CredentialManager }) { 95 - const rpc = new XRPC({ handler: manager }); 96 - const { data: recommendedDidDoc } = await rpc.get( 97 - 'com.atproto.identity.getRecommendedDidCredentials', 98 - {}, 99 - ); 95 + const rpc = new Client({ handler: manager }); 96 + const recommendedDidDoc = await ok(rpc.get('com.atproto.identity.getRecommendedDidCredentials')); 100 97 101 98 return { recommendedDidDoc }; 102 99 },
+3 -9
src/views/identity/plc-applicator/steps/step2_private-key-input.tsx
··· 8 8 import Button from '~/components/inputs/button'; 9 9 import RadioInput from '~/components/inputs/radio-input'; 10 10 import TextInput from '~/components/inputs/text-input'; 11 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 11 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 12 12 13 13 import type { Keypair, PlcApplicatorConstraints, PrivateKeySigningMethod } from '../page'; 14 14 ··· 97 97 }); 98 98 }, 99 99 onError(error) { 100 - let message: string | undefined; 101 - 102 - if (message !== undefined) { 103 - setError(message); 104 - } else { 105 - console.error(error); 106 - setError(`Something went wrong: ${error}`); 107 - } 100 + console.error(error); 101 + setError(`Something went wrong: ${error}`); 108 102 }, 109 103 }); 110 104
+115 -69
src/views/identity/plc-applicator/steps/step3_operation-select.tsx
··· 1 - import { createMemo, createSignal } from 'solid-js'; 1 + import { createMemo, createSignal, Show } from 'solid-js'; 2 + 3 + import { 4 + type CompatibleOperation, 5 + type DisputeCandidate, 6 + getDisputeCandidates, 7 + type IndexedEntryWithSigner, 8 + normalizeOp, 9 + } from '@atcute/did-plc'; 10 + import type { Did } from '@atcute/lexicons'; 2 11 3 12 import Button from '~/components/inputs/button'; 13 + import RadioInput from '~/components/inputs/radio-input'; 4 14 import SelectInput from '~/components/inputs/select-input'; 5 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 15 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 6 16 7 - import { PlcApplicatorConstraints } from '../page'; 8 - import { getCurrentSignersFromEntry } from '../plc-utils'; 17 + import type { PlcApplicatorConstraints } from '../page'; 9 18 10 19 const Step3_OperationSelect = ({ 11 20 data, ··· 14 23 onNext, 15 24 }: WizardStepProps<PlcApplicatorConstraints, 'Step3_OperationSelect'>) => { 16 25 const [error, setError] = createSignal<string>(); 17 - const [selectedCid, setSelectedCid] = createSignal<string>(); 18 26 19 - const options = createMemo(() => { 20 - const signingMethod = data.method; 21 - const logs = data.info.logs; 22 - 23 - let ownKey: string | undefined; 24 - if (signingMethod.type === 'pds') { 25 - ownKey = signingMethod.recommendedDidDoc.rotationKeys?.at(-1); 26 - } else if (signingMethod.type === 'private_key') { 27 - ownKey = signingMethod.didPublicKey; 28 - } 29 - 30 - if (ownKey === undefined) { 31 - return []; 32 - } 27 + const [type, setType] = createSignal<'append' | 'dispute'>(); 28 + const [cid, setCid] = createSignal<string>(); 33 29 34 - const length = logs.length; 35 - const items = logs.map((entry, idx) => { 36 - const signers = getCurrentSignersFromEntry(entry); 37 - const last = idx === length - 1; 38 - 39 - let enabled = signers.includes(ownKey!); 40 - 41 - // If we're showing older operations for forking/nullification, 42 - // check to see that our key has priority over the signer. 43 - if (enabled && !last) { 44 - if (signingMethod.type === 'pds') { 45 - // `signPlcOperation` will always grab the last op 46 - enabled = false; 47 - } else { 48 - const holderKey = logs[idx + 1].signedBy; 30 + const canAppend = createMemo(() => { 31 + const signing = data.method; 49 32 50 - const holderPriority = signers.indexOf(holderKey); 51 - const ownPriority = signers.indexOf(ownKey); 33 + const lastOp = data.info.logs.at(-1) as IndexedEntryWithSigner<CompatibleOperation>; 34 + const { rotationKeys } = normalizeOp(lastOp.operation); 52 35 53 - enabled = ownPriority < holderPriority; 36 + switch (signing.type) { 37 + case 'pds': { 38 + const key = signing.recommendedDidDoc.rotationKeys?.at(-1) as Did<'key'> | undefined; 39 + if (!key) { 40 + return false; 54 41 } 42 + 43 + return rotationKeys.includes(key); 55 44 } 45 + case 'private_key': { 46 + return rotationKeys.includes(signing.didPublicKey); 47 + } 48 + } 49 + }); 56 50 57 - return { 58 - value: entry.cid, 59 - label: `${entry.createdAt} (by ${entry.signedBy})`, 60 - disabled: !enabled, 61 - }; 62 - }); 51 + const disputes = createMemo((): DisputeCandidate[] => { 52 + const signing = data.method; 63 53 64 - return items.reverse(); 54 + switch (signing.type) { 55 + case 'pds': { 56 + // signPlcOperation always grabs the last operation, so we can't make 57 + // any dispute attempts. 58 + return []; 59 + } 60 + case 'private_key': { 61 + return getDisputeCandidates(data.info.logs, signing.didPublicKey); 62 + } 63 + } 65 64 }); 66 65 67 66 return ( 68 67 <Stage 69 - title="Select which operation to use as foundation" 68 + title="What do you want to do?" 70 69 onSubmit={() => { 71 70 setError(); 72 71 73 - const cid = selectedCid(); 74 - const entry = data.info.logs.find((entry) => entry.cid === cid); 72 + const $type = type(); 73 + const $cid = cid(); 75 74 76 - if (!entry) { 77 - setError(`Can't find CID ${cid}`); 78 - return; 79 - } 75 + switch ($type) { 76 + case 'append': { 77 + const lastOp = data.info.logs.at(-1) as IndexedEntryWithSigner<CompatibleOperation>; 80 78 81 - const operation = entry.operation; 82 - if (operation.type !== 'plc_operation' && operation.type === 'create') { 83 - setError(`Expected operation to be of type "plc_operation" or "create"`); 84 - return; 85 - } 79 + onNext('Step4_PayloadInput', { 80 + info: data.info, 81 + method: data.method, 82 + base: lastOp, 83 + }); 86 84 87 - onNext('Step4_PayloadInput', { 88 - info: data.info, 89 - method: data.method, 90 - base: entry, 91 - }); 85 + break; 86 + } 87 + case 'dispute': { 88 + const entry = disputes().find((entry) => entry.base.cid === $cid); 89 + if (!entry) { 90 + setError(`Can't find dispute entry for ${$cid}`); 91 + return; 92 + } 93 + 94 + onNext('Step4_PayloadInput', { 95 + info: data.info, 96 + method: data.method, 97 + base: entry.base, 98 + }); 99 + 100 + break; 101 + } 102 + } 92 103 }} 93 104 > 94 - <SelectInput 95 - label="Base operation" 96 - blurb="Some operations can't be used as a base if the rotation key does not have the privilege for nullification, or if it is not listed." 105 + <RadioInput 106 + label="I want to..." 97 107 required 98 - value={selectedCid()} 99 - autofocus={isActive()} 100 - options={[{ value: '', label: `Select an operation...` }, ...options()]} 101 - onChange={setSelectedCid} 108 + value={type()} 109 + options={[ 110 + { 111 + value: 'append', 112 + label: `Append an operation`, 113 + disabled: !canAppend(), 114 + }, 115 + { 116 + value: 'dispute', 117 + label: `Dispute an existing operation`, 118 + disabled: disputes().length === 0, 119 + }, 120 + ]} 121 + onChange={setType} 102 122 /> 103 123 124 + <Show when={type() === 'dispute'}> 125 + <SelectInput 126 + label="Dispute operation" 127 + blurb="Select an operation to dispute." 128 + required 129 + value={cid()} 130 + autofocus={isActive()} 131 + options={[ 132 + { value: '', label: `Select an operation...` }, 133 + ...disputes().map((entry) => ({ 134 + value: entry.base.cid, 135 + label: `${entry.base.cid} โž” ${entry.disputed.cid} (by ${entry.disputed.signedBy})`, 136 + })), 137 + ]} 138 + onChange={setCid} 139 + /> 140 + </Show> 141 + 142 + <Show when={!canAppend() && disputes().length === 0}> 143 + <p class="whitespace-pre-wrap text-[0.8125rem] font-medium leading-5 text-red-800"> 144 + This rotation key can't be used. 145 + </p> 146 + </Show> 147 + 104 148 <StageErrorView error={error()} /> 105 149 106 150 <StageActions hidden={!isActive()}> ··· 108 152 <Button variant="secondary" onClick={onPrevious}> 109 153 Previous 110 154 </Button> 111 - <Button type="submit">Next</Button> 155 + <Button type="submit" disabled={type() === undefined}> 156 + Next 157 + </Button> 112 158 </StageActions> 113 159 </Stage> 114 160 );
+2 -2
src/views/identity/plc-applicator/steps/step4_payload-input.tsx
··· 4 4 5 5 import Button from '~/components/inputs/button'; 6 6 import MultilineInput from '~/components/inputs/multiline-input'; 7 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 7 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 8 8 9 - import { PlcApplicatorConstraints } from '../page'; 9 + import type { PlcApplicatorConstraints } from '../page'; 10 10 import { getPlcPayload } from '../plc-utils'; 11 11 12 12 export const Step4_PayloadInput = ({
+30 -25
src/views/identity/plc-applicator/steps/step5_pds-confirmation.tsx
··· 1 1 import { createSignal } from 'solid-js'; 2 2 3 - import { XRPC, XRPCError } from '@atcute/client'; 3 + import { Client, ClientResponseError, ok } from '@atcute/client'; 4 4 5 5 import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 6 6 ··· 9 9 import CheckIcon from '~/components/ic-icons/baseline-check'; 10 10 import Button from '~/components/inputs/button'; 11 11 import TextInput from '~/components/inputs/text-input'; 12 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 12 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 13 13 14 - import { PlcApplicatorConstraints } from '../page'; 14 + import type { PlcApplicatorConstraints } from '../page'; 15 15 16 16 export const Step5_PdsConfirmation = ({ 17 17 data, ··· 27 27 const requestMutation = createMutation({ 28 28 async mutationFn() { 29 29 const manager = data.method.manager; 30 - const rpc = new XRPC({ handler: manager }); 30 + const rpc = new Client({ handler: manager }); 31 31 32 - await rpc.call('com.atproto.identity.requestPlcOperationSignature', {}); 32 + await ok(rpc.post('com.atproto.identity.requestPlcOperationSignature', { as: null })); 33 33 }, 34 34 onMutate() { 35 35 setRequestError(); ··· 49 49 const applyMutation = createMutation({ 50 50 async mutationFn({ code }: { code: string }) { 51 51 const manager = data.method.manager; 52 - const rpc = new XRPC({ handler: manager }); 52 + const client = new Client({ handler: manager }); 53 53 54 54 const payload = data.payload; 55 55 56 - const { data: signage } = await rpc.call('com.atproto.identity.signPlcOperation', { 57 - data: { 58 - token: formatTotpCode(code), 59 - alsoKnownAs: payload.alsoKnownAs, 60 - rotationKeys: payload.rotationKeys, 61 - services: payload.services, 62 - verificationMethods: payload.verificationMethods, 63 - }, 64 - }); 56 + const signage = await ok( 57 + client.post('com.atproto.identity.signPlcOperation', { 58 + input: { 59 + token: formatTotpCode(code), 60 + alsoKnownAs: payload.alsoKnownAs, 61 + rotationKeys: payload.rotationKeys, 62 + services: payload.services, 63 + verificationMethods: payload.verificationMethods, 64 + }, 65 + }), 66 + ); 65 67 66 - await rpc.call('com.atproto.identity.submitPlcOperation', { 67 - data: { 68 - operation: signage.operation, 69 - }, 70 - }); 68 + await ok( 69 + client.post('com.atproto.identity.submitPlcOperation', { 70 + as: null, 71 + input: { 72 + operation: signage.operation, 73 + }, 74 + }), 75 + ); 71 76 }, 72 77 onMutate() { 73 78 setApplyError(); ··· 75 80 onSuccess() { 76 81 onNext('Step6_Finished', {}); 77 82 }, 78 - onError(error) { 83 + onError(err) { 79 84 let message: string | undefined; 80 85 81 - if (error instanceof XRPCError) { 82 - if (error.kind === 'InvalidToken' || error.kind === 'ExpiredToken') { 86 + if (err instanceof ClientResponseError) { 87 + if (err.error === 'InvalidToken' || err.error === 'ExpiredToken') { 83 88 message = `Confirmation code has expired`; 84 89 } 85 90 } ··· 87 92 if (message !== undefined) { 88 93 setApplyError(message); 89 94 } else { 90 - console.error(error); 91 - setApplyError(`Something went wrong: ${error}`); 95 + console.error(err); 96 + setApplyError(`Something went wrong: ${err}`); 92 97 } 93 98 }, 94 99 });
+9 -11
src/views/identity/plc-applicator/steps/step5_private-key-confirmation.tsx
··· 1 1 import { createSignal } from 'solid-js'; 2 2 3 3 import * as CBOR from '@atcute/cbor'; 4 + import type { Operation, UnsignedOperation } from '@atcute/did-plc'; 4 5 import { toBase64Url } from '@atcute/multibase'; 5 - 6 - import { PlcUpdateOp } from '~/api/types/plc'; 7 6 8 7 import { generateConfirmationCode } from '~/lib/utils/confirmation-code'; 9 8 import { createMutation } from '~/lib/utils/mutation'; 10 9 11 10 import Button from '~/components/inputs/button'; 12 11 import TextInput from '~/components/inputs/text-input'; 13 - import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 12 + import { Stage, StageActions, StageErrorView, type WizardStepProps } from '~/components/wizard'; 14 13 15 - import { PlcApplicatorConstraints } from '../page'; 14 + import type { PlcApplicatorConstraints } from '../page'; 16 15 17 16 const Step5_PrivateKeyConfirmation = ({ 18 17 data, ··· 30 29 const payload = data.payload; 31 30 const prev = data.base; 32 31 33 - const operation: Omit<PlcUpdateOp, 'sig'> = { 32 + const operation: UnsignedOperation = { 34 33 type: 'plc_operation', 35 34 prev: prev!.cid, 36 35 ··· 45 44 46 45 const signature = toBase64Url(sigBytes); 47 46 48 - const signedOperation: PlcUpdateOp = { 47 + const signedOperation: Operation = { 49 48 ...operation, 50 49 sig: signature, 51 50 }; ··· 79 78 }} 80 79 > 81 80 <p class="text-pretty"> 82 - To continue with this submission, type in the following code{' '} 83 - <code class="whitespace-nowrap font-bold">{code}</code> to the confirmation box below. 81 + To continue with this submission, type in <code class="whitespace-nowrap font-bold">{code}</code> to 82 + the confirmation box below. 84 83 </p> 85 84 86 85 <TextInput 87 - label="Confirmation code" 86 + label="Confirmation" 88 87 type="text" 89 88 autocomplete="one-time-code" 90 89 autocorrect="off" 91 90 required 92 91 pattern={code} 93 - placeholder="AAAAA-BBBBB" 94 92 autofocus={isActive()} 95 93 monospace 96 94 /> ··· 122 120 123 121 export default Step5_PrivateKeyConfirmation; 124 122 125 - const pushPlcOperation = async (did: string, operation: PlcUpdateOp) => { 123 + const pushPlcOperation = async (did: string, operation: Operation) => { 126 124 const origin = import.meta.env.VITE_PLC_DIRECTORY_URL; 127 125 const response = await fetch(`${origin}/${did}`, { 128 126 method: 'post',
+2 -2
src/views/identity/plc-applicator/steps/step6_finished.tsx
··· 1 - import { Stage, WizardStepProps } from '~/components/wizard'; 1 + import { Stage, type WizardStepProps } from '~/components/wizard'; 2 2 3 - import { PlcApplicatorConstraints } from '../page'; 3 + import type { PlcApplicatorConstraints } from '../page'; 4 4 5 5 export const Step6_Finished = ({}: WizardStepProps<PlcApplicatorConstraints, 'Step6_Finished'>) => { 6 6 return (
+31 -32
src/views/identity/plc-oplogs.tsx
··· 1 - import { createSignal, JSX, Match, onCleanup, Switch } from 'solid-js'; 1 + import { createSignal, Match, onCleanup, Switch, type JSX } from 'solid-js'; 2 2 3 - import { At } from '@atcute/client/lexicons'; 3 + import type { IndexedEntry, Service } from '@atcute/did-plc'; 4 + import { isPlcDid } from '@atcute/identity'; 5 + import { isHandle, type Did, type Handle } from '@atcute/lexicons/syntax'; 4 6 5 7 import { resolveHandleViaAppView } from '~/api/queries/handle'; 6 - import { PlcLogEntry, Service } from '~/api/types/plc'; 7 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 8 8 9 9 import { getPlcAuditLogs } from '~/api/queries/plc'; 10 10 import { useTitle } from '~/lib/navigation/router'; ··· 20 20 import ContentCopyIcon from '~/components/ic-icons/baseline-content-copy'; 21 21 import Button from '~/components/inputs/button'; 22 22 import TextInput from '~/components/inputs/text-input'; 23 + import PageHeader from '~/components/page-header'; 23 24 24 25 const PlcOperationLogPage = () => { 25 26 const [params, setParams] = useSearchParams({ ··· 29 30 const query = createQuery( 30 31 () => params.q, 31 32 async (identifier, signal) => { 32 - let did: At.DID; 33 - if (isDid(identifier)) { 33 + let did: Did<'plc'>; 34 + if (isPlcDid(identifier)) { 34 35 did = identifier; 35 - } else { 36 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 37 - } 36 + } else if (isHandle(identifier)) { 37 + const resolved = await resolveHandleViaAppView({ handle: identifier, signal }); 38 + if (!isPlcDid(resolved)) { 39 + throw new Error(`${identifier} is not a valid identifier`); 40 + } 38 41 39 - if (!did.startsWith('did:plc:')) { 40 - throw new Error(`${did} is not plc`); 42 + did = resolved; 43 + } else { 44 + throw new Error(`${identifier} is not a valid identifier`); 41 45 } 42 46 43 47 const logs = await getPlcAuditLogs({ did, signal }); ··· 52 56 53 57 return ( 54 58 <> 55 - <div class="p-4"> 56 - <h1 class="text-lg font-bold text-purple-800">View PLC operation logs</h1> 57 - <p class="text-gray-600">Show history of a did:plc identity</p> 58 - </div> 59 - <hr class="mx-4 border-gray-300" /> 59 + <PageHeader title="View PLC operation logs" subtitle="Show history of a did:plc identity" /> 60 60 61 61 <form 62 62 onSubmit={(ev) => { 63 63 const formData = new FormData(ev.currentTarget); 64 64 ev.preventDefault(); 65 65 66 - const ident = formData.get('ident') as string; 66 + const ident = formData.get('ident') as Did | Handle; 67 67 setParams({ q: ident }); 68 68 }} 69 69 class="m-4 flex flex-col gap-4" ··· 73 73 type="text" 74 74 name="ident" 75 75 autocomplete="username" 76 - pattern={/* @once */ DID_OR_HANDLE_RE.source} 77 76 placeholder="paul.bsky.social" 78 77 autofocus 79 78 /> ··· 372 371 type DiffEntry = 373 372 | { 374 373 type: 'identity_created'; 375 - orig: PlcLogEntry; 374 + orig: IndexedEntry; 376 375 nullified: boolean; 377 376 at: string; 378 377 rotationKeys: string[]; ··· 382 381 } 383 382 | { 384 383 type: 'identity_tombstoned'; 385 - orig: PlcLogEntry; 384 + orig: IndexedEntry; 386 385 nullified: boolean; 387 386 at: string; 388 387 } 389 388 | { 390 389 type: 'rotation_key_added'; 391 - orig: PlcLogEntry; 390 + orig: IndexedEntry; 392 391 nullified: boolean; 393 392 at: string; 394 393 rotation_key: string; 395 394 } 396 395 | { 397 396 type: 'rotation_key_removed'; 398 - orig: PlcLogEntry; 397 + orig: IndexedEntry; 399 398 nullified: boolean; 400 399 at: string; 401 400 rotation_key: string; 402 401 } 403 402 | { 404 403 type: 'verification_method_added'; 405 - orig: PlcLogEntry; 404 + orig: IndexedEntry; 406 405 nullified: boolean; 407 406 at: string; 408 407 method_id: string; ··· 410 409 } 411 410 | { 412 411 type: 'verification_method_removed'; 413 - orig: PlcLogEntry; 412 + orig: IndexedEntry; 414 413 nullified: boolean; 415 414 at: string; 416 415 method_id: string; ··· 418 417 } 419 418 | { 420 419 type: 'verification_method_changed'; 421 - orig: PlcLogEntry; 420 + orig: IndexedEntry; 422 421 nullified: boolean; 423 422 at: string; 424 423 method_id: string; ··· 427 426 } 428 427 | { 429 428 type: 'handle_added'; 430 - orig: PlcLogEntry; 429 + orig: IndexedEntry; 431 430 nullified: boolean; 432 431 at: string; 433 432 handle: string; 434 433 } 435 434 | { 436 435 type: 'handle_removed'; 437 - orig: PlcLogEntry; 436 + orig: IndexedEntry; 438 437 nullified: boolean; 439 438 at: string; 440 439 handle: string; 441 440 } 442 441 | { 443 442 type: 'handle_changed'; 444 - orig: PlcLogEntry; 443 + orig: IndexedEntry; 445 444 nullified: boolean; 446 445 at: string; 447 446 prev_handle: string; ··· 449 448 } 450 449 | { 451 450 type: 'service_added'; 452 - orig: PlcLogEntry; 451 + orig: IndexedEntry; 453 452 nullified: boolean; 454 453 at: string; 455 454 service_id: string; ··· 458 457 } 459 458 | { 460 459 type: 'service_removed'; 461 - orig: PlcLogEntry; 460 + orig: IndexedEntry; 462 461 nullified: boolean; 463 462 at: string; 464 463 service_id: string; ··· 467 466 } 468 467 | { 469 468 type: 'service_changed'; 470 - orig: PlcLogEntry; 469 + orig: IndexedEntry; 471 470 nullified: boolean; 472 471 at: string; 473 472 service_id: string; ··· 477 476 next_service_endpoint: string; 478 477 }; 479 478 480 - const createOperationHistory = (entries: PlcLogEntry[]): DiffEntry[] => { 479 + const createOperationHistory = (entries: IndexedEntry[]): DiffEntry[] => { 481 480 const history: DiffEntry[] = []; 482 481 483 482 for (let idx = 0, len = entries.length; idx < len; idx++) {
-209
src/views/repository/car-unpack.tsx
··· 1 - import { FileSystemWritableFileStream, showSaveFilePicker } from 'native-file-system-adapter'; 2 - import { createSignal } from 'solid-js'; 3 - 4 - import { iterateAtpRepo } from '@atcute/car'; 5 - import { writeTarEntry } from '@mary/tar'; 6 - 7 - import { createDropZone } from '~/lib/hooks/dropzone'; 8 - import { useTitle } from '~/lib/navigation/router'; 9 - import { makeAbortable } from '~/lib/utils/abortable'; 10 - 11 - import Logger, { createLogger } from '~/components/logger'; 12 - 13 - // @ts-expect-error: new API 14 - const yieldToScheduler: () => Promise<void> = window?.scheduler?.yield 15 - ? // @ts-expect-error: whatever 16 - window.scheduler.yield.bind(window.scheduler) 17 - : undefined; 18 - 19 - const yieldToIdle = 20 - typeof requestIdleCallback === 'function' 21 - ? () => new Promise((resolve) => requestIdleCallback(resolve)) 22 - : () => new Promise((resolve) => setTimeout(resolve, 1)); 23 - 24 - const UnpackCarPage = () => { 25 - const logger = createLogger(); 26 - 27 - const [getSignal, cleanup] = makeAbortable(); 28 - const [pending, setPending] = createSignal(false); 29 - 30 - const { ref: dropRef, isDropping } = createDropZone({ 31 - // Checked, the mime type for CAR files is blank. 32 - dataTypes: [''], 33 - multiple: false, 34 - onDrop(files) { 35 - if (files) { 36 - onFileDrop(files); 37 - } 38 - }, 39 - }); 40 - 41 - const mutate = async (file: File, signal: AbortSignal) => { 42 - logger.info(`Starting extraction for ${file.name}`); 43 - 44 - const buf = await file.arrayBuffer(); 45 - const ui8 = new Uint8Array(buf); 46 - 47 - let currentCollection: string | undefined; 48 - let count = 0; 49 - 50 - let writable: FileSystemWritableFileStream | undefined; 51 - 52 - for (const { collection, rkey, record } of iterateAtpRepo(ui8)) { 53 - if (writable === undefined) { 54 - using _progress = logger.progress(`Waiting for the user`); 55 - 56 - const fd = await showSaveFilePicker({ 57 - suggestedName: `${file.name.replace(/\.car$/, '')}.tar`, 58 - 59 - // @ts-expect-error: ponyfill doesn't have the full typings 60 - id: 'car-unpack', 61 - startIn: 'downloads', 62 - types: [ 63 - { 64 - description: 'Tarball archive', 65 - accept: { 'application/tar': ['.tar'] }, 66 - }, 67 - ], 68 - }).catch((err) => { 69 - console.warn(err); 70 - 71 - if (err instanceof DOMException && err.name === 'AbortError') { 72 - logger.warn(`Opened the file picker, but it was aborted`); 73 - } else { 74 - logger.warn(`Something went wrong when opening the file picker`); 75 - } 76 - 77 - return undefined; 78 - }); 79 - 80 - writable = await fd?.createWritable(); 81 - 82 - if (writable === undefined) { 83 - // We already handled the errors above 84 - return; 85 - } 86 - } 87 - 88 - signal.throwIfAborted(); 89 - 90 - if (currentCollection !== collection) { 91 - logger.log(`Current progress: ${collection}`); 92 - currentCollection = collection; 93 - 94 - if (yieldToScheduler === undefined) { 95 - await yieldToIdle(); 96 - } 97 - } 98 - 99 - const entry = writeTarEntry({ 100 - filename: `${collection}/${filenamify(rkey)}.json`, 101 - data: JSON.stringify(record, null, 2), 102 - }); 103 - 104 - writable.write(entry); 105 - count++; 106 - 107 - if (yieldToScheduler !== undefined) { 108 - await yieldToScheduler(); 109 - } 110 - } 111 - 112 - signal.throwIfAborted(); 113 - 114 - if (writable === undefined) { 115 - // If we got here it means the above loop never iterated 116 - logger.log(`CAR file has no records`); 117 - } else { 118 - logger.log(`${count} records extracted`); 119 - 120 - { 121 - using _progress = logger.progress(`Flushing writes`); 122 - await writable.close(); 123 - } 124 - 125 - logger.log(`Finished!`); 126 - } 127 - }; 128 - 129 - const onFileDrop = (files: File[]) => { 130 - if (pending() || files.length < 1) { 131 - return; 132 - } 133 - 134 - const signal = getSignal(); 135 - 136 - setPending(true); 137 - mutate(files[0], signal).then( 138 - () => { 139 - if (signal.aborted) { 140 - return; 141 - } 142 - 143 - cleanup(); 144 - setPending(false); 145 - }, 146 - (err) => { 147 - if (signal.aborted) { 148 - return; 149 - } 150 - 151 - cleanup(); 152 - setPending(false); 153 - 154 - console.error(err); 155 - logger.error(`Critical error: ${err}\nFile might be malformed, or might not be a CAR archive`); 156 - }, 157 - ); 158 - }; 159 - 160 - useTitle(() => `Unpack CAR file โ€” boat`); 161 - 162 - return ( 163 - <> 164 - <div class="p-4"> 165 - <h1 class="text-lg font-bold text-purple-800">Unpack CAR file</h1> 166 - <p class="text-gray-600">Extract a repository archive into a tarball</p> 167 - </div> 168 - <hr class="mx-4 border-gray-300" /> 169 - 170 - <div class="p-4"> 171 - <fieldset 172 - ref={dropRef} 173 - disabled={pending()} 174 - class={ 175 - `grid place-items-center rounded border border-gray-300 px-6 py-12 disabled:opacity-50` + 176 - (pending() || !isDropping() ? ` bg-gray-100` : ` bg-green-100`) 177 - } 178 - > 179 - <div class="flex flex-col items-center gap-4"> 180 - <button 181 - onClick={() => { 182 - const input = document.createElement('input'); 183 - input.type = 'file'; 184 - input.accept = '.car,application/vnd.ipld.car'; 185 - input.oninput = () => onFileDrop(Array.from(input.files!)); 186 - 187 - input.click(); 188 - }} 189 - class="flex h-9 select-none items-center rounded border border-gray-400 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-200 active:bg-gray-200 disabled:pointer-events-none" 190 - > 191 - Browse files 192 - </button> 193 - <p class="select-none font-medium text-gray-600">or drop your file here</p> 194 - </div> 195 - </fieldset> 196 - </div> 197 - <hr class="mx-4 border-gray-300" /> 198 - 199 - <Logger logger={logger} /> 200 - </> 201 - ); 202 - }; 203 - 204 - export default UnpackCarPage; 205 - 206 - const INVALID_CHAR_RE = /[<>:"/\\|?*\x00-\x1F]/g; 207 - const filenamify = (name: string) => { 208 - return name.replace(INVALID_CHAR_RE, '~'); 209 - };
+65
src/views/repository/repo-archive-explore/page.tsx
··· 1 + import { Match, Switch } from 'solid-js'; 2 + 3 + import { fromStream } from '@atcute/repo'; 4 + 5 + import { createMutation } from '~/lib/utils/mutation'; 6 + 7 + import type { Archive, RecordEntry } from './types'; 8 + import ExploreView from './views/explore'; 9 + import WelcomeView from './views/welcome'; 10 + 11 + const ArchiveExplorePage = () => { 12 + const mutation = createMutation({ 13 + async mutationFn({ file }: { file: File }): Promise<Archive> { 14 + const stream = file.stream(); 15 + await using repo = fromStream(stream); 16 + 17 + const collections = new Map<string, RecordEntry[]>(); 18 + const archive: Archive = { 19 + file: file, 20 + entries: [], 21 + }; 22 + 23 + for await (const entry of repo) { 24 + let list = collections.get(entry.collection); 25 + if (list === undefined) { 26 + collections.set(entry.collection, (list = [])); 27 + archive.entries.push({ 28 + name: entry.collection, 29 + entries: list, 30 + }); 31 + } 32 + 33 + const carEntry = entry.carEntry; 34 + 35 + list.push({ 36 + key: entry.rkey, 37 + cid: entry.cid.$link, 38 + dataStart: carEntry.bytesStart, 39 + dataEnd: carEntry.bytesEnd, 40 + }); 41 + } 42 + 43 + return archive; 44 + }, 45 + onError(err) { 46 + console.error(err); 47 + }, 48 + }); 49 + 50 + return ( 51 + <> 52 + <Switch> 53 + <Match when={mutation.data} keyed> 54 + {(archive) => <ExploreView archive={archive} onClose={mutation.reset} />} 55 + </Match> 56 + 57 + <Match when> 58 + <WelcomeView mutation={mutation} /> 59 + </Match> 60 + </Switch> 61 + </> 62 + ); 63 + }; 64 + 65 + export default ArchiveExplorePage;
+29
src/views/repository/repo-archive-explore/types.ts
··· 1 + export interface Archive { 2 + /** Actual CAR file */ 3 + file: File; 4 + /** Collections in the CAR file */ 5 + entries: CollectionEntry[]; 6 + } 7 + 8 + export interface CollectionEntry { 9 + /** Collection name, e.g. "app.bsky.feed.post" */ 10 + name: string; 11 + /** Entries under this collection */ 12 + entries: RecordEntry[]; 13 + } 14 + 15 + export interface RecordEntry { 16 + /** Record key, e.g. "3ll3hjomcxka6" */ 17 + key: string; 18 + /** Record digest, e.g. "bafyreieueqsjugefehodlh4o4idd7fzik3koxno7io7x4qu3q4wofsfjl4" */ 19 + cid: string; 20 + /** Start position of the record in the CAR file */ 21 + dataStart: number; 22 + /** End position of the record in the CAR file */ 23 + dataEnd: number; 24 + } 25 + 26 + export type View = 27 + | { type: 'repo' } 28 + | { type: 'collection'; collection: CollectionEntry } 29 + | { type: 'record'; collection: CollectionEntry; record: RecordEntry };
+41
src/views/repository/repo-archive-explore/views/explore/collection.tsx
··· 1 + import * as TID from '@atcute/tid'; 2 + 3 + import type { CollectionEntry, View } from '../../types'; 4 + 5 + interface CollectionSubviewProps { 6 + collection: CollectionEntry; 7 + onRoute: (view: View) => void; 8 + } 9 + 10 + const CollectionSubview = ({ collection, onRoute }: CollectionSubviewProps) => { 11 + return ( 12 + <div class="px-2 pb-4 pt-0"> 13 + <ul> 14 + {collection.entries.map((entry) => { 15 + const isTid = TID.validate(entry.key); 16 + 17 + return ( 18 + <li class="flex items-center justify-between gap-1"> 19 + <button 20 + onClick={() => { 21 + onRoute({ type: 'record', collection, record: entry }); 22 + }} 23 + class="flex min-w-0 flex-wrap items-center gap-0.5 rounded p-1.5 font-mono hover:bg-gray-200" 24 + > 25 + <span class="truncate font-medium text-purple-700">{/* @once */ entry.key}</span> 26 + </button> 27 + 28 + {isTid && ( 29 + <span class="p-1.5 font-mono text-xs text-gray-600"> 30 + {/* @once */ new Date(TID.parse(entry.key).timestamp / 1_000).toISOString()} 31 + </span> 32 + )} 33 + </li> 34 + ); 35 + })} 36 + </ul> 37 + </div> 38 + ); 39 + }; 40 + 41 + export default CollectionSubview;
+114
src/views/repository/repo-archive-explore/views/explore/record.tsx
··· 1 + import { Match, Switch } from 'solid-js'; 2 + 3 + import * as CBOR from '@atcute/cbor'; 4 + 5 + import { createQuery } from '~/lib/utils/query'; 6 + 7 + import type { Archive, RecordEntry } from '../../types'; 8 + 9 + interface RecordSubviewProps { 10 + archive: Archive; 11 + record: RecordEntry; 12 + } 13 + 14 + const RecordSubview = ({ archive, record }: RecordSubviewProps) => { 15 + const query = createQuery( 16 + () => record, 17 + async (_record, signal) => { 18 + const stream = archive.file.stream(); 19 + 20 + const raw = await readStreamRange(stream, record.dataStart, record.dataEnd, signal); 21 + const decoded = CBOR.decode(raw); 22 + 23 + return { raw, decoded }; 24 + }, 25 + ); 26 + 27 + return ( 28 + <Switch> 29 + <Match when={query.data} keyed> 30 + {({ decoded }) => { 31 + return ( 32 + <div class="flex grow flex-col"> 33 + <textarea 34 + readonly 35 + value={JSON.stringify(decoded, null, 2)} 36 + class="grow resize-none border-0 px-4 pb-4 pt-2 font-mono text-sm" 37 + /> 38 + </div> 39 + ); 40 + }} 41 + </Match> 42 + </Switch> 43 + ); 44 + }; 45 + 46 + export default RecordSubview; 47 + 48 + const readStreamRange = async ( 49 + stream: ReadableStream<Uint8Array>, 50 + start: number, 51 + end: number, 52 + signal?: AbortSignal, 53 + ): Promise<Uint8Array> => { 54 + if (start < 0) { 55 + throw new RangeError(`invalid start position: ${start}`); 56 + } 57 + if (end <= start) { 58 + throw new RangeError(`invalid end position: ${end}`); 59 + } 60 + 61 + const length = end - start; 62 + const result = new Uint8Array(length); 63 + 64 + let read = 0; 65 + let written = 0; 66 + 67 + for await (const chunk of createStreamIterator(stream)) { 68 + signal?.throwIfAborted(); 69 + 70 + if (read + chunk.length <= start) { 71 + read += chunk.length; 72 + continue; 73 + } 74 + 75 + const offset = Math.max(0, start - read); 76 + const toRead = Math.min(chunk.length - offset, length - written); 77 + 78 + result.set(chunk.subarray(offset, offset + toRead), written); 79 + 80 + written += toRead; 81 + read += chunk.length; 82 + 83 + if (written >= length) { 84 + break; 85 + } 86 + } 87 + 88 + return result; 89 + }; 90 + 91 + const createStreamIterator: <T>(stream: ReadableStream<T>) => AsyncIterableIterator<T> = 92 + Symbol.asyncIterator in ReadableStream.prototype 93 + ? // @ts-expect-error 94 + (stream) => stream[Symbol.asyncIterator]() 95 + : (stream) => { 96 + const reader = stream.getReader(); 97 + 98 + return { 99 + [Symbol.asyncIterator]() { 100 + return this; 101 + }, 102 + next() { 103 + return reader.read() as Promise<IteratorResult<any>>; 104 + }, 105 + async return() { 106 + await reader.cancel(); 107 + return { done: true, value: undefined }; 108 + }, 109 + async throw(error: unknown) { 110 + await reader.cancel(error); 111 + return { done: true, value: undefined }; 112 + }, 113 + }; 114 + };
+50
src/views/repository/repo-archive-explore/views/explore/repo.tsx
··· 1 + import ChevronRightIcon from '~/components/ic-icons/baseline-chevron-right'; 2 + 3 + import type { Archive, View } from '../../types'; 4 + 5 + interface RepoSubviewProps { 6 + archive: Archive; 7 + onRoute: (view: View) => void; 8 + } 9 + 10 + const RepoSubview = ({ archive, onRoute }: RepoSubviewProps) => { 11 + return ( 12 + <div class="px-2 pb-4 pt-0"> 13 + <ul> 14 + {archive.entries.map((entry) => { 15 + const hasSingleEntry = entry.entries.length === 1; 16 + 17 + return ( 18 + <li> 19 + <button 20 + onClick={() => { 21 + if (hasSingleEntry) { 22 + onRoute({ type: 'record', collection: entry, record: entry.entries[0] }); 23 + } else { 24 + onRoute({ type: 'collection', collection: entry }); 25 + } 26 + }} 27 + class="flex max-w-full flex-wrap items-center gap-0.5 rounded p-1.5 font-mono hover:bg-gray-200" 28 + > 29 + <span 30 + class={`truncate font-medium` + (hasSingleEntry ? ` text-gray-700` : ` text-purple-700`)} 31 + > 32 + {/* @once */ entry.name} 33 + </span> 34 + 35 + {hasSingleEntry && ( 36 + <> 37 + <ChevronRightIcon class="shrink-0 text-base text-gray-500" /> 38 + <span class="truncate font-medium text-purple-700">{entry.entries[0].key}</span> 39 + </> 40 + )} 41 + </button> 42 + </li> 43 + ); 44 + })} 45 + </ul> 46 + </div> 47 + ); 48 + }; 49 + 50 + export default RepoSubview;
+134
src/views/repository/repo-archive-explore/views/explore.tsx
··· 1 + import { createSignal, Match, Show, Switch } from 'solid-js'; 2 + 3 + import ChevronRightIcon from '~/components/ic-icons/baseline-chevron-right'; 4 + import ArchiveOutlinedIcon from '~/components/ic-icons/outline-archive'; 5 + 6 + import type { Archive, View } from '../types'; 7 + 8 + import CloseIcon from '~/components/ic-icons/baseline-close'; 9 + import CollectionSubview from './explore/collection'; 10 + import RecordSubview from './explore/record'; 11 + import RepoSubview from './explore/repo'; 12 + 13 + export interface ExploreViewProps { 14 + archive: Archive; 15 + onClose: () => void; 16 + } 17 + 18 + const ExploreView = ({ archive, onClose }: ExploreViewProps) => { 19 + const [view, setView] = createSignal<View>({ type: 'repo' }); 20 + 21 + return ( 22 + <> 23 + <div class="sticky top-0 z-10 flex items-start justify-between gap-1 bg-white p-2"> 24 + <div class="flex flex-wrap items-center"> 25 + <button 26 + type="button" 27 + title="This repository" 28 + disabled={view().type === 'repo'} 29 + onClick={() => { 30 + setView({ type: 'repo' }); 31 + }} 32 + class="grid shrink-0 place-items-center rounded p-1.5 text-xl text-purple-700 hover:bg-gray-200 disabled:pointer-events-none disabled:text-black" 33 + > 34 + <ArchiveOutlinedIcon /> 35 + </button> 36 + 37 + <Show 38 + when={(() => { 39 + const $view = view(); 40 + switch ($view.type) { 41 + case 'collection': 42 + case 'record': { 43 + return $view.collection; 44 + } 45 + } 46 + })()} 47 + > 48 + {(collection) => ( 49 + <> 50 + <ChevronRightIcon class="shrink-0 text-base text-gray-500" /> 51 + <button 52 + type="button" 53 + disabled={view().type === 'collection'} 54 + onClick={() => { 55 + setView({ type: 'collection', collection: collection() }); 56 + }} 57 + class="truncate rounded p-1.5 font-mono font-medium text-purple-700 hover:bg-gray-200 disabled:pointer-events-none disabled:text-black" 58 + > 59 + {collection().name} 60 + </button> 61 + </> 62 + )} 63 + </Show> 64 + 65 + <Show 66 + when={(() => { 67 + const $view = view(); 68 + switch ($view.type) { 69 + case 'record': { 70 + return $view.record; 71 + } 72 + } 73 + })()} 74 + > 75 + {(record) => ( 76 + <> 77 + <ChevronRightIcon class="shrink-0 text-base text-gray-500" /> 78 + <button 79 + type="button" 80 + disabled={view().type === 'record'} 81 + class="truncate rounded p-1.5 font-mono font-medium text-purple-700 hover:bg-gray-200 disabled:pointer-events-none disabled:text-black" 82 + > 83 + {record().key} 84 + </button> 85 + </> 86 + )} 87 + </Show> 88 + </div> 89 + 90 + <div class="grow"></div> 91 + 92 + <button 93 + type="button" 94 + onClick={onClose} 95 + class="grid shrink-0 place-items-center rounded p-1.5 text-xl hover:bg-gray-200" 96 + > 97 + <CloseIcon /> 98 + </button> 99 + </div> 100 + 101 + <Switch> 102 + <Match when={view().type === 'repo'}> 103 + <RepoSubview archive={archive} onRoute={setView} /> 104 + </Match> 105 + 106 + <Match 107 + when={(() => { 108 + const $view = view(); 109 + if ($view.type === 'collection') { 110 + return $view; 111 + } 112 + })()} 113 + keyed 114 + > 115 + {({ collection }) => <CollectionSubview collection={collection} onRoute={setView} />} 116 + </Match> 117 + 118 + <Match 119 + when={(() => { 120 + const $view = view(); 121 + if ($view.type === 'record') { 122 + return $view; 123 + } 124 + })()} 125 + keyed 126 + > 127 + {({ record }) => <RecordSubview archive={archive} record={record} />} 128 + </Match> 129 + </Switch> 130 + </> 131 + ); 132 + }; 133 + 134 + export default ExploreView;
+45
src/views/repository/repo-archive-explore/views/welcome.tsx
··· 1 + import { Show } from 'solid-js'; 2 + 3 + import type { MutationReturn } from '~/lib/utils/mutation'; 4 + 5 + import CircularProgress from '~/components/circular-progress'; 6 + import FileDropZone from '~/components/file-drop-zone'; 7 + import PageHeader from '~/components/page-header'; 8 + 9 + import type { Archive } from '../types'; 10 + 11 + interface WelcomeViewProps { 12 + mutation: MutationReturn<Archive, { file: File }>; 13 + } 14 + 15 + const WelcomeView = ({ mutation }: WelcomeViewProps) => { 16 + return ( 17 + <> 18 + <PageHeader title="Explore archive" subtitle="Explore a repository archive" /> 19 + 20 + <div class="flex flex-col gap-4 p-4"> 21 + <FileDropZone 22 + accept=".car,application/vnd.ipld.car" 23 + dataTypes={['']} 24 + onFiles={(files) => mutation.mutate({ file: files[0] })} 25 + > 26 + <div 27 + hidden={!mutation.isPending} 28 + class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-gray-50" 29 + > 30 + <CircularProgress /> 31 + <span class="font-medium">Reading CAR file</span> 32 + </div> 33 + </FileDropZone> 34 + 35 + <Show when={mutation.error}> 36 + <p class="whitespace-pre-wrap text-[0.8125rem] font-medium leading-5 text-red-800"> 37 + {'' + mutation.error} 38 + </p> 39 + </Show> 40 + </div> 41 + </> 42 + ); 43 + }; 44 + 45 + export default WelcomeView;
+170
src/views/repository/repo-archive-unpack.tsx
··· 1 + import { FileSystemWritableFileStream, showSaveFilePicker } from 'native-file-system-adapter'; 2 + import { createSignal } from 'solid-js'; 3 + 4 + import { fromStream } from '@atcute/repo'; 5 + import { writeTarEntry } from '@mary/tar'; 6 + 7 + import { useTitle } from '~/lib/navigation/router'; 8 + import { makeAbortable } from '~/lib/utils/abortable'; 9 + 10 + import FileDropZone from '~/components/file-drop-zone'; 11 + import Logger, { createLogger } from '~/components/logger'; 12 + import PageHeader from '~/components/page-header'; 13 + 14 + // @ts-expect-error: new API 15 + const yieldToScheduler: () => Promise<void> = window?.scheduler?.yield 16 + ? // @ts-expect-error: whatever 17 + window.scheduler.yield.bind(window.scheduler) 18 + : undefined; 19 + 20 + const UnpackCarPage = () => { 21 + const logger = createLogger(); 22 + 23 + const [getSignal, cleanup] = makeAbortable(); 24 + const [pending, setPending] = createSignal(false); 25 + 26 + const mutate = async (file: File, signal: AbortSignal) => { 27 + logger.log(`Starting extraction`); 28 + 29 + const stream = file.stream(); 30 + await using repo = fromStream(stream); 31 + 32 + let count = 0; 33 + 34 + let writable: FileSystemWritableFileStream | undefined; 35 + 36 + using progress = logger.progress(`Unpacking records (${count} entries)`); 37 + 38 + for await (const { collection, rkey, record } of repo) { 39 + if (writable === undefined) { 40 + using _progress = logger.progress(`Waiting for the user`); 41 + 42 + const fd = await showSaveFilePicker({ 43 + suggestedName: `${file.name.replace(/\.car$/, '')}.tar`, 44 + 45 + // @ts-expect-error: ponyfill doesn't have the full typings 46 + id: 'car-unpack', 47 + startIn: 'downloads', 48 + types: [ 49 + { 50 + description: 'Tarball archive', 51 + accept: { 'application/tar': ['.tar'] }, 52 + }, 53 + ], 54 + }).catch((err) => { 55 + console.warn(err); 56 + 57 + if (err instanceof DOMException && err.name === 'AbortError') { 58 + logger.warn(`Opened the file picker, but it was aborted`); 59 + } else { 60 + logger.warn(`Something went wrong when opening the file picker`); 61 + } 62 + 63 + return undefined; 64 + }); 65 + 66 + writable = await fd?.createWritable(); 67 + 68 + if (writable === undefined) { 69 + // We already handled the errors above 70 + return; 71 + } 72 + } 73 + 74 + signal.throwIfAborted(); 75 + 76 + const entry = writeTarEntry({ 77 + filename: `${collection}/${filenamify(rkey)}.json`, 78 + data: JSON.stringify(record, null, 2), 79 + }); 80 + 81 + count++; 82 + 83 + if (count % 100 !== 0) { 84 + writable.write(entry); 85 + } else { 86 + await writable.write(entry); 87 + } 88 + 89 + progress.update(`Unpacking records (${count} entries)`); 90 + 91 + if (yieldToScheduler !== undefined) { 92 + await yieldToScheduler(); 93 + } 94 + } 95 + 96 + signal.throwIfAborted(); 97 + 98 + if (writable === undefined) { 99 + // If we got here it means the above loop never iterated 100 + logger.log(`CAR file has no records`); 101 + } else { 102 + logger.log(`${count} records extracted`); 103 + 104 + { 105 + using _progress = logger.progress(`Flushing writes`); 106 + await writable.close(); 107 + } 108 + 109 + logger.log(`Finished!`); 110 + } 111 + }; 112 + 113 + const onFileDrop = (files: File[]) => { 114 + if (pending() || files.length < 1) { 115 + return; 116 + } 117 + 118 + const signal = getSignal(); 119 + 120 + setPending(true); 121 + mutate(files[0], signal).then( 122 + () => { 123 + if (signal.aborted) { 124 + return; 125 + } 126 + 127 + cleanup(); 128 + setPending(false); 129 + }, 130 + (err) => { 131 + if (signal.aborted) { 132 + return; 133 + } 134 + 135 + cleanup(); 136 + setPending(false); 137 + 138 + console.error(err); 139 + logger.error(`Critical error: ${err}\nFile might be malformed, or might not be a CAR archive`); 140 + }, 141 + ); 142 + }; 143 + 144 + useTitle(() => `Unpack archive โ€” boat`); 145 + 146 + return ( 147 + <> 148 + <PageHeader title="Unpack archive" subtitle="Extract a repository archive into a tarball" /> 149 + 150 + <div class="p-4"> 151 + <FileDropZone 152 + accept=".car,application/vnd.ipld.car" 153 + dataTypes={['']} 154 + disabled={pending()} 155 + onFiles={onFileDrop} 156 + /> 157 + </div> 158 + <hr class="mx-4 border-gray-300" /> 159 + 160 + <Logger logger={logger} /> 161 + </> 162 + ); 163 + }; 164 + 165 + export default UnpackCarPage; 166 + 167 + const INVALID_CHAR_RE = /[<>:"/\\|?*\x00-\x1F]/g; 168 + const filenamify = (name: string) => { 169 + return name.replace(INVALID_CHAR_RE, '~'); 170 + };
+17 -35
src/views/repository/repo-export.tsx
··· 1 1 import { type FileSystemFileHandle, showSaveFilePicker } from 'native-file-system-adapter'; 2 2 import { createSignal } from 'solid-js'; 3 3 4 - import { At } from '@atcute/client/lexicons'; 4 + import { getPdsEndpoint, isAtprotoDid } from '@atcute/identity'; 5 + import { type AtprotoDid, isHandle } from '@atcute/lexicons/syntax'; 5 6 6 7 import { getDidDocument } from '~/api/queries/did-doc'; 7 8 import { resolveHandleViaAppView, resolveHandleViaPds } from '~/api/queries/handle'; 8 - import { getPdsEndpoint } from '~/api/types/did-doc'; 9 9 import { isServiceUrlString } from '~/api/types/strings'; 10 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 11 10 12 11 import { useTitle } from '~/lib/navigation/router'; 13 12 import { makeAbortable } from '~/lib/utils/abortable'; 14 13 import { formatBytes } from '~/lib/utils/intl/bytes'; 14 + import { iterateStream } from '~/lib/utils/stream'; 15 15 16 16 import Button from '~/components/inputs/button'; 17 17 import TextInput from '~/components/inputs/text-input'; 18 18 import Logger, { createLogger } from '~/components/logger'; 19 + import PageHeader from '~/components/page-header'; 19 20 20 21 const RepoExportPage = () => { 21 22 const logger = createLogger(); ··· 34 35 }) => { 35 36 logger.info(`Starting export for ${identifier}`); 36 37 37 - let did: At.DID; 38 - if (isDid(identifier)) { 38 + let did: AtprotoDid; 39 + if (isAtprotoDid(identifier)) { 39 40 did = identifier; 40 - } else if (service) { 41 - did = await resolveHandleViaPds({ service, handle: identifier, signal }); 42 - logger.log(`Resolved handle to ${did}`); 41 + } else if (isHandle(identifier)) { 42 + if (service) { 43 + did = await resolveHandleViaPds({ service, handle: identifier, signal }); 44 + logger.log(`Resolved handle to ${did}`); 45 + } else { 46 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 47 + logger.log(`Resolved handle to ${did}`); 48 + } 43 49 } else { 44 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 45 - logger.log(`Resolved handle to ${did}`); 50 + logger.error(`Invalid identifier`); 51 + return; 46 52 } 47 53 48 54 if (!service) { ··· 131 137 132 138 return ( 133 139 <> 134 - <div class="p-4"> 135 - <h1 class="text-lg font-bold text-purple-800">Export repository</h1> 136 - <p class="text-gray-600">Download an archive of an account's repository</p> 137 - </div> 138 - <hr class="mx-4 border-gray-300" /> 140 + <PageHeader title="Export repository" subtitle="Download an archive of an account's repository" /> 139 141 140 142 <form 141 143 onSubmit={(ev) => { ··· 186 188 type="text" 187 189 name="ident" 188 190 autocomplete="username" 189 - pattern={/* @once */ DID_OR_HANDLE_RE.source} 190 191 placeholder="paul.bsky.social" 191 192 autofocus 192 193 /> ··· 219 220 }; 220 221 221 222 export default RepoExportPage; 222 - 223 - export async function* iterateStream<T>(stream: ReadableStream<T>) { 224 - // Get a lock on the stream 225 - const reader = stream.getReader(); 226 - 227 - try { 228 - while (true) { 229 - const { done, value } = await reader.read(); 230 - 231 - if (done) { 232 - return; 233 - } 234 - 235 - yield value; 236 - } 237 - } finally { 238 - reader.releaseLock(); 239 - } 240 - }
+4 -2
tsconfig.app.json
··· 1 1 { 2 2 "compilerOptions": { 3 3 "target": "ESNext", 4 - "useDefineForClassFields": false, 5 4 "module": "ESNext", 6 5 "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 - "types": [], 6 + "types": ["@atcute/atproto", "@atcute/bluesky"], 8 7 "skipLibCheck": true, 9 8 10 9 "moduleResolution": "Bundler", 11 10 "allowImportingTsExtensions": true, 12 11 "isolatedModules": true, 12 + "verbatimModuleSyntax": true, 13 13 "moduleDetection": "force", 14 14 "noEmit": true, 15 + 15 16 "jsx": "preserve", 16 17 "jsxImportSource": "solid-js", 18 + "useDefineForClassFields": false, 17 19 18 20 "strict": true, 19 21 "noUnusedLocals": true,
+9
wrangler.jsonc
··· 1 + { 2 + "$schema": "https://unpkg.com/wrangler@latest/config-schema.json", 3 + "name": "boat", 4 + "compatibility_date": "2025-10-05", 5 + "assets": { 6 + "directory": "dist", 7 + "not_found_handling": "single-page-application", 8 + }, 9 + }