personal web client for Bluesky
typescript solidjs bluesky atcute

feat!: OAuth login

Squashed commit of the following:

commit 6ad96ed868a1d9d9f45454a88097551943d1a495
Author: Mary <git@mary.my.id>
Date: Fri Aug 30 06:51:44 2024 +0700

don't fetch next page if we're refetching

commit 45b5d1891f65d39be8ed79710c828381ae47e710
Author: Mary <git@mary.my.id>
Date: Fri Aug 30 06:34:38 2024 +0700

add an error boundary dammit

commit f6d9ca022612aac32bd9a02e813f5ed97435b8ea
Author: Mary <git@mary.my.id>
Date: Thu Aug 29 23:34:44 2024 +0700

esnext for optimizedeps

commit bbf7989237bf6e0ef5ff9e0791b829757e3bab37
Author: Mary <git@mary.my.id>
Date: Thu Aug 29 23:19:04 2024 +0700

upgrade dependencies

commit 6f4c277e3851502648023fab50df79f0cff8e3dd
Author: Mary <git@mary.my.id>
Date: Thu Aug 29 22:22:02 2024 +0700

pls

commit 4a917d82b47265d7625f1ae459c42e1999ec8b48
Author: Mary <git@mary.my.id>
Date: Thu Aug 29 22:13:35 2024 +0700

only grab new token set when necessary

commit bb2fd5e87472a0d3817be1766dc231d51eb28d94
Author: Mary <git@mary.my.id>
Date: Thu Aug 29 14:17:22 2024 +0700

wrap the execution flow in lock

commit 176c91c4e96246a2873e994b9fcb2562d8fe30f2
Author: Mary <git@mary.my.id>
Date: Thu Aug 29 14:06:04 2024 +0700

move locks inside the getter

commit d9af704cf2006165f2aed16913dacfaa403e61da
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 23:10:41 2024 +0700

properly abort on back navigation

commit 8ba04463206e900435ef511d1a71b788bdab9a4d
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 21:49:15 2024 +0700

if logged in, use existing session instance to logout

commit 302e15ee5e18cda44d760ace222fcdf0b9569fa7
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 21:48:50 2024 +0700

don't delete tokens on fail

commit d50e76b9a7e642d280add495d88ffd0a36daa940
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 17:11:27 2024 +0700

properly revoke

commit 72e2c1a770083675e7113b9d4f75a551b3de0e14
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 17:07:00 2024 +0700

don't delete automatically for now

commit 248b7e0a3fef549f1eb789d433f2aac48f62785a
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 17:06:42 2024 +0700

missing error description on invalid_grant

commit 5f0a33bf102144904f331e57a1f94befbb3c064d
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 11:36:08 2024 +0700

upgrade dependencies

commit 5fc3b8b2d30a182930816925b98a14e53db6aa68
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 10:44:07 2024 +0700

oops

commit 24f04414960e8f733aefc00dd08a2905c4632296
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 10:42:38 2024 +0700

remove these now that we don't do oidc

commit 985a54c0bf790d41294dedb400234a09ba7884e6
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 10:42:12 2024 +0700

fix: properly display authorization error

commit b0007b991759e961a14f927d137e1665fc27a426
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 10:39:31 2024 +0700

fix: don't retrieve notification count if navbar is not shown

commit 6de12e931195161419ee3d1b210efd459bf31b9a
Author: Mary <git@mary.my.id>
Date: Wed Aug 28 10:37:45 2024 +0700

finalize oauth

commit f5facaccc5f47affb4c60a8a20564176ff304738
Author: Mary <git@mary.my.id>
Date: Sat Aug 24 23:45:21 2024 +0700

relaxed

commit 43053cac8510e56410bf4821ecdcb33afc12b9a6
Author: Mary <git@mary.my.id>
Date: Sat Aug 24 23:44:12 2024 +0700

clear expired

commit 374ee5219922f9196377c1e079f9372747aea0e8
Author: Mary <git@mary.my.id>
Date: Sat Aug 24 23:23:17 2024 +0700

wow it's working

commit 4b6e7e830929c348b47bdabc46ab1322f93671e5
Merge: dcc97bc 9e20fa7
Author: Mary <git@mary.my.id>
Date: Sat Aug 24 12:41:44 2024 +0700

Merge branch 'trunk' into oauth

commit dcc97bce09f287eb4f9620497e774d02e1c5015b
Author: Mary <git@mary.my.id>
Date: Sat Aug 24 12:21:05 2024 +0700

unwrap

commit f42f828ac6de03cd41c40577b678e841f2eec011
Merge: 814b75d 03cb82a
Author: Mary <git@mary.my.id>
Date: Thu Aug 22 17:18:41 2024 +0700

Merge remote-tracking branch 'origin/trunk' into oauth

commit 814b75d5f97fc3af256885ad0ea0e01df5db0020
Author: Mary <git@mary.my.id>
Date: Thu Aug 22 07:45:44 2024 +0700

wip

commit 254ed1d70aa9e4747538bfc2ddddab0c28fe7c8c
Author: Mary <git@mary.my.id>
Date: Wed Aug 21 09:39:28 2024 +0700

fix: don't query notification count if not signed in

mary.my.id 5af1c244 9e20fa78

verified
+8 -8
package.json
··· 10 10 }, 11 11 "dependencies": { 12 12 "@atcute/base32": "^1.0.0", 13 - "@atcute/bluemoji": "^1.0.0", 14 - "@atcute/bluesky": "^1.0.2", 13 + "@atcute/bluemoji": "^1.0.2", 14 + "@atcute/bluesky": "^1.0.4", 15 15 "@atcute/cbor": "^1.0.0", 16 16 "@atcute/cid": "^1.0.0", 17 - "@atcute/client": "^1.0.0", 17 + "@atcute/client": "^2.0.0", 18 18 "@atcute/tid": "^1.0.0", 19 19 "@floating-ui/dom": "^1.6.10", 20 20 "@floating-ui/utils": "^0.2.7", ··· 25 25 "idb": "^8.0.0", 26 26 "nanoid": "^5.0.7", 27 27 "solid-floating-ui": "~0.2.1", 28 - "solid-js": "^1.8.20" 28 + "solid-js": "^1.8.22" 29 29 }, 30 30 "devDependencies": { 31 31 "@types/dom-close-watcher": "^1.0.0", ··· 36 36 "tailwindcss": "^3.4.10", 37 37 "terser": "^5.31.6", 38 38 "typescript": "5.6.0-beta", 39 - "vite": "^5.4.0", 39 + "vite": "^5.4.2", 40 40 "vite-plugin-pwa": "0.17.4", 41 41 "vite-plugin-solid": "^2.10.2", 42 - "wrangler": "^3.71.0" 42 + "wrangler": "^3.72.3" 43 43 }, 44 44 "pnpm": { 45 45 "patchedDependencies": { 46 46 "@tanstack/query-core@5.17.19": "patches/@tanstack__query-core@5.17.19.patch", 47 - "solid-js@1.8.20": "patches/solid-js@1.8.20.patch", 47 + "solid-js": "patches/solid-js.patch", 48 48 "vite-plugin-pwa@0.17.4": "patches/vite-plugin-pwa@0.17.4.patch", 49 - "vite@5.4.0": "patches/vite@5.4.0.patch", 49 + "vite": "patches/vite.patch", 50 50 "workbox-precaching@7.1.0": "patches/workbox-precaching@7.1.0.patch" 51 51 } 52 52 }
+20
patches/solid-js.patch
··· 1 + diff --git a/dist/solid.js b/dist/solid.js 2 + index 953d20d6c542b7ba5f841a10649c2757bafc44d7..7beaac316d86ddc65dbb4fec449303d962fd795f 100644 3 + --- a/dist/solid.js 4 + +++ b/dist/solid.js 5 + @@ -1454,7 +1454,6 @@ function Show(props) { 6 + const child = props.children; 7 + const fn = typeof child === "function" && child.length > 0; 8 + return fn ? untrack(() => child(keyed ? c : () => { 9 + - if (!untrack(condition)) throw narrowedError("Show"); 10 + return props.when; 11 + })) : child; 12 + } 13 + @@ -1485,7 +1484,6 @@ function Switch(props) { 14 + const c = cond.children; 15 + const fn = typeof c === "function" && c.length > 0; 16 + return fn ? untrack(() => c(keyed ? when : () => { 17 + - if (untrack(evalConditions)[0] !== index) throw narrowedError("Match"); 18 + return cond.when; 19 + })) : c; 20 + }, undefined, undefined);
-20
patches/solid-js@1.8.20.patch
··· 1 - diff --git a/dist/solid.js b/dist/solid.js 2 - index 330452ff2dea9deb039a3c13e111292633cced58..0b4497b63214196733b7a187e1f835e0241a7cfb 100644 3 - --- a/dist/solid.js 4 - +++ b/dist/solid.js 5 - @@ -1578,7 +1578,6 @@ function Show(props) { 6 - keyed 7 - ? c 8 - : () => { 9 - - if (!untrack(condition)) throw narrowedError("Show"); 10 - return props.when; 11 - } 12 - ) 13 - @@ -1625,7 +1624,6 @@ function Switch(props) { 14 - keyed 15 - ? when 16 - : () => { 17 - - if (untrack(evalConditions)[0] !== index) throw narrowedError("Match"); 18 - return cond.when; 19 - } 20 - )
+13
patches/vite.patch
··· 1 + diff --git a/dist/node/chunks/dep-BzOvws4Y.js b/dist/node/chunks/dep-BzOvws4Y.js 2 + index 184597a2d7b1acc23556e16264e6677457023e81..d98dc4f1d3457ce2705aa0badbb972780e238060 100644 3 + --- a/dist/node/chunks/dep-BzOvws4Y.js 4 + +++ b/dist/node/chunks/dep-BzOvws4Y.js 5 + @@ -65109,7 +65109,7 @@ async function resolveBuildPlugins(config) { 6 + ...config.isWorker ? [webWorkerPostPlugin()] : [] 7 + ], 8 + post: [ 9 + - buildImportAnalysisPlugin(config), 10 + + ...(config.build.modulePreload !== false ? [buildImportAnalysisPlugin(config)] : []), 11 + ...config.esbuild !== false ? [buildEsbuildPlugin(config)] : [], 12 + ...options.minify ? [terserPlugin(config)] : [], 13 + ...!config.isWorker ? [
-13
patches/vite@5.4.0.patch
··· 1 - diff --git a/dist/node/chunks/dep-NjL7WTE1.js b/dist/node/chunks/dep-NjL7WTE1.js 2 - index e8ab22b8512cc988241bff18d899ffdd897a7d3a..983e14481c1e90c38ec3d220b679c8f7497fafa5 100644 3 - --- a/dist/node/chunks/dep-NjL7WTE1.js 4 - +++ b/dist/node/chunks/dep-NjL7WTE1.js 5 - @@ -65095,7 +65095,7 @@ async function resolveBuildPlugins(config) { 6 - ...config.isWorker ? [webWorkerPostPlugin()] : [] 7 - ], 8 - post: [ 9 - - buildImportAnalysisPlugin(config), 10 - + ...(config.build.modulePreload !== false ? [buildImportAnalysisPlugin(config)] : []), 11 - ...config.esbuild !== false ? [buildEsbuildPlugin(config)] : [], 12 - ...options.minify ? [terserPlugin(config)] : [], 13 - ...!config.isWorker ? [
+280 -345
pnpm-lock.yaml
··· 8 8 '@tanstack/query-core@5.17.19': 9 9 hash: mh34qchsf4y2z6x2owv2ljabky 10 10 path: patches/@tanstack__query-core@5.17.19.patch 11 - solid-js@1.8.20: 12 - hash: 76exa6qxo2meqzgohn3ssck4qe 13 - path: patches/solid-js@1.8.20.patch 11 + solid-js: 12 + hash: 5rodyfcb76rtbo26dwlsojy7jy 13 + path: patches/solid-js.patch 14 + vite: 15 + hash: enol6dkeaosc6vsynualw3gkvi 16 + path: patches/vite.patch 14 17 vite-plugin-pwa@0.17.4: 15 18 hash: ve5hypcrajivuvoyst6zln6qyq 16 19 path: patches/vite-plugin-pwa@0.17.4.patch 17 - vite@5.4.0: 18 - hash: ll22cxodgkjat6cp6mjwk5dp4y 19 - path: patches/vite@5.4.0.patch 20 20 workbox-precaching@7.1.0: 21 21 hash: uwqzx25dqx6gokakqgp7nxcupi 22 22 path: patches/workbox-precaching@7.1.0.patch ··· 29 29 specifier: ^1.0.0 30 30 version: 1.0.0 31 31 '@atcute/bluemoji': 32 - specifier: ^1.0.0 33 - version: 1.0.0(@atcute/bluesky@1.0.2(@atcute/client@1.0.0))(@atcute/client@1.0.0) 34 - '@atcute/bluesky': 35 32 specifier: ^1.0.2 36 - version: 1.0.2(@atcute/client@1.0.0) 33 + version: 1.0.2(@atcute/bluesky@1.0.4(@atcute/client@2.0.0))(@atcute/client@2.0.0) 34 + '@atcute/bluesky': 35 + specifier: ^1.0.4 36 + version: 1.0.4(@atcute/client@2.0.0) 37 37 '@atcute/cbor': 38 38 specifier: ^1.0.0 39 39 version: 1.0.0 ··· 41 41 specifier: ^1.0.0 42 42 version: 1.0.0 43 43 '@atcute/client': 44 - specifier: ^1.0.0 45 - version: 1.0.0 44 + specifier: ^2.0.0 45 + version: 2.0.0 46 46 '@atcute/tid': 47 47 specifier: ^1.0.0 48 48 version: 1.0.0 ··· 60 60 version: '@jsr/mary__exif-rm@0.2.1' 61 61 '@mary/solid-freeze': 62 62 specifier: npm:@externdefs/solid-freeze@^0.1.1 63 - version: '@externdefs/solid-freeze@0.1.1(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe))' 63 + version: '@externdefs/solid-freeze@0.1.1(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy))' 64 64 '@mary/solid-query': 65 65 specifier: npm:@externdefs/solid-query@^0.1.5 66 - version: '@externdefs/solid-query@0.1.5(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe))' 66 + version: '@externdefs/solid-query@0.1.5(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy))' 67 67 idb: 68 68 specifier: ^8.0.0 69 69 version: 8.0.0 ··· 72 72 version: 5.0.7 73 73 solid-floating-ui: 74 74 specifier: ~0.2.1 75 - version: 0.2.1(@floating-ui/dom@1.6.10)(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe)) 75 + version: 0.2.1(@floating-ui/dom@1.6.10)(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy)) 76 76 solid-js: 77 - specifier: ^1.8.20 78 - version: 1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe) 77 + specifier: ^1.8.22 78 + version: 1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy) 79 79 devDependencies: 80 80 '@types/dom-close-watcher': 81 81 specifier: ^1.0.0 ··· 102 102 specifier: 5.6.0-beta 103 103 version: 5.6.0-beta 104 104 vite: 105 - specifier: ^5.4.0 106 - version: 5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6) 105 + specifier: ^5.4.2 106 + version: 5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6) 107 107 vite-plugin-pwa: 108 108 specifier: 0.17.4 109 - version: 0.17.4(patch_hash=ve5hypcrajivuvoyst6zln6qyq)(vite@5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) 109 + version: 0.17.4(patch_hash=ve5hypcrajivuvoyst6zln6qyq)(vite@5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) 110 110 vite-plugin-solid: 111 111 specifier: ^2.10.2 112 - version: 2.10.2(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe))(vite@5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6)) 112 + version: 2.10.2(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy))(vite@5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6)) 113 113 wrangler: 114 - specifier: ^3.71.0 115 - version: 3.71.0 114 + specifier: ^3.72.3 115 + version: 3.72.3 116 116 117 117 packages: 118 118 ··· 133 133 '@atcute/base32@1.0.0': 134 134 resolution: {integrity: sha512-Mbjsv6kd/ymvDMGjCoh9eqhlpFsoJ6zYguU6xtKxqh1wGhe5rvBOfMRXsEqcp7srn8Bfp8QhevqLgmwrWvzqrA==} 135 135 136 - '@atcute/bluemoji@1.0.0': 137 - resolution: {integrity: sha512-roxi2DaN9Xuy4YcCj+ADYIeva2qEilsAmJOBqgPWmyR+myxFNsL4Fme5u3iddRmyCdzQQmW8tIgpXF4cnmdIPw==} 136 + '@atcute/bluemoji@1.0.2': 137 + resolution: {integrity: sha512-DlFSXgRMSXMPgeXeEg0AheSIZVBKQnNLXJbSPBgoJo/j+BUeh2FsIXe1gQKizy5JM1ww1BUlEFXHdK4NmY96Hg==} 138 138 peerDependencies: 139 139 '@atcute/bluesky': ^1.0.0 140 - '@atcute/client': ^1.0.0 140 + '@atcute/client': ^1.0.0 || ^2.0.0 141 141 142 - '@atcute/bluesky@1.0.2': 143 - resolution: {integrity: sha512-CHDj9oKh8XKgTKVZlSYKXf/qdjGZAkibtcQA/kLZy/pY495BH2aF2RoAR5988XLbwDoebHgKstB8UfaycRDDwA==} 142 + '@atcute/bluesky@1.0.4': 143 + resolution: {integrity: sha512-gG22ZQpg8gDYO7HdfJEZR6hIjCdCRl3zyUY6K16GTVU5KWn1x+VMyuoJeaFj5CwveUPF8OYMM3ZSOoKcvuhRFA==} 144 144 peerDependencies: 145 - '@atcute/client': ^1.0.0 145 + '@atcute/client': ^1.0.0 || ^2.0.0 146 146 147 147 '@atcute/cbor@1.0.0': 148 148 resolution: {integrity: sha512-aHbURHim6cem7ZRLYg+Q9CkbGAPAV9P2pms7V/p5OkpP/dAb7RgoFwf49vg1454xrCtfFOhCtheUnmxLROdG3Q==} ··· 150 150 '@atcute/cid@1.0.0': 151 151 resolution: {integrity: sha512-JnWv3sg48zDBP318ErPYPI482Vw1Nm7e7WG+VYGSLRLp56b9LgcIh28p28gEmPtmsnM9hTAkKvJdi+CAkNDQUA==} 152 152 153 - '@atcute/client@1.0.0': 154 - resolution: {integrity: sha512-C9Xvl1vaCjy6xLRE/uprDgWyyp/efM0HDqkeROcUSA1nuIEYL1wXqbMRFiwT6y9b81GHO8GAJ+p/pEDGIHOJ0A==} 153 + '@atcute/client@2.0.0': 154 + resolution: {integrity: sha512-ifbYegz/4mUCqByz1G9msDcOJaIxnh0z6z8GPwlqNM9VUhQc/tEsJl7R6eeDp1DKonzhL+fQ//6OMY7w/tzTYw==} 155 155 156 156 '@atcute/tid@1.0.0': 157 157 resolution: {integrity: sha512-hwvo19zlhSskYRmIzTQqj1Kxd9IX+x8B593flXTaR5jO+/3krUwlRnXd/pnZp5wZqAI/j2OlcPGadX93rsoozg==} ··· 161 161 162 162 '@babel/code-frame@7.24.7': 163 163 resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} 164 - engines: {node: '>=6.9.0'} 165 - 166 - '@babel/compat-data@7.25.2': 167 - resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==} 168 164 engines: {node: '>=6.9.0'} 169 165 170 166 '@babel/compat-data@7.25.4': ··· 175 171 resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} 176 172 engines: {node: '>=6.9.0'} 177 173 178 - '@babel/generator@7.25.0': 179 - resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} 180 - engines: {node: '>=6.9.0'} 181 - 182 - '@babel/generator@7.25.5': 183 - resolution: {integrity: sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==} 174 + '@babel/generator@7.25.6': 175 + resolution: {integrity: sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==} 184 176 engines: {node: '>=6.9.0'} 185 177 186 178 '@babel/helper-annotate-as-pure@7.24.7': ··· 274 266 resolution: {integrity: sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==} 275 267 engines: {node: '>=6.9.0'} 276 268 277 - '@babel/helpers@7.25.0': 278 - resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} 269 + '@babel/helpers@7.25.6': 270 + resolution: {integrity: sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==} 279 271 engines: {node: '>=6.9.0'} 280 272 281 273 '@babel/highlight@7.24.7': 282 274 resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} 283 275 engines: {node: '>=6.9.0'} 284 276 285 - '@babel/parser@7.25.3': 286 - resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==} 287 - engines: {node: '>=6.0.0'} 288 - hasBin: true 289 - 290 - '@babel/parser@7.25.4': 291 - resolution: {integrity: sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==} 277 + '@babel/parser@7.25.6': 278 + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} 292 279 engines: {node: '>=6.0.0'} 293 280 hasBin: true 294 281 ··· 354 341 peerDependencies: 355 342 '@babel/core': ^7.0.0-0 356 343 357 - '@babel/plugin-syntax-import-assertions@7.24.7': 358 - resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} 344 + '@babel/plugin-syntax-import-assertions@7.25.6': 345 + resolution: {integrity: sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==} 359 346 engines: {node: '>=6.9.0'} 360 347 peerDependencies: 361 348 '@babel/core': ^7.0.0-0 362 349 363 - '@babel/plugin-syntax-import-attributes@7.24.7': 364 - resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} 350 + '@babel/plugin-syntax-import-attributes@7.25.6': 351 + resolution: {integrity: sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==} 365 352 engines: {node: '>=6.9.0'} 366 353 peerDependencies: 367 354 '@babel/core': ^7.0.0-0 ··· 738 725 '@babel/regjsgen@0.8.0': 739 726 resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} 740 727 741 - '@babel/runtime@7.25.4': 742 - resolution: {integrity: sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==} 728 + '@babel/runtime@7.25.6': 729 + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} 743 730 engines: {node: '>=6.9.0'} 744 731 745 732 '@babel/template@7.25.0': 746 733 resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} 747 734 engines: {node: '>=6.9.0'} 748 735 749 - '@babel/traverse@7.25.3': 750 - resolution: {integrity: sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==} 736 + '@babel/traverse@7.25.6': 737 + resolution: {integrity: sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==} 751 738 engines: {node: '>=6.9.0'} 752 739 753 - '@babel/traverse@7.25.4': 754 - resolution: {integrity: sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==} 755 - engines: {node: '>=6.9.0'} 756 - 757 - '@babel/types@7.25.2': 758 - resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} 759 - engines: {node: '>=6.9.0'} 760 - 761 - '@babel/types@7.25.4': 762 - resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==} 740 + '@babel/types@7.25.6': 741 + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} 763 742 engines: {node: '>=6.9.0'} 764 743 765 744 '@cloudflare/kv-asset-handler@0.3.4': 766 745 resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} 767 746 engines: {node: '>=16.13'} 768 747 769 - '@cloudflare/workerd-darwin-64@1.20240806.0': 770 - resolution: {integrity: sha512-FqcVBBCO//I39K5F+HqE/v+UkqY1UrRnS653Jv+XsNNH9TpX5fTs7VCKG4kDSnmxlAaKttyIN5sMEt7lpuNExQ==} 748 + '@cloudflare/workerd-darwin-64@1.20240821.1': 749 + resolution: {integrity: sha512-CDBpfZKrSy4YrIdqS84z67r3Tzal2pOhjCsIb63IuCnvVes59/ft1qhczBzk9EffeOE2iTCrA4YBT7Sbn7USew==} 771 750 engines: {node: '>=16'} 772 751 cpu: [x64] 773 752 os: [darwin] 774 753 775 - '@cloudflare/workerd-darwin-arm64@1.20240806.0': 776 - resolution: {integrity: sha512-8c3KvmzYp/wg+82KHSOzDetJK+pThH4MTrU1OsjmsR2cUfedm5dk5Lah9/0Ld68+6A0umFACi4W2xJHs/RoBpA==} 754 + '@cloudflare/workerd-darwin-arm64@1.20240821.1': 755 + resolution: {integrity: sha512-Q+9RedvNbPcEt/dKni1oN94OxbvuNAeJkgHmrLFTGF8zu21wzOhVkQeRNxcYxrMa9mfStc457NAg13OVCj2kHQ==} 777 756 engines: {node: '>=16'} 778 757 cpu: [arm64] 779 758 os: [darwin] 780 759 781 - '@cloudflare/workerd-linux-64@1.20240806.0': 782 - resolution: {integrity: sha512-/149Bpxw4e2p5QqnBc06g0mx+4sZYh9j0doilnt0wk/uqYkLp0DdXGMQVRB74sBLg2UD3wW8amn1w3KyFhK2tQ==} 760 + '@cloudflare/workerd-linux-64@1.20240821.1': 761 + resolution: {integrity: sha512-j6z3KsPtawrscoLuP985LbqFrmsJL6q1mvSXOXTqXGODAHIzGBipHARdOjms3UQqovzvqB2lQaQsZtLBwCZxtA==} 783 762 engines: {node: '>=16'} 784 763 cpu: [x64] 785 764 os: [linux] 786 765 787 - '@cloudflare/workerd-linux-arm64@1.20240806.0': 788 - resolution: {integrity: sha512-lacDWY3S1rKL/xT6iMtTQJEKmTTKrBavPczInEuBFXElmrS6IwVjZwv8hhVm32piyNt/AuFu9BYoJALi9D85/g==} 766 + '@cloudflare/workerd-linux-arm64@1.20240821.1': 767 + resolution: {integrity: sha512-I9bHgZOxJQW0CV5gTdilyxzTG7ILzbTirehQWgfPx9X77E/7eIbR9sboOMgyeC69W4he0SKtpx0sYZuTJu4ERw==} 789 768 engines: {node: '>=16'} 790 769 cpu: [arm64] 791 770 os: [linux] 792 771 793 - '@cloudflare/workerd-windows-64@1.20240806.0': 794 - resolution: {integrity: sha512-hC6JEfTSQK6//Lg+D54TLVn1ceTPY+fv4MXqDZIYlPP53iN+dL8Xd0utn2SG57UYdlL5FRAhm/EWHcATZg1RgA==} 772 + '@cloudflare/workerd-windows-64@1.20240821.1': 773 + resolution: {integrity: sha512-keC97QPArs6LWbPejQM7/Y8Jy8QqyaZow4/ZdsGo+QjlOLiZRDpAenfZx3CBUoWwEeFwQTl2FLO+8hV1SWFFYw==} 795 774 engines: {node: '>=16'} 796 775 cpu: [x64] 797 776 os: [win32] 798 777 799 - '@cloudflare/workers-shared@0.1.0': 800 - resolution: {integrity: sha512-SyD4iw6jM4anZaG+ujgVETV4fulF2KHBOW31eavbVN7TNpk2l4aJgwY1YSPK00IKSWsoQuH2TigR446KuT5lqQ==} 778 + '@cloudflare/workers-shared@0.4.0': 779 + resolution: {integrity: sha512-XAFOldVQsbxQ7mjbqX2q1dNIgcLbKSytk41pwuZTn9e0p7OeTpFTosJef8uwosL6CcOAHqcW1f1HJxyjwmtGxw==} 780 + engines: {node: '>=16.7.0'} 801 781 802 782 '@cspotcode/source-map-support@0.8.1': 803 783 resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} ··· 1205 1185 rollup: 1206 1186 optional: true 1207 1187 1208 - '@rollup/rollup-android-arm-eabi@4.20.0': 1209 - resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} 1188 + '@rollup/rollup-android-arm-eabi@4.21.1': 1189 + resolution: {integrity: sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==} 1210 1190 cpu: [arm] 1211 1191 os: [android] 1212 1192 1213 - '@rollup/rollup-android-arm64@4.20.0': 1214 - resolution: {integrity: sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==} 1193 + '@rollup/rollup-android-arm64@4.21.1': 1194 + resolution: {integrity: sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==} 1215 1195 cpu: [arm64] 1216 1196 os: [android] 1217 1197 1218 - '@rollup/rollup-darwin-arm64@4.20.0': 1219 - resolution: {integrity: sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==} 1198 + '@rollup/rollup-darwin-arm64@4.21.1': 1199 + resolution: {integrity: sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==} 1220 1200 cpu: [arm64] 1221 1201 os: [darwin] 1222 1202 1223 - '@rollup/rollup-darwin-x64@4.20.0': 1224 - resolution: {integrity: sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==} 1203 + '@rollup/rollup-darwin-x64@4.21.1': 1204 + resolution: {integrity: sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==} 1225 1205 cpu: [x64] 1226 1206 os: [darwin] 1227 1207 1228 - '@rollup/rollup-linux-arm-gnueabihf@4.20.0': 1229 - resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==} 1208 + '@rollup/rollup-linux-arm-gnueabihf@4.21.1': 1209 + resolution: {integrity: sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==} 1230 1210 cpu: [arm] 1231 1211 os: [linux] 1232 1212 1233 - '@rollup/rollup-linux-arm-musleabihf@4.20.0': 1234 - resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==} 1213 + '@rollup/rollup-linux-arm-musleabihf@4.21.1': 1214 + resolution: {integrity: sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==} 1235 1215 cpu: [arm] 1236 1216 os: [linux] 1237 1217 1238 - '@rollup/rollup-linux-arm64-gnu@4.20.0': 1239 - resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==} 1218 + '@rollup/rollup-linux-arm64-gnu@4.21.1': 1219 + resolution: {integrity: sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==} 1240 1220 cpu: [arm64] 1241 1221 os: [linux] 1242 1222 1243 - '@rollup/rollup-linux-arm64-musl@4.20.0': 1244 - resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==} 1223 + '@rollup/rollup-linux-arm64-musl@4.21.1': 1224 + resolution: {integrity: sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==} 1245 1225 cpu: [arm64] 1246 1226 os: [linux] 1247 1227 1248 - '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': 1249 - resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==} 1228 + '@rollup/rollup-linux-powerpc64le-gnu@4.21.1': 1229 + resolution: {integrity: sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==} 1250 1230 cpu: [ppc64] 1251 1231 os: [linux] 1252 1232 1253 - '@rollup/rollup-linux-riscv64-gnu@4.20.0': 1254 - resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==} 1233 + '@rollup/rollup-linux-riscv64-gnu@4.21.1': 1234 + resolution: {integrity: sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==} 1255 1235 cpu: [riscv64] 1256 1236 os: [linux] 1257 1237 1258 - '@rollup/rollup-linux-s390x-gnu@4.20.0': 1259 - resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==} 1238 + '@rollup/rollup-linux-s390x-gnu@4.21.1': 1239 + resolution: {integrity: sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==} 1260 1240 cpu: [s390x] 1261 1241 os: [linux] 1262 1242 1263 - '@rollup/rollup-linux-x64-gnu@4.20.0': 1264 - resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==} 1243 + '@rollup/rollup-linux-x64-gnu@4.21.1': 1244 + resolution: {integrity: sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==} 1265 1245 cpu: [x64] 1266 1246 os: [linux] 1267 1247 1268 - '@rollup/rollup-linux-x64-musl@4.20.0': 1269 - resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==} 1248 + '@rollup/rollup-linux-x64-musl@4.21.1': 1249 + resolution: {integrity: sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==} 1270 1250 cpu: [x64] 1271 1251 os: [linux] 1272 1252 1273 - '@rollup/rollup-win32-arm64-msvc@4.20.0': 1274 - resolution: {integrity: sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==} 1253 + '@rollup/rollup-win32-arm64-msvc@4.21.1': 1254 + resolution: {integrity: sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==} 1275 1255 cpu: [arm64] 1276 1256 os: [win32] 1277 1257 1278 - '@rollup/rollup-win32-ia32-msvc@4.20.0': 1279 - resolution: {integrity: sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==} 1258 + '@rollup/rollup-win32-ia32-msvc@4.21.1': 1259 + resolution: {integrity: sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==} 1280 1260 cpu: [ia32] 1281 1261 os: [win32] 1282 1262 1283 - '@rollup/rollup-win32-x64-msvc@4.20.0': 1284 - resolution: {integrity: sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==} 1263 + '@rollup/rollup-win32-x64-msvc@4.21.1': 1264 + resolution: {integrity: sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==} 1285 1265 cpu: [x64] 1286 1266 os: [win32] 1287 1267 ··· 1315 1295 '@types/node-forge@1.3.11': 1316 1296 resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} 1317 1297 1318 - '@types/node@22.3.0': 1319 - resolution: {integrity: sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==} 1320 - 1321 - '@types/node@22.5.0': 1322 - resolution: {integrity: sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==} 1298 + '@types/node@22.5.1': 1299 + resolution: {integrity: sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==} 1323 1300 1324 1301 '@types/resolve@1.20.2': 1325 1302 resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} ··· 1398 1375 resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} 1399 1376 engines: {node: '>= 0.4'} 1400 1377 1401 - babel-plugin-jsx-dom-expressions@0.38.1: 1402 - resolution: {integrity: sha512-4FD4H69Cu4jHx2uLDEvx4YC5T/fC/Dmaafhsm8hXm7SjHYzjr09gBVyHdoFza+91f/g9e6tIzjbLCMkOXwmlew==} 1378 + babel-plugin-jsx-dom-expressions@0.38.5: 1379 + resolution: {integrity: sha512-JfjHYKOKGwoiOYQ56Oo8gbZPb9wNMpPuEEUhSCjMpnuHM9K21HFIUBm83TZPB40Av4caCIW4Tfjzpkp/MtFpMw==} 1403 1380 peerDependencies: 1404 1381 '@babel/core': ^7.20.12 1405 1382 ··· 1423 1400 peerDependencies: 1424 1401 '@babel/core': ^7.24.4 1425 1402 1426 - babel-preset-solid@1.8.19: 1427 - resolution: {integrity: sha512-F3MoUdx3i4znhStnXUBno+5kGSbvhpbGrPgqfRPrS8W7foVJUOSd1/F9QDyd9dgClHfr+J7V14931eu1PEDDMQ==} 1403 + babel-preset-solid@1.8.22: 1404 + resolution: {integrity: sha512-nKwisb//lZsiRF2NErlRP64zVTJqa1OSZiDnSl0YbcTiCZoMt52CY2Pg+9fsYAPtjYMT7RHBmzU41pxK6hFOcg==} 1428 1405 peerDependencies: 1429 1406 '@babel/core': ^7.0.0 1430 1407 ··· 1468 1445 resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 1469 1446 engines: {node: '>= 6'} 1470 1447 1471 - caniuse-lite@1.0.30001651: 1472 - resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} 1448 + caniuse-lite@1.0.30001653: 1449 + resolution: {integrity: sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==} 1473 1450 1474 1451 capnp-ts@0.7.0: 1475 1452 resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} ··· 1599 1576 engines: {node: '>=0.10.0'} 1600 1577 hasBin: true 1601 1578 1602 - electron-to-chromium@1.5.5: 1603 - resolution: {integrity: sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==} 1579 + electron-to-chromium@1.5.13: 1580 + resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} 1604 1581 1605 1582 emoji-regex@8.0.0: 1606 1583 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} ··· 1853 1830 resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} 1854 1831 engines: {node: '>= 0.4'} 1855 1832 1856 - is-core-module@2.15.0: 1857 - resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} 1833 + is-core-module@2.15.1: 1834 + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} 1858 1835 engines: {node: '>= 0.4'} 1859 1836 1860 1837 is-data-view@1.0.1: ··· 2020 1997 resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 2021 1998 engines: {node: '>= 8'} 2022 1999 2023 - micromatch@4.0.7: 2024 - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} 2000 + micromatch@4.0.8: 2001 + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 2025 2002 engines: {node: '>=8.6'} 2026 2003 2027 2004 mime@3.0.0: ··· 2029 2006 engines: {node: '>=10.0.0'} 2030 2007 hasBin: true 2031 2008 2032 - miniflare@3.20240806.0: 2033 - resolution: {integrity: sha512-jDsXBJOLUVpIQXHsluX3xV0piDxXolTCsxdje2Ex2LTC9PsSoBIkMwvCmnCxe9wpJJCq8rb0UMyeEn3KOF3LOw==} 2009 + miniflare@3.20240821.0: 2010 + resolution: {integrity: sha512-4BhLGpssQxM/O6TZmJ10GkT3wBJK6emFkZ3V87/HyvQmVt8zMxEBvyw5uv6kdtp+7F54Nw6IKFJjPUL8rFVQrQ==} 2034 2011 engines: {node: '>=16.13'} 2035 2012 hasBin: true 2036 2013 ··· 2338 2315 engines: {node: '>=10.0.0'} 2339 2316 hasBin: true 2340 2317 2341 - rollup@4.20.0: 2342 - resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==} 2318 + rollup@4.21.1: 2319 + resolution: {integrity: sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==} 2343 2320 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 2344 2321 hasBin: true 2345 2322 ··· 2412 2389 '@floating-ui/dom': ^1.0 2413 2390 solid-js: ^1.3 2414 2391 2415 - solid-js@1.8.20: 2416 - resolution: {integrity: sha512-SsgaExCJ97mPm9WpAusjZ484Z8zTp8ggiueQOsrm81iAP7UaxaN+wiOgnPcJ9u6B2SQpoQ4FiDPAZBqVWi1V4g==} 2392 + solid-js@1.8.22: 2393 + resolution: {integrity: sha512-VBzN5j+9Y4rqIKEnK301aBk+S7fvFSTs9ljg+YEdFxjNjH0hkjXPiQRcws9tE5fUzMznSS6KToL5hwMfHDgpLA==} 2417 2394 2418 2395 solid-refresh@0.6.3: 2419 2396 resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} ··· 2541 2518 ts-interface-checker@0.1.13: 2542 2519 resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 2543 2520 2544 - tslib@2.6.3: 2545 - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} 2521 + tslib@2.7.0: 2522 + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} 2546 2523 2547 2524 type-fest@0.16.0: 2548 2525 resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} ··· 2574 2551 2575 2552 unbox-primitive@1.0.2: 2576 2553 resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} 2577 - 2578 - undici-types@6.18.2: 2579 - resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} 2580 2554 2581 2555 undici-types@6.19.8: 2582 2556 resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} ··· 2646 2620 '@testing-library/jest-dom': 2647 2621 optional: true 2648 2622 2649 - vite@5.4.0: 2650 - resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==} 2623 + vite@5.4.2: 2624 + resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==} 2651 2625 engines: {node: ^18.0.0 || >=20.0.0} 2652 2626 hasBin: true 2653 2627 peerDependencies: ··· 2752 2726 workbox-window@7.1.0: 2753 2727 resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==} 2754 2728 2755 - workerd@1.20240806.0: 2756 - resolution: {integrity: sha512-yyNtyzTMgVY0sgYijHBONqZFVXsOFGj2jDjS8MF/RbO2ZdGROvs4Hkc/9QnmqFWahE0STxXeJ1yW1yVotdF0UQ==} 2729 + workerd@1.20240821.1: 2730 + resolution: {integrity: sha512-y4phjCnEG96u8ZkgkkHB+gSw0i6uMNo23rBmixylWpjxDklB+LWD8dztasvsu7xGaZbLoTxQESdEw956F7VJDA==} 2757 2731 engines: {node: '>=16'} 2758 2732 hasBin: true 2759 2733 2760 - wrangler@3.71.0: 2761 - resolution: {integrity: sha512-WHWBmU2z0p1hRtSIIP5HEeoR+6aNpuZR82HMXAwpfeiyijjfkMyt/TUs8gvIOKC3x3+ETQQIdVeX2al5KtrIxQ==} 2734 + wrangler@3.72.3: 2735 + resolution: {integrity: sha512-EBlJGOcwanbzFkiJkRB47WKhvevh1AZK0ty0MyD0gptsgWnAxBfmFGiBuzOuRXbvH45ZrFrTqgi8c67EwcV1nA==} 2762 2736 engines: {node: '>=16.17.0'} 2763 2737 hasBin: true 2764 2738 peerDependencies: 2765 - '@cloudflare/workers-types': ^4.20240806.0 2739 + '@cloudflare/workers-types': ^4.20240821.1 2766 2740 peerDependenciesMeta: 2767 2741 '@cloudflare/workers-types': 2768 2742 optional: true ··· 2825 2799 2826 2800 '@atcute/base32@1.0.0': {} 2827 2801 2828 - '@atcute/bluemoji@1.0.0(@atcute/bluesky@1.0.2(@atcute/client@1.0.0))(@atcute/client@1.0.0)': 2802 + '@atcute/bluemoji@1.0.2(@atcute/bluesky@1.0.4(@atcute/client@2.0.0))(@atcute/client@2.0.0)': 2829 2803 dependencies: 2830 - '@atcute/bluesky': 1.0.2(@atcute/client@1.0.0) 2831 - '@atcute/client': 1.0.0 2804 + '@atcute/bluesky': 1.0.4(@atcute/client@2.0.0) 2805 + '@atcute/client': 2.0.0 2832 2806 2833 - '@atcute/bluesky@1.0.2(@atcute/client@1.0.0)': 2807 + '@atcute/bluesky@1.0.4(@atcute/client@2.0.0)': 2834 2808 dependencies: 2835 - '@atcute/client': 1.0.0 2809 + '@atcute/client': 2.0.0 2836 2810 2837 2811 '@atcute/cbor@1.0.0': 2838 2812 dependencies: ··· 2845 2819 '@atcute/base32': 1.0.0 2846 2820 '@atcute/varint': 1.0.0 2847 2821 2848 - '@atcute/client@1.0.0': {} 2822 + '@atcute/client@2.0.0': {} 2849 2823 2850 2824 '@atcute/tid@1.0.0': {} 2851 2825 ··· 2856 2830 '@babel/highlight': 7.24.7 2857 2831 picocolors: 1.0.1 2858 2832 2859 - '@babel/compat-data@7.25.2': {} 2860 - 2861 2833 '@babel/compat-data@7.25.4': {} 2862 2834 2863 2835 '@babel/core@7.25.2': 2864 2836 dependencies: 2865 2837 '@ampproject/remapping': 2.3.0 2866 2838 '@babel/code-frame': 7.24.7 2867 - '@babel/generator': 7.25.0 2839 + '@babel/generator': 7.25.6 2868 2840 '@babel/helper-compilation-targets': 7.25.2 2869 2841 '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) 2870 - '@babel/helpers': 7.25.0 2871 - '@babel/parser': 7.25.3 2842 + '@babel/helpers': 7.25.6 2843 + '@babel/parser': 7.25.6 2872 2844 '@babel/template': 7.25.0 2873 - '@babel/traverse': 7.25.3 2874 - '@babel/types': 7.25.2 2845 + '@babel/traverse': 7.25.6 2846 + '@babel/types': 7.25.6 2875 2847 convert-source-map: 2.0.0 2876 2848 debug: 4.3.6 2877 2849 gensync: 1.0.0-beta.2 ··· 2880 2852 transitivePeerDependencies: 2881 2853 - supports-color 2882 2854 2883 - '@babel/generator@7.25.0': 2855 + '@babel/generator@7.25.6': 2884 2856 dependencies: 2885 - '@babel/types': 7.25.2 2886 - '@jridgewell/gen-mapping': 0.3.5 2887 - '@jridgewell/trace-mapping': 0.3.25 2888 - jsesc: 2.5.2 2889 - 2890 - '@babel/generator@7.25.5': 2891 - dependencies: 2892 - '@babel/types': 7.25.4 2857 + '@babel/types': 7.25.6 2893 2858 '@jridgewell/gen-mapping': 0.3.5 2894 2859 '@jridgewell/trace-mapping': 0.3.25 2895 2860 jsesc: 2.5.2 2896 2861 2897 2862 '@babel/helper-annotate-as-pure@7.24.7': 2898 2863 dependencies: 2899 - '@babel/types': 7.25.4 2864 + '@babel/types': 7.25.6 2900 2865 2901 2866 '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': 2902 2867 dependencies: 2903 - '@babel/traverse': 7.25.4 2904 - '@babel/types': 7.25.4 2868 + '@babel/traverse': 7.25.6 2869 + '@babel/types': 7.25.6 2905 2870 transitivePeerDependencies: 2906 2871 - supports-color 2907 2872 2908 2873 '@babel/helper-compilation-targets@7.25.2': 2909 2874 dependencies: 2910 - '@babel/compat-data': 7.25.2 2875 + '@babel/compat-data': 7.25.4 2911 2876 '@babel/helper-validator-option': 7.24.8 2912 2877 browserslist: 4.23.3 2913 2878 lru-cache: 5.1.1 ··· 2921 2886 '@babel/helper-optimise-call-expression': 7.24.7 2922 2887 '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) 2923 2888 '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 2924 - '@babel/traverse': 7.25.4 2889 + '@babel/traverse': 7.25.6 2925 2890 semver: 6.3.1 2926 2891 transitivePeerDependencies: 2927 2892 - supports-color ··· 2946 2911 2947 2912 '@babel/helper-member-expression-to-functions@7.24.8': 2948 2913 dependencies: 2949 - '@babel/traverse': 7.25.4 2950 - '@babel/types': 7.25.4 2914 + '@babel/traverse': 7.25.6 2915 + '@babel/types': 7.25.6 2951 2916 transitivePeerDependencies: 2952 2917 - supports-color 2953 2918 2954 2919 '@babel/helper-module-imports@7.18.6': 2955 2920 dependencies: 2956 - '@babel/types': 7.25.2 2921 + '@babel/types': 7.25.6 2957 2922 2958 2923 '@babel/helper-module-imports@7.24.7': 2959 2924 dependencies: 2960 - '@babel/traverse': 7.25.3 2961 - '@babel/types': 7.25.2 2925 + '@babel/traverse': 7.25.6 2926 + '@babel/types': 7.25.6 2962 2927 transitivePeerDependencies: 2963 2928 - supports-color 2964 2929 ··· 2968 2933 '@babel/helper-module-imports': 7.24.7 2969 2934 '@babel/helper-simple-access': 7.24.7 2970 2935 '@babel/helper-validator-identifier': 7.24.7 2971 - '@babel/traverse': 7.25.3 2936 + '@babel/traverse': 7.25.6 2972 2937 transitivePeerDependencies: 2973 2938 - supports-color 2974 2939 2975 2940 '@babel/helper-optimise-call-expression@7.24.7': 2976 2941 dependencies: 2977 - '@babel/types': 7.25.4 2942 + '@babel/types': 7.25.6 2978 2943 2979 2944 '@babel/helper-plugin-utils@7.24.8': {} 2980 2945 ··· 2983 2948 '@babel/core': 7.25.2 2984 2949 '@babel/helper-annotate-as-pure': 7.24.7 2985 2950 '@babel/helper-wrap-function': 7.25.0 2986 - '@babel/traverse': 7.25.4 2951 + '@babel/traverse': 7.25.6 2987 2952 transitivePeerDependencies: 2988 2953 - supports-color 2989 2954 ··· 2992 2957 '@babel/core': 7.25.2 2993 2958 '@babel/helper-member-expression-to-functions': 7.24.8 2994 2959 '@babel/helper-optimise-call-expression': 7.24.7 2995 - '@babel/traverse': 7.25.4 2960 + '@babel/traverse': 7.25.6 2996 2961 transitivePeerDependencies: 2997 2962 - supports-color 2998 2963 2999 2964 '@babel/helper-simple-access@7.24.7': 3000 2965 dependencies: 3001 - '@babel/traverse': 7.25.3 3002 - '@babel/types': 7.25.2 2966 + '@babel/traverse': 7.25.6 2967 + '@babel/types': 7.25.6 3003 2968 transitivePeerDependencies: 3004 2969 - supports-color 3005 2970 3006 2971 '@babel/helper-skip-transparent-expression-wrappers@7.24.7': 3007 2972 dependencies: 3008 - '@babel/traverse': 7.25.4 3009 - '@babel/types': 7.25.4 2973 + '@babel/traverse': 7.25.6 2974 + '@babel/types': 7.25.6 3010 2975 transitivePeerDependencies: 3011 2976 - supports-color 3012 2977 ··· 3019 2984 '@babel/helper-wrap-function@7.25.0': 3020 2985 dependencies: 3021 2986 '@babel/template': 7.25.0 3022 - '@babel/traverse': 7.25.4 3023 - '@babel/types': 7.25.4 2987 + '@babel/traverse': 7.25.6 2988 + '@babel/types': 7.25.6 3024 2989 transitivePeerDependencies: 3025 2990 - supports-color 3026 2991 3027 - '@babel/helpers@7.25.0': 2992 + '@babel/helpers@7.25.6': 3028 2993 dependencies: 3029 2994 '@babel/template': 7.25.0 3030 - '@babel/types': 7.25.2 2995 + '@babel/types': 7.25.6 3031 2996 3032 2997 '@babel/highlight@7.24.7': 3033 2998 dependencies: ··· 3036 3001 js-tokens: 4.0.0 3037 3002 picocolors: 1.0.1 3038 3003 3039 - '@babel/parser@7.25.3': 3040 - dependencies: 3041 - '@babel/types': 7.25.2 3042 - 3043 - '@babel/parser@7.25.4': 3004 + '@babel/parser@7.25.6': 3044 3005 dependencies: 3045 - '@babel/types': 7.25.4 3006 + '@babel/types': 7.25.6 3046 3007 3047 3008 '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3(@babel/core@7.25.2)': 3048 3009 dependencies: 3049 3010 '@babel/core': 7.25.2 3050 3011 '@babel/helper-plugin-utils': 7.24.8 3051 - '@babel/traverse': 7.25.4 3012 + '@babel/traverse': 7.25.6 3052 3013 transitivePeerDependencies: 3053 3014 - supports-color 3054 3015 ··· 3075 3036 dependencies: 3076 3037 '@babel/core': 7.25.2 3077 3038 '@babel/helper-plugin-utils': 7.24.8 3078 - '@babel/traverse': 7.25.4 3039 + '@babel/traverse': 7.25.6 3079 3040 transitivePeerDependencies: 3080 3041 - supports-color 3081 3042 ··· 3108 3069 '@babel/core': 7.25.2 3109 3070 '@babel/helper-plugin-utils': 7.24.8 3110 3071 3111 - '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.25.2)': 3072 + '@babel/plugin-syntax-import-assertions@7.25.6(@babel/core@7.25.2)': 3112 3073 dependencies: 3113 3074 '@babel/core': 7.25.2 3114 3075 '@babel/helper-plugin-utils': 7.24.8 3115 3076 3116 - '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.25.2)': 3077 + '@babel/plugin-syntax-import-attributes@7.25.6(@babel/core@7.25.2)': 3117 3078 dependencies: 3118 3079 '@babel/core': 7.25.2 3119 3080 '@babel/helper-plugin-utils': 7.24.8 ··· 3190 3151 '@babel/helper-plugin-utils': 7.24.8 3191 3152 '@babel/helper-remap-async-to-generator': 7.25.0(@babel/core@7.25.2) 3192 3153 '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) 3193 - '@babel/traverse': 7.25.4 3154 + '@babel/traverse': 7.25.6 3194 3155 transitivePeerDependencies: 3195 3156 - supports-color 3196 3157 ··· 3237 3198 '@babel/helper-compilation-targets': 7.25.2 3238 3199 '@babel/helper-plugin-utils': 7.24.8 3239 3200 '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) 3240 - '@babel/traverse': 7.25.4 3201 + '@babel/traverse': 7.25.6 3241 3202 globals: 11.12.0 3242 3203 transitivePeerDependencies: 3243 3204 - supports-color ··· 3303 3264 '@babel/core': 7.25.2 3304 3265 '@babel/helper-compilation-targets': 7.25.2 3305 3266 '@babel/helper-plugin-utils': 7.24.8 3306 - '@babel/traverse': 7.25.4 3267 + '@babel/traverse': 7.25.6 3307 3268 transitivePeerDependencies: 3308 3269 - supports-color 3309 3270 ··· 3352 3313 '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) 3353 3314 '@babel/helper-plugin-utils': 7.24.8 3354 3315 '@babel/helper-validator-identifier': 7.24.7 3355 - '@babel/traverse': 7.25.4 3316 + '@babel/traverse': 7.25.6 3356 3317 transitivePeerDependencies: 3357 3318 - supports-color 3358 3319 ··· 3526 3487 '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) 3527 3488 '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) 3528 3489 '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) 3529 - '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.25.2) 3530 - '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.25.2) 3490 + '@babel/plugin-syntax-import-assertions': 7.25.6(@babel/core@7.25.2) 3491 + '@babel/plugin-syntax-import-attributes': 7.25.6(@babel/core@7.25.2) 3531 3492 '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) 3532 3493 '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) 3533 3494 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) ··· 3601 3562 dependencies: 3602 3563 '@babel/core': 7.25.2 3603 3564 '@babel/helper-plugin-utils': 7.24.8 3604 - '@babel/types': 7.25.4 3565 + '@babel/types': 7.25.6 3605 3566 esutils: 2.0.3 3606 3567 3607 3568 '@babel/regjsgen@0.8.0': {} 3608 3569 3609 - '@babel/runtime@7.25.4': 3570 + '@babel/runtime@7.25.6': 3610 3571 dependencies: 3611 3572 regenerator-runtime: 0.14.1 3612 3573 3613 3574 '@babel/template@7.25.0': 3614 3575 dependencies: 3615 3576 '@babel/code-frame': 7.24.7 3616 - '@babel/parser': 7.25.3 3617 - '@babel/types': 7.25.2 3618 - 3619 - '@babel/traverse@7.25.3': 3620 - dependencies: 3621 - '@babel/code-frame': 7.24.7 3622 - '@babel/generator': 7.25.0 3623 - '@babel/parser': 7.25.3 3624 - '@babel/template': 7.25.0 3625 - '@babel/types': 7.25.2 3626 - debug: 4.3.6 3627 - globals: 11.12.0 3628 - transitivePeerDependencies: 3629 - - supports-color 3577 + '@babel/parser': 7.25.6 3578 + '@babel/types': 7.25.6 3630 3579 3631 - '@babel/traverse@7.25.4': 3580 + '@babel/traverse@7.25.6': 3632 3581 dependencies: 3633 3582 '@babel/code-frame': 7.24.7 3634 - '@babel/generator': 7.25.5 3635 - '@babel/parser': 7.25.4 3583 + '@babel/generator': 7.25.6 3584 + '@babel/parser': 7.25.6 3636 3585 '@babel/template': 7.25.0 3637 - '@babel/types': 7.25.4 3586 + '@babel/types': 7.25.6 3638 3587 debug: 4.3.6 3639 3588 globals: 11.12.0 3640 3589 transitivePeerDependencies: 3641 3590 - supports-color 3642 3591 3643 - '@babel/types@7.25.2': 3644 - dependencies: 3645 - '@babel/helper-string-parser': 7.24.8 3646 - '@babel/helper-validator-identifier': 7.24.7 3647 - to-fast-properties: 2.0.0 3648 - 3649 - '@babel/types@7.25.4': 3592 + '@babel/types@7.25.6': 3650 3593 dependencies: 3651 3594 '@babel/helper-string-parser': 7.24.8 3652 3595 '@babel/helper-validator-identifier': 7.24.7 ··· 3656 3599 dependencies: 3657 3600 mime: 3.0.0 3658 3601 3659 - '@cloudflare/workerd-darwin-64@1.20240806.0': 3602 + '@cloudflare/workerd-darwin-64@1.20240821.1': 3660 3603 optional: true 3661 3604 3662 - '@cloudflare/workerd-darwin-arm64@1.20240806.0': 3605 + '@cloudflare/workerd-darwin-arm64@1.20240821.1': 3663 3606 optional: true 3664 3607 3665 - '@cloudflare/workerd-linux-64@1.20240806.0': 3608 + '@cloudflare/workerd-linux-64@1.20240821.1': 3666 3609 optional: true 3667 3610 3668 - '@cloudflare/workerd-linux-arm64@1.20240806.0': 3611 + '@cloudflare/workerd-linux-arm64@1.20240821.1': 3669 3612 optional: true 3670 3613 3671 - '@cloudflare/workerd-windows-64@1.20240806.0': 3614 + '@cloudflare/workerd-windows-64@1.20240821.1': 3672 3615 optional: true 3673 3616 3674 - '@cloudflare/workers-shared@0.1.0': {} 3617 + '@cloudflare/workers-shared@0.4.0': {} 3675 3618 3676 3619 '@cspotcode/source-map-support@0.8.1': 3677 3620 dependencies: ··· 3822 3765 '@esbuild/win32-x64@0.21.5': 3823 3766 optional: true 3824 3767 3825 - '@externdefs/solid-freeze@0.1.1(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe))': 3768 + '@externdefs/solid-freeze@0.1.1(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy))': 3826 3769 dependencies: 3827 - solid-js: 1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe) 3770 + solid-js: 1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy) 3828 3771 3829 - '@externdefs/solid-query@0.1.5(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe))': 3772 + '@externdefs/solid-query@0.1.5(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy))': 3830 3773 dependencies: 3831 3774 '@tanstack/query-core': 5.17.19(patch_hash=mh34qchsf4y2z6x2owv2ljabky) 3832 - solid-js: 1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe) 3775 + solid-js: 1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy) 3833 3776 3834 3777 '@fastify/busboy@2.1.1': {} 3835 3778 ··· 3950 3893 optionalDependencies: 3951 3894 rollup: 2.79.1 3952 3895 3953 - '@rollup/rollup-android-arm-eabi@4.20.0': 3896 + '@rollup/rollup-android-arm-eabi@4.21.1': 3954 3897 optional: true 3955 3898 3956 - '@rollup/rollup-android-arm64@4.20.0': 3899 + '@rollup/rollup-android-arm64@4.21.1': 3957 3900 optional: true 3958 3901 3959 - '@rollup/rollup-darwin-arm64@4.20.0': 3902 + '@rollup/rollup-darwin-arm64@4.21.1': 3960 3903 optional: true 3961 3904 3962 - '@rollup/rollup-darwin-x64@4.20.0': 3905 + '@rollup/rollup-darwin-x64@4.21.1': 3963 3906 optional: true 3964 3907 3965 - '@rollup/rollup-linux-arm-gnueabihf@4.20.0': 3908 + '@rollup/rollup-linux-arm-gnueabihf@4.21.1': 3966 3909 optional: true 3967 3910 3968 - '@rollup/rollup-linux-arm-musleabihf@4.20.0': 3911 + '@rollup/rollup-linux-arm-musleabihf@4.21.1': 3969 3912 optional: true 3970 3913 3971 - '@rollup/rollup-linux-arm64-gnu@4.20.0': 3914 + '@rollup/rollup-linux-arm64-gnu@4.21.1': 3972 3915 optional: true 3973 3916 3974 - '@rollup/rollup-linux-arm64-musl@4.20.0': 3917 + '@rollup/rollup-linux-arm64-musl@4.21.1': 3975 3918 optional: true 3976 3919 3977 - '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': 3920 + '@rollup/rollup-linux-powerpc64le-gnu@4.21.1': 3978 3921 optional: true 3979 3922 3980 - '@rollup/rollup-linux-riscv64-gnu@4.20.0': 3923 + '@rollup/rollup-linux-riscv64-gnu@4.21.1': 3981 3924 optional: true 3982 3925 3983 - '@rollup/rollup-linux-s390x-gnu@4.20.0': 3926 + '@rollup/rollup-linux-s390x-gnu@4.21.1': 3984 3927 optional: true 3985 3928 3986 - '@rollup/rollup-linux-x64-gnu@4.20.0': 3929 + '@rollup/rollup-linux-x64-gnu@4.21.1': 3987 3930 optional: true 3988 3931 3989 - '@rollup/rollup-linux-x64-musl@4.20.0': 3932 + '@rollup/rollup-linux-x64-musl@4.21.1': 3990 3933 optional: true 3991 3934 3992 - '@rollup/rollup-win32-arm64-msvc@4.20.0': 3935 + '@rollup/rollup-win32-arm64-msvc@4.21.1': 3993 3936 optional: true 3994 3937 3995 - '@rollup/rollup-win32-ia32-msvc@4.20.0': 3938 + '@rollup/rollup-win32-ia32-msvc@4.21.1': 3996 3939 optional: true 3997 3940 3998 - '@rollup/rollup-win32-x64-msvc@4.20.0': 3941 + '@rollup/rollup-win32-x64-msvc@4.21.1': 3999 3942 optional: true 4000 3943 4001 3944 '@surma/rollup-plugin-off-main-thread@2.2.3': ··· 4009 3952 4010 3953 '@types/babel__core@7.20.5': 4011 3954 dependencies: 4012 - '@babel/parser': 7.25.3 4013 - '@babel/types': 7.25.2 3955 + '@babel/parser': 7.25.6 3956 + '@babel/types': 7.25.6 4014 3957 '@types/babel__generator': 7.6.8 4015 3958 '@types/babel__template': 7.4.4 4016 3959 '@types/babel__traverse': 7.20.6 4017 3960 4018 3961 '@types/babel__generator@7.6.8': 4019 3962 dependencies: 4020 - '@babel/types': 7.25.2 3963 + '@babel/types': 7.25.6 4021 3964 4022 3965 '@types/babel__template@7.4.4': 4023 3966 dependencies: 4024 - '@babel/parser': 7.25.3 4025 - '@babel/types': 7.25.2 3967 + '@babel/parser': 7.25.6 3968 + '@babel/types': 7.25.6 4026 3969 4027 3970 '@types/babel__traverse@7.20.6': 4028 3971 dependencies: 4029 - '@babel/types': 7.25.2 3972 + '@babel/types': 7.25.6 4030 3973 4031 3974 '@types/dom-close-watcher@1.0.0': {} 4032 3975 ··· 4036 3979 4037 3980 '@types/node-forge@1.3.11': 4038 3981 dependencies: 4039 - '@types/node': 22.3.0 3982 + '@types/node': 22.5.1 4040 3983 4041 - '@types/node@22.3.0': 4042 - dependencies: 4043 - undici-types: 6.18.2 4044 - 4045 - '@types/node@22.5.0': 3984 + '@types/node@22.5.1': 4046 3985 dependencies: 4047 3986 undici-types: 6.19.8 4048 - optional: true 4049 3987 4050 3988 '@types/resolve@1.20.2': {} 4051 3989 ··· 4114 4052 autoprefixer@10.4.20(postcss@8.4.41): 4115 4053 dependencies: 4116 4054 browserslist: 4.23.3 4117 - caniuse-lite: 1.0.30001651 4055 + caniuse-lite: 1.0.30001653 4118 4056 fraction.js: 4.3.7 4119 4057 normalize-range: 0.1.2 4120 4058 picocolors: 1.0.1 ··· 4125 4063 dependencies: 4126 4064 possible-typed-array-names: 1.0.0 4127 4065 4128 - babel-plugin-jsx-dom-expressions@0.38.1(@babel/core@7.25.2): 4066 + babel-plugin-jsx-dom-expressions@0.38.5(@babel/core@7.25.2): 4129 4067 dependencies: 4130 4068 '@babel/core': 7.25.2 4131 4069 '@babel/helper-module-imports': 7.18.6 4132 4070 '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) 4133 - '@babel/types': 7.25.2 4071 + '@babel/types': 7.25.6 4134 4072 html-entities: 2.3.3 4135 4073 validate-html-nesting: 1.2.2 4136 4074 ··· 4162 4100 dependencies: 4163 4101 '@babel/core': 7.25.2 4164 4102 '@babel/helper-plugin-utils': 7.24.8 4165 - '@babel/types': 7.25.2 4103 + '@babel/types': 7.25.6 4166 4104 4167 - babel-preset-solid@1.8.19(@babel/core@7.25.2): 4105 + babel-preset-solid@1.8.22(@babel/core@7.25.2): 4168 4106 dependencies: 4169 4107 '@babel/core': 7.25.2 4170 - babel-plugin-jsx-dom-expressions: 0.38.1(@babel/core@7.25.2) 4108 + babel-plugin-jsx-dom-expressions: 0.38.5(@babel/core@7.25.2) 4171 4109 4172 4110 balanced-match@1.0.2: {} 4173 4111 ··· 4190 4128 4191 4129 browserslist@4.23.3: 4192 4130 dependencies: 4193 - caniuse-lite: 1.0.30001651 4194 - electron-to-chromium: 1.5.5 4131 + caniuse-lite: 1.0.30001653 4132 + electron-to-chromium: 1.5.13 4195 4133 node-releases: 2.0.18 4196 4134 update-browserslist-db: 1.1.0(browserslist@4.23.3) 4197 4135 ··· 4209 4147 4210 4148 camelcase-css@2.0.1: {} 4211 4149 4212 - caniuse-lite@1.0.30001651: {} 4150 + caniuse-lite@1.0.30001653: {} 4213 4151 4214 4152 capnp-ts@0.7.0: 4215 4153 dependencies: 4216 4154 debug: 4.3.6 4217 - tslib: 2.6.3 4155 + tslib: 2.7.0 4218 4156 transitivePeerDependencies: 4219 4157 - supports-color 4220 4158 ··· 4335 4273 dependencies: 4336 4274 jake: 10.9.2 4337 4275 4338 - electron-to-chromium@1.5.5: {} 4276 + electron-to-chromium@1.5.13: {} 4339 4277 4340 4278 emoji-regex@8.0.0: {} 4341 4279 ··· 4487 4425 '@nodelib/fs.walk': 1.2.8 4488 4426 glob-parent: 5.1.2 4489 4427 merge2: 1.4.1 4490 - micromatch: 4.0.7 4428 + micromatch: 4.0.8 4491 4429 4492 4430 fast-json-stable-stringify@2.1.0: {} 4493 4431 ··· 4670 4608 4671 4609 is-callable@1.2.7: {} 4672 4610 4673 - is-core-module@2.15.0: 4611 + is-core-module@2.15.1: 4674 4612 dependencies: 4675 4613 hasown: 2.0.2 4676 4614 ··· 4802 4740 4803 4741 merge2@1.4.1: {} 4804 4742 4805 - micromatch@4.0.7: 4743 + micromatch@4.0.8: 4806 4744 dependencies: 4807 4745 braces: 3.0.3 4808 4746 picomatch: 2.3.1 4809 4747 4810 4748 mime@3.0.0: {} 4811 4749 4812 - miniflare@3.20240806.0: 4750 + miniflare@3.20240821.0: 4813 4751 dependencies: 4814 4752 '@cspotcode/source-map-support': 0.8.1 4815 4753 acorn: 8.12.1 ··· 4819 4757 glob-to-regexp: 0.4.1 4820 4758 stoppable: 1.1.0 4821 4759 undici: 5.28.4 4822 - workerd: 1.20240806.0 4760 + workerd: 1.20240821.1 4823 4761 ws: 8.18.0 4824 4762 youch: 3.3.3 4825 4763 zod: 3.23.8 ··· 4987 4925 4988 4926 regenerator-transform@0.15.2: 4989 4927 dependencies: 4990 - '@babel/runtime': 7.25.4 4928 + '@babel/runtime': 7.25.6 4991 4929 4992 4930 regexp.prototype.flags@1.5.2: 4993 4931 dependencies: ··· 5015 4953 5016 4954 resolve@1.22.8: 5017 4955 dependencies: 5018 - is-core-module: 2.15.0 4956 + is-core-module: 2.15.1 5019 4957 path-parse: 1.0.7 5020 4958 supports-preserve-symlinks-flag: 1.0.0 5021 4959 ··· 5039 4977 optionalDependencies: 5040 4978 fsevents: 2.3.3 5041 4979 5042 - rollup@4.20.0: 4980 + rollup@4.21.1: 5043 4981 dependencies: 5044 4982 '@types/estree': 1.0.5 5045 4983 optionalDependencies: 5046 - '@rollup/rollup-android-arm-eabi': 4.20.0 5047 - '@rollup/rollup-android-arm64': 4.20.0 5048 - '@rollup/rollup-darwin-arm64': 4.20.0 5049 - '@rollup/rollup-darwin-x64': 4.20.0 5050 - '@rollup/rollup-linux-arm-gnueabihf': 4.20.0 5051 - '@rollup/rollup-linux-arm-musleabihf': 4.20.0 5052 - '@rollup/rollup-linux-arm64-gnu': 4.20.0 5053 - '@rollup/rollup-linux-arm64-musl': 4.20.0 5054 - '@rollup/rollup-linux-powerpc64le-gnu': 4.20.0 5055 - '@rollup/rollup-linux-riscv64-gnu': 4.20.0 5056 - '@rollup/rollup-linux-s390x-gnu': 4.20.0 5057 - '@rollup/rollup-linux-x64-gnu': 4.20.0 5058 - '@rollup/rollup-linux-x64-musl': 4.20.0 5059 - '@rollup/rollup-win32-arm64-msvc': 4.20.0 5060 - '@rollup/rollup-win32-ia32-msvc': 4.20.0 5061 - '@rollup/rollup-win32-x64-msvc': 4.20.0 4984 + '@rollup/rollup-android-arm-eabi': 4.21.1 4985 + '@rollup/rollup-android-arm64': 4.21.1 4986 + '@rollup/rollup-darwin-arm64': 4.21.1 4987 + '@rollup/rollup-darwin-x64': 4.21.1 4988 + '@rollup/rollup-linux-arm-gnueabihf': 4.21.1 4989 + '@rollup/rollup-linux-arm-musleabihf': 4.21.1 4990 + '@rollup/rollup-linux-arm64-gnu': 4.21.1 4991 + '@rollup/rollup-linux-arm64-musl': 4.21.1 4992 + '@rollup/rollup-linux-powerpc64le-gnu': 4.21.1 4993 + '@rollup/rollup-linux-riscv64-gnu': 4.21.1 4994 + '@rollup/rollup-linux-s390x-gnu': 4.21.1 4995 + '@rollup/rollup-linux-x64-gnu': 4.21.1 4996 + '@rollup/rollup-linux-x64-musl': 4.21.1 4997 + '@rollup/rollup-win32-arm64-msvc': 4.21.1 4998 + '@rollup/rollup-win32-ia32-msvc': 4.21.1 4999 + '@rollup/rollup-win32-x64-msvc': 4.21.1 5062 5000 fsevents: 2.3.3 5063 5001 5064 5002 run-parallel@1.2.0: ··· 5130 5068 5131 5069 smob@1.5.0: {} 5132 5070 5133 - solid-floating-ui@0.2.1(@floating-ui/dom@1.6.10)(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe)): 5071 + solid-floating-ui@0.2.1(@floating-ui/dom@1.6.10)(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy)): 5134 5072 dependencies: 5135 5073 '@floating-ui/dom': 1.6.10 5136 - solid-js: 1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe) 5074 + solid-js: 1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy) 5137 5075 5138 - solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe): 5076 + solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy): 5139 5077 dependencies: 5140 5078 csstype: 3.1.3 5141 5079 seroval: 1.1.1 5142 5080 seroval-plugins: 1.1.1(seroval@1.1.1) 5143 5081 5144 - solid-refresh@0.6.3(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe)): 5082 + solid-refresh@0.6.3(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy)): 5145 5083 dependencies: 5146 - '@babel/generator': 7.25.0 5084 + '@babel/generator': 7.25.6 5147 5085 '@babel/helper-module-imports': 7.24.7 5148 - '@babel/types': 7.25.2 5149 - solid-js: 1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe) 5086 + '@babel/types': 7.25.6 5087 + solid-js: 1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy) 5150 5088 transitivePeerDependencies: 5151 5089 - supports-color 5152 5090 ··· 5266 5204 is-glob: 4.0.3 5267 5205 jiti: 1.21.6 5268 5206 lilconfig: 2.1.0 5269 - micromatch: 4.0.7 5207 + micromatch: 4.0.8 5270 5208 normalize-path: 3.0.0 5271 5209 object-hash: 3.0.0 5272 5210 picocolors: 1.0.1 ··· 5317 5255 5318 5256 ts-interface-checker@0.1.13: {} 5319 5257 5320 - tslib@2.6.3: {} 5258 + tslib@2.7.0: {} 5321 5259 5322 5260 type-fest@0.16.0: {} 5323 5261 ··· 5364 5302 has-symbols: 1.0.3 5365 5303 which-boxed-primitive: 1.0.2 5366 5304 5367 - undici-types@6.18.2: {} 5368 - 5369 - undici-types@6.19.8: 5370 - optional: true 5305 + undici-types@6.19.8: {} 5371 5306 5372 5307 undici@5.28.4: 5373 5308 dependencies: ··· 5411 5346 5412 5347 validate-html-nesting@1.2.2: {} 5413 5348 5414 - vite-plugin-pwa@0.17.4(patch_hash=ve5hypcrajivuvoyst6zln6qyq)(vite@5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0): 5349 + vite-plugin-pwa@0.17.4(patch_hash=ve5hypcrajivuvoyst6zln6qyq)(vite@5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0): 5415 5350 dependencies: 5416 5351 debug: 4.3.6 5417 5352 fast-glob: 3.3.2 5418 5353 pretty-bytes: 6.1.1 5419 - vite: 5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6) 5354 + vite: 5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6) 5420 5355 workbox-build: 7.1.1(@types/babel__core@7.20.5) 5421 5356 workbox-window: 7.1.0 5422 5357 transitivePeerDependencies: 5423 5358 - supports-color 5424 5359 5425 - vite-plugin-solid@2.10.2(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe))(vite@5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6)): 5360 + vite-plugin-solid@2.10.2(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy))(vite@5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6)): 5426 5361 dependencies: 5427 5362 '@babel/core': 7.25.2 5428 5363 '@types/babel__core': 7.20.5 5429 - babel-preset-solid: 1.8.19(@babel/core@7.25.2) 5364 + babel-preset-solid: 1.8.22(@babel/core@7.25.2) 5430 5365 merge-anything: 5.1.7 5431 - solid-js: 1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe) 5432 - solid-refresh: 0.6.3(solid-js@1.8.20(patch_hash=76exa6qxo2meqzgohn3ssck4qe)) 5433 - vite: 5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6) 5434 - vitefu: 0.2.5(vite@5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6)) 5366 + solid-js: 1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy) 5367 + solid-refresh: 0.6.3(solid-js@1.8.22(patch_hash=5rodyfcb76rtbo26dwlsojy7jy)) 5368 + vite: 5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6) 5369 + vitefu: 0.2.5(vite@5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6)) 5435 5370 transitivePeerDependencies: 5436 5371 - supports-color 5437 5372 5438 - vite@5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6): 5373 + vite@5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6): 5439 5374 dependencies: 5440 5375 esbuild: 0.21.5 5441 5376 postcss: 8.4.41 5442 - rollup: 4.20.0 5377 + rollup: 4.21.1 5443 5378 optionalDependencies: 5444 - '@types/node': 22.5.0 5379 + '@types/node': 22.5.1 5445 5380 fsevents: 2.3.3 5446 5381 terser: 5.31.6 5447 5382 5448 - vitefu@0.2.5(vite@5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6)): 5383 + vitefu@0.2.5(vite@5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6)): 5449 5384 optionalDependencies: 5450 - vite: 5.4.0(patch_hash=ll22cxodgkjat6cp6mjwk5dp4y)(@types/node@22.5.0)(terser@5.31.6) 5385 + vite: 5.4.2(patch_hash=enol6dkeaosc6vsynualw3gkvi)(@types/node@22.5.1)(terser@5.31.6) 5451 5386 5452 5387 webidl-conversions@4.0.2: {} 5453 5388 ··· 5491 5426 '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) 5492 5427 '@babel/core': 7.25.2 5493 5428 '@babel/preset-env': 7.25.4(@babel/core@7.25.2) 5494 - '@babel/runtime': 7.25.4 5429 + '@babel/runtime': 7.25.6 5495 5430 '@rollup/plugin-babel': 5.3.1(@babel/core@7.25.2)(@types/babel__core@7.20.5)(rollup@2.79.1) 5496 5431 '@rollup/plugin-node-resolve': 15.2.3(rollup@2.79.1) 5497 5432 '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) ··· 5590 5525 '@types/trusted-types': 2.0.7 5591 5526 workbox-core: 7.1.0 5592 5527 5593 - workerd@1.20240806.0: 5528 + workerd@1.20240821.1: 5594 5529 optionalDependencies: 5595 - '@cloudflare/workerd-darwin-64': 1.20240806.0 5596 - '@cloudflare/workerd-darwin-arm64': 1.20240806.0 5597 - '@cloudflare/workerd-linux-64': 1.20240806.0 5598 - '@cloudflare/workerd-linux-arm64': 1.20240806.0 5599 - '@cloudflare/workerd-windows-64': 1.20240806.0 5530 + '@cloudflare/workerd-darwin-64': 1.20240821.1 5531 + '@cloudflare/workerd-darwin-arm64': 1.20240821.1 5532 + '@cloudflare/workerd-linux-64': 1.20240821.1 5533 + '@cloudflare/workerd-linux-arm64': 1.20240821.1 5534 + '@cloudflare/workerd-windows-64': 1.20240821.1 5600 5535 5601 - wrangler@3.71.0: 5536 + wrangler@3.72.3: 5602 5537 dependencies: 5603 5538 '@cloudflare/kv-asset-handler': 0.3.4 5604 - '@cloudflare/workers-shared': 0.1.0 5539 + '@cloudflare/workers-shared': 0.4.0 5605 5540 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) 5606 5541 '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) 5607 5542 blake3-wasm: 2.1.5 5608 5543 chokidar: 3.6.0 5609 5544 date-fns: 3.6.0 5610 5545 esbuild: 0.17.19 5611 - miniflare: 3.20240806.0 5546 + miniflare: 3.20240821.0 5612 5547 nanoid: 3.3.7 5613 5548 path-to-regexp: 6.2.2 5614 5549 resolve: 1.22.8 ··· 5616 5551 selfsigned: 2.4.1 5617 5552 source-map: 0.6.1 5618 5553 unenv: unenv-nightly@1.10.0-1717606461.a117952 5619 - workerd: 1.20240806.0 5554 + workerd: 1.20240821.1 5620 5555 xxhash-wasm: 1.0.2 5621 5556 optionalDependencies: 5622 5557 fsevents: 2.3.3
+12
public/oauth/client-metadata.json
··· 1 + { 2 + "client_id": "https://aglais.pages.dev/oauth/client-metadata.json", 3 + "client_uri": "https://aglais.pages.dev", 4 + "client_name": "Aglais", 5 + "application_type": "web", 6 + "scope": "atproto transition:generic transition:chat.bsky", 7 + "grant_types": ["authorization_code", "refresh_token"], 8 + "redirect_uris": ["https://aglais.pages.dev/oauth/callback"], 9 + "response_types": ["code"], 10 + "token_endpoint_auth_method": "none", 11 + "dpop_bound_access_tokens": true 12 + }
+4 -1
src/api/queries/labeler.ts
··· 26 26 const service = data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed; 27 27 28 28 if (!service) { 29 - throw new XRPCError(400, { kind: 'NotFound', message: `Labeler not found: ${$did}` }); 29 + throw new XRPCError(400, { 30 + kind: 'NotFound', 31 + description: `Labeler not found: ${$did}`, 32 + }); 30 33 } 31 34 32 35 return interpretLabelerDefinition(service);
+4 -1
src/api/queries/notification-count.tsx
··· 1 1 import { createQuery } from '@mary/solid-query'; 2 2 3 3 import { useAgent } from '~/lib/states/agent'; 4 + import { useSession } from '~/lib/states/session'; 4 5 5 - export const createNotificationCountQuery = () => { 6 + export const createNotificationCountQuery = (options?: { readonly disabled?: boolean }) => { 7 + const { currentAccount } = useSession(); 6 8 const { rpc } = useAgent(); 7 9 8 10 const query = createQuery(() => ({ 9 11 queryKey: ['notification', 'count'], 12 + enabled: currentAccount !== undefined && !options?.disabled, 10 13 async queryFn() { 11 14 const { data } = await rpc.get('app.bsky.notification.getUnreadCount', { 12 15 params: {},
+4 -1
src/api/queries/post-thread.ts
··· 33 33 const thread = data.thread; 34 34 35 35 if (thread.$type === 'app.bsky.feed.defs#notFoundPost') { 36 - throw new XRPCError(400, { kind: 'NotFound', message: `Post not found: ${$uri}` }); 36 + throw new XRPCError(400, { 37 + kind: 'NotFound', 38 + description: `Post not found: ${$uri}`, 39 + }); 37 40 } 38 41 39 42 return thread;
-103
src/api/utils/did-doc.ts
··· 1 - import { XRPC } from '@atcute/client'; 2 - import type { At } from '@atcute/client/lexicons'; 3 - import { getPdsEndpoint, type DidDocument } from '@atcute/client/utils/did'; 4 - 5 - import { DEFAULT_APP_VIEW } from '../defaults'; 6 - import type { DataServer } from '../types'; 7 - import { isDid } from './strings'; 8 - 9 - const HOST_RE = /^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]+))$/; 10 - 11 - export type ResolutionErrorKind = 12 - | 'DID_UNSUPPORTED' 13 - | 'PLC_NOT_FOUND' 14 - | 'PLC_UNREACHABLE' 15 - | 'WEB_INVALID' 16 - | 'WEB_NOT_FOUND' 17 - | 'WEB_UNREACHABLE'; 18 - 19 - export class DidResolutionError extends Error { 20 - message!: ResolutionErrorKind; 21 - 22 - constructor(kind: ResolutionErrorKind) { 23 - super(kind); 24 - } 25 - } 26 - 27 - export const findDidDocument = async (identifier: string): Promise<DidDocument> => { 28 - let did: At.DID; 29 - 30 - if (isDid(identifier)) { 31 - did = identifier; 32 - } else { 33 - const rpc = new XRPC({ service: DEFAULT_APP_VIEW }); 34 - const response = await rpc.get('com.atproto.identity.resolveHandle', { 35 - params: { 36 - handle: identifier, 37 - }, 38 - }); 39 - 40 - did = response.data.did; 41 - } 42 - 43 - const colon_index = did.indexOf(':', 4); 44 - 45 - const type = did.slice(4, colon_index); 46 - const ident = did.slice(colon_index + 1); 47 - 48 - // 2. retrieve their DID documents 49 - let doc: DidDocument; 50 - 51 - if (type === 'plc') { 52 - const response = await fetch(`https://plc.directory/${did}`); 53 - 54 - if (response.status === 404) { 55 - throw new DidResolutionError('PLC_NOT_FOUND'); 56 - } else if (!response.ok) { 57 - throw new DidResolutionError('PLC_UNREACHABLE'); 58 - } 59 - 60 - const json = await response.json(); 61 - 62 - doc = json as DidDocument; 63 - } else if (type === 'web') { 64 - if (!HOST_RE.test(ident)) { 65 - throw new DidResolutionError('WEB_INVALID'); 66 - } 67 - 68 - const response = await fetch(`https://${ident}/.well-known/did.json`); 69 - 70 - if (response.status === 404) { 71 - throw new DidResolutionError('WEB_NOT_FOUND'); 72 - } else if (!response.ok) { 73 - throw new DidResolutionError('WEB_UNREACHABLE'); 74 - } 75 - 76 - const json = await response.json(); 77 - 78 - doc = json as DidDocument; 79 - } else { 80 - throw new DidResolutionError('DID_UNSUPPORTED'); 81 - } 82 - 83 - return doc; 84 - }; 85 - 86 - export const getDataServer = (doc: DidDocument): DataServer | null => { 87 - const pds = getPdsEndpoint(doc); 88 - 89 - if (pds) { 90 - // Check if this is bsky.social, and give it a nice name. 91 - const url = new URL(pds); 92 - const host = url.host; 93 - 94 - const isBskySocial = host === 'bsky.social' || host.endsWith('.host.bsky.network'); 95 - 96 - return { 97 - name: isBskySocial ? `Bluesky Social` : host, 98 - uri: pds, 99 - }; 100 - } 101 - 102 - return null; 103 - };
+1 -1
src/components/composer/lib/api.ts
··· 49 49 50 50 export const publish = async ({ agent, queryClient, state, onLog: log }: PublishOptions) => { 51 51 const rpc = agent.rpc; 52 - const did = agent.auth!.session!.did; 52 + const did = agent.did!; 53 53 54 54 const now = new Date(); 55 55 const writes: Brand.Union<ComAtprotoRepoApplyWrites.Create>[] = [];
+2 -2
src/components/main/main-sidebar-authenticated.tsx
··· 92 92 {(account) => { 93 93 const profile = createProfileQuery(() => account.did); 94 94 const handleClick = () => { 95 - resumeSession(account); 95 + resumeSession(account.did); 96 96 close(); 97 97 }; 98 98 ··· 107 107 /> 108 108 ); 109 109 }} 110 - title={`@${profile.data?.handle ?? account.session.handle}`} 110 + title={`@${profile.data?.handle}`} 111 111 size="sm" 112 112 onClick={handleClick} 113 113 />
+3 -3
src/components/main/manage-account-dialog.tsx
··· 18 18 const { currentAccount, getAccounts, resumeSession } = useSession(); 19 19 20 20 const switchAccount = async (account: AccountData) => { 21 - resumeSession(account); 21 + resumeSession(account.did); 22 22 closeAllModals(); 23 23 }; 24 24 ··· 78 78 {profile.data?.displayName} 79 79 </p> 80 80 <p class="overflow-hidden text-ellipsis whitespace-nowrap text-de text-contrast-muted"> 81 - {'@' + (profile.data?.handle ?? currentAccount!.data.session.handle)} 81 + {'@' + profile.data?.handle} 82 82 </p> 83 83 </div> 84 84 ··· 104 104 {profile.data?.displayName} 105 105 </p> 106 106 <p class="overflow-hidden text-ellipsis whitespace-nowrap text-de text-contrast-muted"> 107 - {'@' + (profile.data?.handle ?? account.session.handle)} 107 + {'@' + profile.data?.handle} 108 108 </p> 109 109 </div> 110 110 </button>
+103 -376
src/components/main/sign-in-dialog.tsx
··· 1 - import { Match, Switch, batch, createSignal } from 'solid-js'; 1 + import { createSignal, Match, Switch } from 'solid-js'; 2 2 3 - import { XRPCError } from '@atcute/client'; 4 3 import { createMutation } from '@mary/solid-query'; 5 4 6 - import { DEFAULT_DATA_SERVER } from '~/api/defaults'; 7 - import type { DataServer } from '~/api/types'; 8 - import { DidResolutionError, findDidDocument, getDataServer } from '~/api/utils/did-doc'; 9 - import { isDid } from '~/api/utils/strings'; 5 + import { autofocusOnMutation } from '~/lib/input-refs'; 10 6 11 - import { closeAllModals } from '~/globals/modals'; 12 - 13 - import { autofocusNode, autofocusOnMutation, modelText } from '~/lib/input-refs'; 14 - import { useSession } from '~/lib/states/session'; 7 + import { OAuthServerAgent } from '~/lib/bsky-oauth/agents/server-agent'; 8 + import { createES256Key } from '~/lib/bsky-oauth/dpop'; 9 + import { CLIENT_ID, REDIRECT_URI, SCOPE } from '~/lib/bsky-oauth/env'; 10 + import { database } from '~/lib/bsky-oauth/globals'; 11 + import { resolveFromIdentity } from '~/lib/bsky-oauth/resolver'; 12 + import { generatePKCE, generateState } from '~/lib/bsky-oauth/utils'; 15 13 16 14 import Button from '../button'; 17 15 import * as Dialog from '../dialog'; ··· 19 17 import InlineLink from '../inline-link'; 20 18 import TextInput from '../text-input'; 21 19 22 - type View = 23 - | { type: 'handle_initial' } 24 - | { type: 'handle_password' } 25 - | { type: 'email_initial' } 26 - | { type: 'otp'; from: 'handle' | 'email' }; 20 + const enum View { 21 + HANDLE, 22 + PDS, 23 + } 27 24 28 - type TargetedError = { target: 'identifier' | 'password' | 'otp'; msg: string }; 25 + class LoginError extends Error {} 29 26 30 27 const SignInDialog = () => { 31 - const session = useSession(); 28 + const [view, setView] = createSignal(View.HANDLE); 32 29 33 - const [view, setView] = createSignal<View>({ type: 'handle_initial' }); 30 + const [pending, setPending] = createSignal<string>(); 31 + const [error, setError] = createSignal<string>(); 34 32 35 - const [service, setService] = createSignal(DEFAULT_DATA_SERVER); 36 - const [identifier, setIdentifier] = createSignal(''); 37 - const [password, setPassword] = createSignal(''); 38 - const [otp, setOtp] = createSignal(''); 33 + const loginMutation = createMutation(() => ({ 34 + async mutationFn({ identifier }: { identifier: string }) { 35 + setPending(`Resolving your identity`); 39 36 40 - const [error, setError] = createSignal<TargetedError>(); 41 - 42 - const pdsMutation = createMutation(() => { 43 - return { 44 - async mutationFn({ identifier }: { identifier: string }) { 45 - const didDoc = await findDidDocument(identifier); 46 - const service = getDataServer(didDoc); 47 - if (!service) { 48 - throw new Error(`PDS_NOT_FOUND`); 49 - } 37 + const { identity, metadata } = await resolveFromIdentity(identifier); 50 38 51 - return { didDoc, service }; 52 - }, 53 - onSuccess({ service }) { 54 - setTimeout(() => { 55 - batch(() => { 56 - setView({ type: 'handle_password' }); 57 - setService(service); 58 - }); 59 - }, 0); 60 - }, 61 - onError(error, { identifier }) { 62 - let msg = `Unknown error, try again later`; 39 + if (!metadata.pushed_authorization_request_endpoint) { 40 + throw new LoginError(`no PAR endpoint is specified`); 41 + } 63 42 64 - if (error instanceof DidResolutionError) { 65 - const type = error.message; 43 + setPending(`Contacting your data server`); 66 44 67 - if (type === 'DID_UNSUPPORTED') { 68 - if (isDid(identifier)) { 69 - msg = `Unsupported DID method`; 70 - } else { 71 - msg = `Account uses an unsupported DID method`; 72 - } 73 - } else if (type === 'PLC_NOT_FOUND') { 74 - msg = `DID not found in PLC directory`; 75 - } else if (type === 'PLC_UNREACHABLE') { 76 - msg = `Can't reach PLC directory right now, try again later`; 77 - } else if (type === 'WEB_INVALID') { 78 - msg = `Specified did:web is invalid`; 79 - } else if (type === 'WEB_NOT_FOUND') { 80 - msg = `Can't find your account, did you type it correctly?`; 81 - } else if (type === 'WEB_UNREACHABLE') { 82 - msg = `Can't reach your DID document right now, try again later`; 83 - } 84 - } else if (error instanceof XRPCError) { 85 - const err = error.kind; 45 + const state = generateState(); 86 46 87 - if (error.message === 'Unable to resolve handle') { 88 - msg = `Can't find your account, did you type it correctly?`; 89 - } else if (err === 'InvalidRequest') { 90 - msg = `That doesn't seem right, did you type it correctly?`; 91 - } 92 - } else if (error instanceof Error) { 93 - if (error.message === 'PDS_NOT_FOUND') { 94 - msg = `Account is not attached to a hosting provider`; 95 - } 96 - } 47 + const pkce = await generatePKCE(); 48 + const dpopKey = await createES256Key(); 97 49 98 - setError({ target: 'identifier', msg }); 99 - }, 100 - }; 101 - }); 50 + const params = { 51 + redirect_uri: REDIRECT_URI, 52 + code_challenge: pkce.challenge, 53 + code_challenge_method: pkce.method, 54 + state: state, 55 + login_hint: identity ? identifier : undefined, 56 + response_mode: 'fragment', 57 + response_type: 'code', 58 + display: 'page', 59 + // id_token_hint: undefined, 60 + // max_age: undefined, 61 + // prompt: undefined, 62 + scope: SCOPE, 63 + // ui_locales: undefined, 64 + } satisfies Record<string, string | undefined>; 102 65 103 - const loginMutation = createMutation(() => ({ 104 - async mutationFn({ 105 - service, 106 - identifier, 107 - password, 108 - authFactorToken, 109 - }: { 110 - from: 'handle' | 'email'; 111 - service: DataServer; 112 - identifier: string; 113 - password: string; 114 - authFactorToken: string | undefined; 115 - }) { 116 - await session.login({ 117 - service: service.uri, 118 - identifier: identifier, 119 - password: password, 120 - authFactorToken: authFactorToken, 66 + await database.states.set(state, { 67 + dpopKey: dpopKey, 68 + issuer: metadata.issuer, 69 + verifier: pkce.verifier, 121 70 }); 122 - }, 123 - onSuccess() { 124 - closeAllModals(); 125 - }, 126 - onError(error: unknown, { from }) { 127 - let msg = `Unknown error, try again later`; 128 71 129 - if (error instanceof XRPCError) { 130 - const err = error.kind; 72 + const server = new OAuthServerAgent(metadata, dpopKey); 73 + const response = await server.request('pushed_authorization_request', params); 131 74 132 - if (err === 'AuthFactorTokenRequired') { 133 - setView({ type: 'otp', from: from }); 134 - return; 135 - } else if (err === 'AuthenticationRequired') { 136 - msg = `Invalid password`; 137 - } else if (err === 'AccountTakedown') { 138 - msg = `Your account has been taken down`; 139 - } 140 - } else if (error instanceof DOMException) { 141 - if (error.name === 'AbortError') { 142 - msg = `Login attempt aborted, try again`; 143 - } 144 - } 75 + const authUrl = new URL(metadata.authorization_endpoint); 76 + authUrl.searchParams.set('client_id', CLIENT_ID); 77 + authUrl.searchParams.set('request_uri', response.request_uri); 78 + 79 + setPending(`Redirecting to your data server`); 80 + 81 + // Wait 250ms just in case. 82 + await new Promise((resolve) => setTimeout(resolve, 250)); 145 83 146 - setError({ target: 'password', msg }); 84 + window.location.assign(authUrl); 85 + 86 + await new Promise((_resolve, reject) => { 87 + window.addEventListener( 88 + 'pageshow', 89 + () => { 90 + reject(new LoginError(`User aborted the login request`)); 91 + }, 92 + { once: true }, 93 + ); 94 + }); 95 + }, 96 + async onError(err) { 97 + console.error(err); 147 98 }, 148 99 })); 149 100 ··· 153 104 <Dialog.Container maxWidth="sm" centered disabled={loginMutation.isPending}> 154 105 <form 155 106 class="contents" 156 - onsubmit={(ev) => { 107 + onSubmit={(ev) => { 157 108 const $view = view(); 109 + const formData = new FormData(ev.currentTarget); 158 110 159 111 ev.preventDefault(); 160 112 161 - batch(() => { 162 - setError(undefined); 163 - 164 - if ($view.type === 'handle_initial') { 165 - pdsMutation.mutate({ identifier: identifier() }); 166 - } else if ($view.type === 'handle_password') { 167 - loginMutation.mutate({ 168 - from: 'handle', 169 - service: service(), 170 - identifier: identifier(), 171 - password: password(), 172 - authFactorToken: undefined, 173 - }); 174 - } else if ($view.type === 'email_initial') { 175 - loginMutation.mutate({ 176 - from: 'email', 177 - service: service(), 178 - identifier: identifier(), 179 - password: password(), 180 - authFactorToken: undefined, 181 - }); 182 - } else if ($view.type === 'otp') { 183 - loginMutation.mutate({ 184 - from: $view.from, 185 - service: service(), 186 - identifier: identifier(), 187 - password: password(), 188 - authFactorToken: formatEmailOtpCode(otp()), 189 - }); 190 - } 191 - }); 113 + if ($view === View.HANDLE) { 114 + loginMutation.mutate({ identifier: formData.get('identifier') as string }); 115 + } else { 116 + } 192 117 }} 193 118 > 194 119 <Dialog.Header> ··· 197 122 </Dialog.HeaderAccessory> 198 123 </Dialog.Header> 199 124 200 - <Fieldset disabled={pdsMutation.isPending}> 125 + <Fieldset disabled={loginMutation.isPending}> 201 126 <Dialog.Body class="flex flex-col gap-6"> 202 - <Switch> 203 - <Match when={view().type === 'handle_initial'}> 204 - <div class="flex flex-col gap-1"> 205 - <h2 class="text-2xl font-bold">Sign in</h2> 206 - <h3 class="text-base text-contrast-muted">To begin, enter your Bluesky handle or DID</h3> 207 - </div> 127 + <div class="flex flex-col gap-1"> 128 + <h2 class="text-2xl font-bold">Sign in</h2> 129 + <h3 class="text-base text-contrast-muted">To begin, enter your Bluesky handle or DID</h3> 130 + </div> 208 131 209 - <div class="flex flex-col gap-4"> 210 - <TextInput 211 - ref={(node) => { 212 - autofocusOnMutation(node, pdsMutation); 213 - modelText(node, identifier, setIdentifier); 214 - }} 215 - autocomplete="username" 216 - pattern="([a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]+))|did:[a-z]+:[a-zA-Z0-9._\-]+" 217 - required 218 - label="Bluesky handle or DID" 219 - placeholder="paul.bsky.social" 220 - error={(() => { 221 - const $error = error(); 222 - if ($error && $error.target === 'identifier') { 223 - return $error.msg; 224 - } 225 - })()} 226 - /> 227 - 228 - <input 229 - ref={(node) => { 230 - modelText(node, password, setPassword); 231 - }} 232 - type="password" 233 - autocomplete="current-password" 234 - hidden 235 - /> 236 - 237 - <div class="flex flex-col gap-2"> 238 - <InlineLink 239 - onClick={() => { 240 - batch(() => { 241 - setView({ type: 'email_initial' }); 242 - 243 - setError(); 244 - 245 - setService(DEFAULT_DATA_SERVER); 246 - setIdentifier(''); 247 - setPassword(''); 248 - }); 249 - }} 250 - > 251 - Sign in with email address instead 252 - </InlineLink> 253 - </div> 254 - </div> 255 - </Match> 256 - 257 - <Match when={view().type === 'handle_password'}> 258 - <div class="flex flex-col gap-1"> 259 - <h2 class="text-2xl font-bold">Enter your password</h2> 260 - </div> 261 - 262 - <div class="flex flex-col gap-4"> 263 - <TextInput 264 - disabled 265 - autocomplete="username" 266 - label="Bluesky handle or DID" 267 - value={identifier()} 268 - /> 132 + <div class="flex flex-col gap-4"> 133 + <TextInput 134 + ref={(node) => { 135 + autofocusOnMutation(node, loginMutation); 136 + }} 137 + name="identifier" 138 + autocomplete="username" 139 + pattern="([a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]+))|did:[a-z]+:[a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-]" 140 + required 141 + label="Bluesky handle or DID" 142 + placeholder="paul.bsky.social" 143 + /> 269 144 270 - <TextInput 271 - ref={(node) => { 272 - autofocusOnMutation(node, loginMutation); 273 - modelText(node, password, setPassword); 274 - }} 275 - type="password" 276 - autocomplete="current-password" 277 - required 278 - label="Password" 279 - error={(() => { 280 - const $error = error(); 281 - if ($error && $error.target === 'password') { 282 - return $error.msg; 283 - } 284 - })()} 285 - /> 145 + <Switch> 146 + <Match when={loginMutation.isPending}> 147 + <p class="text-sm text-contrast-muted/80 empty:hidden">{pending()}</p> 148 + </Match> 286 149 150 + <Match when> 287 151 <div class="flex flex-col gap-2"> 288 - <InlineLink 289 - onClick={() => { 290 - batch(() => { 291 - setView({ type: 'handle_initial' }); 292 - 293 - setError(); 294 - setPassword(''); 295 - }); 296 - }} 297 - > 298 - Sign in with another account 299 - </InlineLink> 152 + <InlineLink>Sign in with your personal data server instead</InlineLink> 300 153 </div> 301 - </div> 302 - </Match> 303 - 304 - <Match when={view().type === 'email_initial'}> 305 - <div class="flex flex-col gap-1"> 306 - <h2 class="text-2xl font-bold">Sign in</h2> 307 - </div> 308 - 309 - <div class="flex flex-col gap-4"> 310 - <TextInput 311 - ref={(node) => { 312 - autofocusNode(node); 313 - modelText(node, identifier, setIdentifier); 314 - }} 315 - type="email" 316 - required 317 - label="Email address" 318 - placeholder="emma@contoso.com" 319 - error={(() => { 320 - const $error = error(); 321 - if ($error && $error.target === 'identifier') { 322 - return $error.msg; 323 - } 324 - })()} 325 - /> 326 - 327 - <TextInput 328 - ref={(node) => { 329 - autofocusOnMutation(node, loginMutation, false); 330 - modelText(node, password, setPassword); 331 - }} 332 - type="password" 333 - autocomplete="current-password" 334 - required 335 - label="Password" 336 - error={(() => { 337 - const $error = error(); 338 - if ($error && $error.target === 'password') { 339 - return $error.msg; 340 - } 341 - })()} 342 - /> 343 - 344 - <div class="flex flex-col gap-2"> 345 - <InlineLink 346 - onClick={() => { 347 - batch(() => { 348 - setView({ type: 'handle_initial' }); 349 - 350 - setError(); 351 - setIdentifier(''); 352 - setPassword(''); 353 - }); 354 - }} 355 - > 356 - Sign in with Bluesky handle instead 357 - </InlineLink> 358 - </div> 359 - </div> 360 - </Match> 361 - 362 - <Match 363 - when={(() => { 364 - const $view = view(); 365 - if ($view.type === 'otp') { 366 - return $view; 367 - } 368 - })()} 369 - keyed 370 - > 371 - {({ from }) => ( 372 - <> 373 - <div class="flex flex-col gap-1"> 374 - <h2 class="text-2xl font-bold">Enter verification code</h2> 375 - <h3 class="max-w-84 text-base text-contrast-muted"> 376 - Check your inbox for an email containing the code and enter it here 377 - </h3> 378 - </div> 379 - 380 - <div class="flex flex-col gap-4"> 381 - <TextInput 382 - ref={(node) => { 383 - autofocusOnMutation(node, loginMutation); 384 - modelText(node, otp, setOtp); 385 - }} 386 - autocomplete="one-time-code" 387 - required 388 - label="Verification code" 389 - placeholder="AAAAA-BBBBB" 390 - error={(() => { 391 - const $error = error(); 392 - if ($error && $error.target === 'otp') { 393 - return $error.msg; 394 - } 395 - })()} 396 - /> 397 - 398 - <InlineLink 399 - onClick={() => { 400 - batch(() => { 401 - setView({ type: `${from}_initial` }); 402 - 403 - setError(); 404 - setOtp(''); 405 - }); 406 - }} 407 - > 408 - Sign in with another account 409 - </InlineLink> 410 - </div> 411 - </> 412 - )} 413 - </Match> 414 - </Switch> 154 + </Match> 155 + </Switch> 156 + </div> 415 157 </Dialog.Body> 416 158 417 159 <Dialog.Actions> 418 160 <Button type="submit" variant="primary" size="lg"> 419 - {(() => { 420 - const $view = view(); 421 - if ($view.type === 'handle_initial' || $view.type === 'otp') { 422 - return `Continue`; 423 - } 424 - 425 - return `Sign in`; 426 - })()} 161 + Continue 427 162 </Button> 428 163 </Dialog.Actions> 429 164 </Fieldset> ··· 434 169 }; 435 170 436 171 export default SignInDialog; 437 - 438 - const formatEmailOtpCode = (code: string): string | undefined => { 439 - if (code.length === 0) { 440 - return undefined; 441 - } 442 - 443 - return (code.includes('-') ? code : code.slice(0, 5) + '-' + code.slice(5)).toUpperCase(); 444 - };
+2
src/components/text-input.tsx
··· 8 8 ref?: (node: HTMLInputElement) => void; 9 9 label?: string; 10 10 type?: 'text' | 'email' | 'password' | 'search' | 'tel' | 'url'; 11 + name?: string; 11 12 autocomplete?: 12 13 | 'off' 13 14 | 'on' ··· 53 54 <input 54 55 ref={props.ref} 55 56 id={id} 57 + name={props.name} 56 58 type={props.type || 'text'} 57 59 autocomplete={props.autocomplete} 58 60 pattern={props.pattern}
+27
src/lib/atproto/labeler.ts
··· 1 + import { buildFetchHandler, type FetchHandler } from '@atcute/client'; 2 + import type { At } from '@atcute/client/lexicons'; 3 + 4 + import { mergeHeaders } from '@atcute/client/utils/http'; 5 + 6 + export interface Labeler { 7 + did: At.DID; 8 + redact: boolean; 9 + } 10 + 11 + export const attachLabelerHeaders = ( 12 + handler: FetchHandler | FetchHandler, 13 + labelers: () => Labeler[], 14 + ): FetchHandler => { 15 + const next = buildFetchHandler(handler); 16 + 17 + return (pathname, init) => { 18 + return next(pathname, { 19 + ...init, 20 + headers: mergeHeaders(init.headers, { 21 + 'atproto-accept-labelers': labelers() 22 + .map((labeler) => labeler.did + (labeler.redact ? `;redact` : ``)) 23 + .join(', '), 24 + }), 25 + }); 26 + }; 27 + };
+139
src/lib/bsky-oauth/agents/server-agent.ts
··· 1 + import type { At } from '@atcute/client/lexicons'; 2 + 3 + import { createDPoPFetch } from '../dpop'; 4 + import { CLIENT_ID, REDIRECT_URI } from '../env'; 5 + import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors'; 6 + import { resolveFromIdentity } from '../resolver'; 7 + import { extractContentType } from '../utils'; 8 + 9 + import type { DPoPKey } from '../types/dpop'; 10 + import type { OAuthParResponse } from '../types/par'; 11 + import type { AuthorizationServerMetadata } from '../types/server'; 12 + import type { OAuthTokenResponse, TokenSet } from '../types/token'; 13 + 14 + export class OAuthServerAgent { 15 + #fetch: typeof fetch; 16 + #metadata: AuthorizationServerMetadata; 17 + 18 + constructor(metadata: AuthorizationServerMetadata, dpopKey: DPoPKey) { 19 + this.#metadata = metadata; 20 + this.#fetch = createDPoPFetch(CLIENT_ID, dpopKey, true); 21 + } 22 + 23 + async request( 24 + endpoint: 'pushed_authorization_request', 25 + payload: Record<string, unknown>, 26 + ): Promise<OAuthParResponse>; 27 + async request(endpoint: 'token', payload: Record<string, unknown>): Promise<OAuthTokenResponse>; 28 + async request(endpoint: 'revocation', payload: Record<string, unknown>): Promise<any>; 29 + async request(endpoint: 'introspection', payload: Record<string, unknown>): Promise<any>; 30 + async request(endpoint: string, payload: Record<string, unknown>): Promise<any> { 31 + const url: string | undefined = (this.#metadata as any)[`${endpoint}_endpoint`]; 32 + if (!url) { 33 + throw new Error(`no endpoint for ${endpoint}`); 34 + } 35 + 36 + const response = await this.#fetch(url, { 37 + method: 'post', 38 + headers: { 39 + 'content-type': 'application/json', 40 + }, 41 + body: JSON.stringify({ 42 + ...payload, 43 + client_id: CLIENT_ID, 44 + }), 45 + }); 46 + 47 + if (extractContentType(response.headers) !== 'application/json') { 48 + throw new FetchResponseError(response, 2, `unexpected content-type`); 49 + } 50 + 51 + const json = await response.json(); 52 + 53 + if (response.ok) { 54 + return json; 55 + } else { 56 + throw new OAuthResponseError(response, json); 57 + } 58 + } 59 + 60 + async revoke(token: string): Promise<void> { 61 + try { 62 + await this.request('revocation', { token: token }); 63 + } catch {} 64 + } 65 + 66 + async exchangeCode(code: string, verifier?: string) { 67 + const response = await this.request('token', { 68 + grant_type: 'authorization_code', 69 + redirect_uri: REDIRECT_URI, 70 + code: code, 71 + code_verifier: verifier, 72 + }); 73 + 74 + try { 75 + return await this.#processTokenResponse(response); 76 + } catch (err) { 77 + await this.revoke(response.access_token); 78 + throw err; 79 + } 80 + } 81 + 82 + async refresh(tokenSet: TokenSet): Promise<TokenSet> { 83 + const sub = tokenSet.sub; 84 + 85 + if (!tokenSet.refresh_token) { 86 + throw new TokenRefreshError(sub, 'No refresh token available'); 87 + } 88 + 89 + const response = await this.request('token', { 90 + grant_type: 'refresh_token', 91 + refresh_token: tokenSet.refresh_token, 92 + }); 93 + 94 + try { 95 + if (sub !== response.sub) { 96 + throw new TokenRefreshError(sub, `sub mismatch in token response; got ${response.sub}`); 97 + } 98 + if (tokenSet.iss !== this.#metadata.issuer) { 99 + throw new TokenRefreshError(sub, `issuer mismatch; got ${this.#metadata.issuer}`); 100 + } 101 + 102 + return this.#processTokenResponse(response); 103 + } catch (err) { 104 + await this.revoke(response.access_token); 105 + 106 + throw err; 107 + } 108 + } 109 + 110 + async #processTokenResponse(response: OAuthTokenResponse): Promise<TokenSet> { 111 + const sub = response.sub; 112 + if (!sub) { 113 + throw new TypeError(`missing sub field in token response`); 114 + } 115 + 116 + const resolved = await resolveFromIdentity(sub, { signal: AbortSignal.timeout(10e3) }); 117 + 118 + if (resolved.metadata.issuer !== this.#metadata.issuer) { 119 + // Best case scenario; the user switched PDS. Worst case scenario; a bad 120 + // actor is trying to impersonate a user. In any case, we must not allow 121 + // this token to be used. 122 + throw new TypeError(`issuer mismatch; got ${resolved.metadata.issuer}`); 123 + } 124 + 125 + return { 126 + sub: sub as At.DID, 127 + aud: resolved.identity.pds.href, 128 + iss: resolved.metadata.issuer, 129 + 130 + scope: response.scope, 131 + id_token: response.id_token, 132 + refresh_token: response.refresh_token, 133 + access_token: response.access_token, 134 + token_type: response.token_type ?? 'Bearer', 135 + expires_at: 136 + typeof response.expires_in === 'number' ? Date.now() + response.expires_in * 1000 : undefined, 137 + }; 138 + } 139 + }
+63
src/lib/bsky-oauth/agents/session.ts
··· 1 + import type { At } from '@atcute/client/lexicons'; 2 + 3 + import { OAuthResponseError, TokenRefreshError } from '../errors'; 4 + import { database } from '../globals'; 5 + import { getMetadataFromAuthorizationServer } from '../resolver'; 6 + import { CachedGetter } from '../store/getter'; 7 + import { OAuthServerAgent } from './server-agent'; 8 + 9 + import type { DPoPKey } from '../types/dpop'; 10 + import type { TokenSet } from '../types/token'; 11 + 12 + export interface Session { 13 + dpopKey: DPoPKey; 14 + tokenSet: TokenSet; 15 + } 16 + 17 + export const sessions = new CachedGetter<At.DID, Session>( 18 + async (sub, options, storedSession): Promise<Session> => { 19 + if (storedSession === undefined) { 20 + throw new TokenRefreshError(sub, `session deleted by another tab`); 21 + } 22 + 23 + const { dpopKey, tokenSet } = storedSession; 24 + 25 + const metadata = await getMetadataFromAuthorizationServer(tokenSet.iss, { signal: options?.signal }); 26 + const server = new OAuthServerAgent(metadata, dpopKey); 27 + 28 + try { 29 + const newTokenSet = await server.refresh(tokenSet); 30 + 31 + return { dpopKey, tokenSet: newTokenSet }; 32 + } catch (cause) { 33 + if (cause instanceof OAuthResponseError && cause.status === 400 && cause.error === 'invalid_grant') { 34 + throw new TokenRefreshError(sub, `session was revoked`, { cause }); 35 + } 36 + 37 + throw cause; 38 + } 39 + }, 40 + database.sessions, 41 + { 42 + lockKey(sub) { 43 + return `oauth-session-${sub}`; 44 + }, 45 + isStale(_sub, { tokenSet }) { 46 + // Add some lee way to ensure the token is not expired when it 47 + // reaches the server. 48 + return tokenSet.expires_at != null && tokenSet.expires_at < Date.now() + 60e3; 49 + }, 50 + async onStoreError(err, _sub, { tokenSet, dpopKey }) { 51 + // If the token data cannot be stored, let's revoke it 52 + const metadata = await getMetadataFromAuthorizationServer(tokenSet.iss); 53 + const server = new OAuthServerAgent(metadata, dpopKey); 54 + 55 + await server.revoke(tokenSet.refresh_token ?? tokenSet.access_token); 56 + throw err; 57 + }, 58 + }, 59 + ); 60 + 61 + export const getSession = (sub: At.DID, refresh?: boolean) => { 62 + return sessions.get(sub, { noCache: refresh === true, allowStale: refresh === false }); 63 + };
+94
src/lib/bsky-oauth/agents/user-agent.ts
··· 1 + import type { FetchHandlerObject } from '@atcute/client'; 2 + 3 + import { createDPoPFetch } from '../dpop'; 4 + import { CLIENT_ID } from '../env'; 5 + import { authorizationServerMetadataResolver } from '../resolver'; 6 + 7 + import type { DPoPKey } from '../types/dpop'; 8 + import type { TokenSet } from '../types/token'; 9 + 10 + import { OAuthServerAgent } from './server-agent'; 11 + import { getSession, sessions } from './session'; 12 + 13 + export class OAuthUserAgent implements FetchHandlerObject { 14 + #fetch: typeof fetch; 15 + 16 + constructor( 17 + public tokenSet: TokenSet, 18 + public dpopKey: DPoPKey, 19 + ) { 20 + this.#fetch = createDPoPFetch(CLIENT_ID, dpopKey, false); 21 + } 22 + 23 + async #getTokenSet(refresh?: boolean) { 24 + const { tokenSet } = await getSession(this.tokenSet.sub, refresh); 25 + return (this.tokenSet = tokenSet); 26 + } 27 + 28 + async signOut(): Promise<void> { 29 + const sub = this.tokenSet.sub; 30 + 31 + try { 32 + const { tokenSet, dpopKey } = await getSession(sub, false); 33 + 34 + const metadata = await authorizationServerMetadataResolver.get(tokenSet.iss); 35 + const server = new OAuthServerAgent(metadata, dpopKey); 36 + 37 + await server.revoke(tokenSet.access_token); 38 + } finally { 39 + await sessions.deleteStored(sub); 40 + } 41 + } 42 + 43 + async handle(pathname: string, init?: RequestInit): Promise<Response> { 44 + const headers = new Headers(init?.headers); 45 + 46 + let tokens = this.tokenSet; 47 + 48 + let url = new URL(pathname, tokens.aud); 49 + headers.set('authorization', `${tokens.token_type} ${tokens.access_token}`); 50 + 51 + let response = await this.#fetch(url, { ...init, headers }); 52 + if (!isInvalidTokenResponse(response)) { 53 + return response; 54 + } 55 + 56 + try { 57 + // Refresh the token normally first, it could just be that we're behind 58 + const newTokens = await this.#getTokenSet(); 59 + 60 + if (newTokens.expires_at === tokens.expires_at) { 61 + // If it returns the same expiry then we need to force it 62 + tokens = await this.#getTokenSet(true); 63 + } else { 64 + tokens = newTokens; 65 + } 66 + } catch { 67 + return response; 68 + } 69 + 70 + // Stream already consumed, can't retry. 71 + if (init?.body instanceof ReadableStream) { 72 + return response; 73 + } 74 + 75 + url = new URL(pathname, tokens.aud); 76 + headers.set('authorization', `${tokens.token_type} ${tokens.access_token}`); 77 + 78 + return await this.#fetch(url, { ...init, headers }); 79 + } 80 + } 81 + 82 + const isInvalidTokenResponse = (response: Response) => { 83 + if (response.status !== 401) { 84 + return false; 85 + } 86 + 87 + const auth = response.headers.get('www-authenticate'); 88 + 89 + return ( 90 + auth != null && 91 + (auth.startsWith('Bearer ') || auth.startsWith('DPoP ')) && 92 + auth.includes('error="invalid_token"') 93 + ); 94 + };
+157
src/lib/bsky-oauth/dpop.ts
··· 1 + import { database } from './globals'; 2 + import { encoder, extractContentType, randomBytes, toBase64Url, toSha256 } from './utils'; 3 + 4 + import type { DPoPKey } from './types/dpop'; 5 + 6 + export const createES256Key = async (): Promise<DPoPKey> => { 7 + const algorithm = { name: 'ECDSA', namedCurve: 'P-256' } as const; 8 + const usage = ['sign', 'verify'] as const; 9 + 10 + const pair = await crypto.subtle.generateKey(algorithm, false, usage); 11 + const jwk = await crypto.subtle.exportKey('jwk', pair.publicKey); 12 + 13 + return { 14 + key: pair.privateKey, 15 + jwt: { 16 + typ: 'dpop+jwt', 17 + alg: 'ES256', 18 + jwk: { 19 + kty: jwk.kty, 20 + x: jwk.x, 21 + y: jwk.y, 22 + crv: jwk.crv, 23 + }, 24 + }, 25 + }; 26 + }; 27 + 28 + export const createDPoPSignage = (issuer: string, dpopKey: DPoPKey) => { 29 + const headerString = toBase64Url(encoder.encode(JSON.stringify(dpopKey.jwt))); 30 + 31 + const constructPayload = ( 32 + method: string, 33 + url: string, 34 + nonce: string | undefined, 35 + ath: string | undefined, 36 + ) => { 37 + const now = Math.floor(Date.now() / 1e3); 38 + 39 + const payload = { 40 + iss: issuer, 41 + iat: now, 42 + jti: randomBytes(12), 43 + htm: method, 44 + htu: url, 45 + nonce: nonce, 46 + ath: ath, 47 + }; 48 + 49 + return toBase64Url(encoder.encode(JSON.stringify(payload))); 50 + }; 51 + 52 + return async (method: string, url: string, nonce: string | undefined, ath: string | undefined) => { 53 + const payloadString = constructPayload(method, url, nonce, ath); 54 + 55 + const signed = await crypto.subtle.sign( 56 + { name: 'ECDSA', hash: { name: 'SHA-256' } }, 57 + dpopKey.key, 58 + encoder.encode(headerString + '.' + payloadString), 59 + ); 60 + 61 + const signatureString = toBase64Url(new Uint8Array(signed)); 62 + 63 + return headerString + '.' + payloadString + '.' + signatureString; 64 + }; 65 + }; 66 + 67 + export const createDPoPFetch = (issuer: string, dpopKey: DPoPKey, isAuthServer?: boolean): typeof fetch => { 68 + const nonces = database.dpopNonces; 69 + const sign = createDPoPSignage(issuer, dpopKey); 70 + 71 + return async (input, init) => { 72 + const request: Request = init == null && input instanceof Request ? input : new Request(input, init); 73 + 74 + const authorizationHeader = request.headers.get('authorization'); 75 + const ath = authorizationHeader?.startsWith('DPoP ') 76 + ? await toSha256(authorizationHeader.slice(5)) 77 + : undefined; 78 + 79 + const { method, url } = request; 80 + const { origin } = new URL(url); 81 + 82 + let initNonce: string | undefined; 83 + try { 84 + initNonce = await nonces.get(origin); 85 + } catch { 86 + // Ignore get errors, we will just not send a nonce 87 + } 88 + 89 + const initProof = await sign(method, url, initNonce, ath); 90 + request.headers.set('dpop', initProof); 91 + 92 + const initResponse = await fetch(request); 93 + 94 + const nextNonce = initResponse.headers.get('DPoP-Nonce'); 95 + if (!nextNonce || nextNonce === initNonce) { 96 + // No nonce was returned or it is the same as the one we sent. No need to 97 + // update the nonce store, or retry the request. 98 + return initResponse; 99 + } 100 + 101 + // Store the fresh nonce for future requests 102 + try { 103 + await nonces.set(origin, nextNonce); 104 + } catch { 105 + // Ignore set errors 106 + } 107 + 108 + const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer); 109 + if (!shouldRetry) { 110 + // Not a "use_dpop_nonce" error, so there is no need to retry 111 + return initResponse; 112 + } 113 + 114 + // If the input stream was already consumed, we cannot retry the request. A 115 + // solution would be to clone() the request but that would bufferize the 116 + // entire stream in memory which can lead to memory starvation. Instead, we 117 + // will return the original response and let the calling code handle retries. 118 + 119 + if (input === request || init?.body instanceof ReadableStream) { 120 + return initResponse; 121 + } 122 + 123 + const nextProof = await sign(method, url, nextNonce, ath); 124 + const nextRequest = new Request(input, init); 125 + nextRequest.headers.set('dpop', nextProof); 126 + 127 + return await fetch(nextRequest); 128 + }; 129 + }; 130 + 131 + const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean): Promise<boolean> => { 132 + // https://datatracker.ietf.org/doc/html/rfc6750#section-3 133 + // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no 134 + if (isAuthServer === undefined || isAuthServer === false) { 135 + if (response.status === 401) { 136 + const wwwAuth = response.headers.get('www-authenticate'); 137 + if (wwwAuth?.startsWith('DPoP')) { 138 + return wwwAuth.includes('error="use_dpop_nonce"'); 139 + } 140 + } 141 + } 142 + 143 + // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid 144 + if (isAuthServer === undefined || isAuthServer === true) { 145 + if (response.status === 400 && extractContentType(response.headers) === 'application/json') { 146 + try { 147 + const json = await response.clone().json(); 148 + return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce'; 149 + } catch { 150 + // Response too big (to be "use_dpop_nonce" error) or invalid JSON 151 + return false; 152 + } 153 + } 154 + } 155 + 156 + return false; 157 + };
+3
src/lib/bsky-oauth/env.ts
··· 1 + export const REDIRECT_URI = import.meta.env.VITE_OAUTH_REDIRECT_URL; 2 + export const CLIENT_ID = import.meta.env.VITE_OAUTH_CLIENT_ID; 3 + export const SCOPE = import.meta.env.VITE_OAUTH_SCOPE;
+76
src/lib/bsky-oauth/errors.ts
··· 1 + import type { At } from '@atcute/client/lexicons'; 2 + 3 + export class TokenRefreshError extends Error { 4 + constructor( 5 + public readonly sub: At.DID, 6 + message: string, 7 + options?: ErrorOptions, 8 + ) { 9 + super(message, options); 10 + } 11 + } 12 + 13 + export class TokenRevokedError extends Error { 14 + constructor( 15 + public readonly sub: At.DID, 16 + message: string = `session for ${sub} has been revoked`, 17 + ) { 18 + super(message); 19 + } 20 + } 21 + 22 + export class TokenInvalidError extends Error { 23 + constructor( 24 + public readonly sub: At.DID, 25 + message = `invalid session for ${sub}`, 26 + ) { 27 + super(message); 28 + } 29 + } 30 + 31 + export class OAuthResponseError extends Error { 32 + readonly error: string | undefined; 33 + readonly description: string | undefined; 34 + 35 + constructor( 36 + public readonly response: Response, 37 + public readonly data: any, 38 + ) { 39 + const error = ifString(ifObject(data)?.['error']); 40 + const errorDescription = ifString(ifObject(data)?.['error_description']); 41 + 42 + const messageError = error ? `"${error}"` : 'unknown'; 43 + const messageDesc = errorDescription ? `: ${errorDescription}` : ''; 44 + const message = `OAuth ${messageError} error${messageDesc}`; 45 + 46 + super(message); 47 + 48 + this.error = error; 49 + this.description = errorDescription; 50 + } 51 + 52 + get status() { 53 + return this.response.status; 54 + } 55 + 56 + get headers() { 57 + return this.response.headers; 58 + } 59 + } 60 + 61 + export class FetchResponseError extends Error { 62 + constructor( 63 + public readonly response: Response, 64 + public status: number, 65 + message: string, 66 + ) { 67 + super(message); 68 + } 69 + } 70 + 71 + const ifString = (v: unknown): string | undefined => { 72 + return typeof v === 'string' ? v : undefined; 73 + }; 74 + const ifObject = (v: unknown): Record<string, unknown> | undefined => { 75 + return typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as any) : undefined; 76 + };
+3
src/lib/bsky-oauth/globals.ts
··· 1 + import { createOAuthDatabase } from './store/db'; 2 + 3 + export const database = createOAuthDatabase({ name: 'aglais-oauth' });
+221
src/lib/bsky-oauth/resolver.ts
··· 1 + import type { At, ComAtprotoIdentityResolveHandle } from '@atcute/client/lexicons'; 2 + import { getPdsEndpoint, type DidDocument } from '@atcute/client/utils/did'; 3 + 4 + import { isDid } from '~/api/utils/strings'; 5 + 6 + import { database } from './globals'; 7 + import { CachedGetter, type GetCachedOptions } from './store/getter'; 8 + import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types/server'; 9 + import { extractContentType } from './utils'; 10 + import type { ResolvedIdentity } from './types/identity'; 11 + 12 + const DID_WEB_RE = 13 + /^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))((?::[a-zA-Z0-9._%-]*[a-zA-Z0-9._-])*)$/; 14 + 15 + export const handleResolver = new CachedGetter(async (handle: string, options): Promise<At.DID> => { 16 + const url = `https://api.bsky.app` + `/xrpc/com.atproto.identity.resolveHandle` + `?handle=${handle}`; 17 + 18 + const response = await fetch(url, { signal: options?.signal }); 19 + if (!response.ok) { 20 + throw new ResolverError(`got http ${response.status}`); 21 + } 22 + 23 + const json = (await response.json()) as ComAtprotoIdentityResolveHandle.Output; 24 + return json.did; 25 + }, database.handles); 26 + 27 + export const identityResolver = new CachedGetter(async (did: At.DID, options): Promise<DidDocument> => { 28 + const init: RequestInit = { signal: options?.signal }; 29 + 30 + const colon_index = did.indexOf(':', 4); 31 + 32 + const type = did.slice(4, colon_index); 33 + const ident = did.slice(colon_index + 1); 34 + 35 + // 2. retrieve their DID documents 36 + let doc: DidDocument; 37 + 38 + if (type === 'plc') { 39 + const response = await fetch(`https://plc.directory/${did}`, init); 40 + 41 + if (response.status === 404) { 42 + throw new ResolverError('plc_not_found'); 43 + } else if (!response.ok) { 44 + throw new ResolverError('plc_unreachable'); 45 + } 46 + 47 + const json = await response.json(); 48 + 49 + doc = json as DidDocument; 50 + } else if (type === 'web') { 51 + const match = DID_WEB_RE.exec(ident); 52 + if (!match) { 53 + throw new ResolverError('web_invalid'); 54 + } 55 + 56 + const [host, raw_path] = ident; 57 + const path = raw_path ? raw_path.replaceAll(':', '/') : `/.well-known`; 58 + 59 + const response = await fetch(`https://${host}${path}/did.json`, init); 60 + 61 + if (response.status === 404) { 62 + throw new ResolverError('web_not_found'); 63 + } else if (!response.ok) { 64 + throw new ResolverError('web_unreachable'); 65 + } 66 + 67 + const json = await response.json(); 68 + 69 + doc = json as DidDocument; 70 + } else { 71 + throw new ResolverError('did_unsupported'); 72 + } 73 + 74 + return doc; 75 + }, database.didDocuments); 76 + 77 + export const protectedResourceMetadataResolver = new CachedGetter( 78 + async (host: string, options): Promise<ProtectedResourceMetadata> => { 79 + const url = new URL(`/.well-known/oauth-protected-resource`, host); 80 + const response = await fetch(url, { 81 + signal: options?.signal, 82 + redirect: 'manual', 83 + headers: { 84 + accept: 'application/json', 85 + }, 86 + }); 87 + 88 + if (response.status !== 200) { 89 + throw new ResolverError(`unexpected status code; got ${response.status}`); 90 + } 91 + 92 + if (extractContentType(response.headers) !== 'application/json') { 93 + throw new ResolverError(`unexpected content-type`); 94 + } 95 + 96 + const metadata = (await response.json()) as ProtectedResourceMetadata; 97 + if (metadata.resource !== url.origin) { 98 + throw new ResolverError(`unexpected issuer; got ${metadata.resource}`); 99 + } 100 + 101 + return metadata; 102 + }, 103 + database.protectedServerMetadata, 104 + ); 105 + 106 + export const authorizationServerMetadataResolver = new CachedGetter( 107 + async (host: string, options): Promise<AuthorizationServerMetadata> => { 108 + const url = new URL(`/.well-known/oauth-authorization-server`, host); 109 + const response = await fetch(url, { 110 + signal: options?.signal, 111 + redirect: 'manual', 112 + headers: { 113 + accept: 'application/json', 114 + }, 115 + }); 116 + 117 + if (response.status !== 200) { 118 + throw new ResolverError(`unexpected status code; got ${response.status}`); 119 + } 120 + 121 + if (extractContentType(response.headers) !== 'application/json') { 122 + throw new ResolverError(`unexpected content-type`); 123 + } 124 + 125 + const metadata = (await response.json()) as AuthorizationServerMetadata; 126 + if (metadata.issuer !== url.origin) { 127 + throw new ResolverError(`unexpected issuer; got ${metadata.issuer}`); 128 + } 129 + if (!metadata.client_id_metadata_document_supported) { 130 + throw new ResolverError(`authorization server does not support 'client_id_metadata_document'`); 131 + } 132 + if (metadata.require_pushed_authorization_requests && !metadata.pushed_authorization_request_endpoint) { 133 + throw new ResolverError(`authorization server requires PAR but no endpoint is specified`); 134 + } 135 + if (metadata.response_types_supported) { 136 + if (!metadata.response_types_supported.includes('code')) { 137 + throw new ResolverError(`authorization server does not support 'code' response type`); 138 + } 139 + } 140 + 141 + return metadata; 142 + }, 143 + database.authorizationServerMetadata, 144 + ); 145 + 146 + export const resolveFromIdentity = async ( 147 + identifier: string, 148 + options?: GetCachedOptions, 149 + ): Promise<{ identity: ResolvedIdentity; metadata: AuthorizationServerMetadata }> => { 150 + let did: At.DID; 151 + if (isDid(identifier)) { 152 + did = identifier; 153 + } else { 154 + const resolved = await handleResolver.get(identifier, options); 155 + did = resolved; 156 + } 157 + 158 + const doc = await identityResolver.get(did, options); 159 + const pds = getPdsEndpoint(doc); 160 + 161 + if (!pds) { 162 + throw new ResolverError(`missing pds endpoint from ${identifier}`); 163 + } 164 + 165 + return { 166 + identity: { 167 + id: did, 168 + pds: new URL(pds), 169 + }, 170 + metadata: await getMetadataFromResourceServer(pds), 171 + }; 172 + }; 173 + 174 + export const resolveFromService = async ( 175 + input: string, 176 + options?: GetCachedOptions, 177 + ): Promise<{ metadata: AuthorizationServerMetadata }> => { 178 + try { 179 + const metadata = await getMetadataFromResourceServer(input, options); 180 + return { metadata }; 181 + } catch (err) { 182 + if (err instanceof ResolverError) { 183 + try { 184 + const metadata = await getMetadataFromAuthorizationServer(input, options); 185 + return { metadata }; 186 + } catch {} 187 + } 188 + 189 + throw err; 190 + } 191 + }; 192 + 193 + export const getMetadataFromResourceServer = async (input: string, options?: GetCachedOptions) => { 194 + const rs_metadata = await protectedResourceMetadataResolver.get(input, options); 195 + 196 + if (rs_metadata.authorization_servers?.length !== 1) { 197 + throw new ResolverError( 198 + `expected exactly one authorization server; got ${rs_metadata.authorization_servers?.length ?? 0}`, 199 + ); 200 + } 201 + 202 + const issuer = rs_metadata.authorization_servers[0]; 203 + 204 + const as_metadata = await getMetadataFromAuthorizationServer(issuer); 205 + 206 + if (as_metadata.protected_resources) { 207 + if (!as_metadata.protected_resources.includes(rs_metadata.resource)) { 208 + throw new ResolverError(`pds is not in authorization server's protected list`); 209 + } 210 + } 211 + 212 + return as_metadata; 213 + }; 214 + 215 + export const getMetadataFromAuthorizationServer = (input: string, options?: GetCachedOptions) => { 216 + return authorizationServerMetadataResolver.get(input, options); 217 + }; 218 + 219 + export class ResolverError extends Error { 220 + name = 'ResolverError'; 221 + }
+202
src/lib/bsky-oauth/store/db.ts
··· 1 + import { openDB, type IDBPDatabase } from 'idb'; 2 + 3 + import type { At } from '@atcute/client/lexicons'; 4 + import type { DidDocument } from '@atcute/client/utils/did'; 5 + 6 + import type { DPoPKey } from '../types/dpop'; 7 + import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from '../types/server'; 8 + import type { SimpleStore } from '../types/store'; 9 + import type { TokenSet } from '../types/token'; 10 + 11 + export interface OAuthDatabaseOptions { 12 + name: string; 13 + } 14 + 15 + interface SchemaItem<T> { 16 + value: T; 17 + expiresAt: number | null; 18 + } 19 + 20 + interface Schema { 21 + states: { 22 + key: string; 23 + value: { 24 + dpopKey: DPoPKey; 25 + issuer: string; 26 + verifier?: string; 27 + }; 28 + }; 29 + sessions: { 30 + key: At.DID; 31 + value: { 32 + dpopKey: DPoPKey; 33 + tokenSet: TokenSet; 34 + }; 35 + indexes: { 36 + expiresAt: number; 37 + }; 38 + }; 39 + dpopNonces: { 40 + key: string; 41 + value: string; 42 + }; 43 + 44 + handles: { 45 + key: string; 46 + value: At.DID; 47 + }; 48 + didDocuments: { 49 + key: At.DID; 50 + value: DidDocument; 51 + }; 52 + 53 + authorizationServerMetadata: { 54 + key: string; 55 + value: AuthorizationServerMetadata; 56 + }; 57 + protectedServerMetadata: { 58 + key: string; 59 + value: ProtectedResourceMetadata; 60 + }; 61 + } 62 + 63 + const schemaStores = [ 64 + 'states', 65 + 'sessions', 66 + 'dpopNonces', 67 + 'handles', 68 + 'didDocuments', 69 + 'authorizationServerMetadata', 70 + 'protectedServerMetadata', 71 + ]; 72 + 73 + const migrations: Array<(db: IDBPDatabase<any>) => void | Promise<void>> = [ 74 + (db) => { 75 + for (const name of schemaStores) { 76 + const store = db.createObjectStore(name); 77 + store.createIndex('expiresAt', 'expiresAt', { unique: false }); 78 + } 79 + }, 80 + ]; 81 + 82 + export const createOAuthDatabase = ({ name }: OAuthDatabaseOptions) => { 83 + const dbPromise = openDB(name, migrations.length, { 84 + async upgrade(db, prev, next) { 85 + for (let version = prev; version < (next ?? migrations.length); ++version) { 86 + const migration = migrations[version]; 87 + await migration(db); 88 + } 89 + }, 90 + }); 91 + 92 + const interval = setInterval(async () => { 93 + const db = await dbPromise; 94 + const range = IDBKeyRange.upperBound(Date.now()); 95 + 96 + for (const name of schemaStores) { 97 + const tx = db.transaction(name, 'readwrite', { durability: 'relaxed' }); 98 + 99 + const store = tx.objectStore(name); 100 + const index = store.index('expiresAt'); 101 + 102 + const keys = await index.getAllKeys(range); 103 + 104 + await Promise.all(keys.map((key) => store.delete(key))); 105 + } 106 + }, 30e3); 107 + 108 + const createStore = <N extends keyof Schema>( 109 + name: N, 110 + { expiresAt }: { expiresAt: (item: Schema[N]['value']) => null | number }, 111 + ): SimpleStore<Schema[N]['key'], Schema[N]['value']> => { 112 + return { 113 + async get(key) { 114 + const db = await dbPromise; 115 + 116 + const tx = db.transaction(name, 'readwrite', { durability: 'relaxed' }); 117 + const store = tx.objectStore(name); 118 + 119 + const item: SchemaItem<Schema[N]['value']> = await store.get(key); 120 + 121 + const expiresAt = item.expiresAt; 122 + if (expiresAt !== null && Date.now() > expiresAt) { 123 + await store.delete(key); 124 + await tx.done; 125 + return; 126 + } 127 + 128 + return item.value; 129 + }, 130 + async set(key, value) { 131 + const item: SchemaItem<Schema[N]['value']> = { 132 + expiresAt: expiresAt(value), 133 + value: value, 134 + }; 135 + 136 + const db = await dbPromise; 137 + 138 + const tx = db.transaction(name, 'readwrite', { durability: 'strict' }); 139 + const store = tx.objectStore(name); 140 + 141 + await store.put(item, key); 142 + 143 + await tx.done; 144 + }, 145 + async delete(key) { 146 + const db = await dbPromise; 147 + 148 + const tx = db.transaction(name, 'readwrite', { durability: 'strict' }); 149 + const store = tx.objectStore(name); 150 + 151 + await store.delete(key); 152 + 153 + await tx.done; 154 + }, 155 + }; 156 + }; 157 + 158 + const TEN_MINUTES = () => Date.now() + 10 * 60e3; 159 + 160 + return { 161 + [Symbol.asyncDispose]() { 162 + return this.dispose(); 163 + }, 164 + 165 + async dispose() { 166 + clearInterval(interval); 167 + 168 + const db = await dbPromise; 169 + db.close(); 170 + }, 171 + 172 + states: createStore('states', { 173 + expiresAt: TEN_MINUTES, 174 + }), 175 + sessions: createStore('sessions', { 176 + expiresAt: ({ tokenSet }) => { 177 + if (tokenSet.refresh_token) { 178 + return null; 179 + } 180 + 181 + return tokenSet.expires_at ?? null; 182 + }, 183 + }), 184 + dpopNonces: createStore('dpopNonces', { 185 + expiresAt: (_item) => Date.now() + 600e3, 186 + }), 187 + 188 + handles: createStore('handles', { 189 + expiresAt: TEN_MINUTES, 190 + }), 191 + didDocuments: createStore('didDocuments', { 192 + expiresAt: TEN_MINUTES, 193 + }), 194 + 195 + authorizationServerMetadata: createStore('authorizationServerMetadata', { 196 + expiresAt: TEN_MINUTES, 197 + }), 198 + protectedServerMetadata: createStore('protectedServerMetadata', { 199 + expiresAt: TEN_MINUTES, 200 + }), 201 + }; 202 + };
+155
src/lib/bsky-oauth/store/getter.ts
··· 1 + import type { Awaitable, SimpleStore } from '../types/store'; 2 + 3 + export interface GetCachedOptions { 4 + signal?: AbortSignal; 5 + noCache?: boolean; 6 + allowStale?: boolean; 7 + } 8 + 9 + export type Getter<K, V> = ( 10 + key: K, 11 + options: undefined | GetCachedOptions, 12 + storedValue: undefined | V, 13 + ) => Awaitable<V>; 14 + 15 + export interface CachedGetterOptions<K, V> { 16 + isStale?: (key: K, value: V) => boolean | PromiseLike<boolean>; 17 + lockKey?: (key: K) => string | undefined; 18 + onStoreError?: (err: unknown, key: K, value: V) => void | PromiseLike<void>; 19 + deleteOnError?: (err: unknown, key: K, value: V) => boolean | PromiseLike<boolean>; 20 + } 21 + 22 + type PendingItem<V> = Promise<{ value: V; isFresh: boolean }>; 23 + 24 + const returnTrue = () => true; 25 + const returnFalse = () => false; 26 + 27 + export class CachedGetter<K extends string | number, V extends {} | null> { 28 + #pending = new Map<K, PendingItem<V>>(); 29 + 30 + #getter: Getter<K, V>; 31 + #store: SimpleStore<K, V>; 32 + #options?: Readonly<CachedGetterOptions<K, V>>; 33 + 34 + constructor(getter: Getter<K, V>, store: SimpleStore<K, V>, options?: Readonly<CachedGetterOptions<K, V>>) { 35 + this.#getter = getter; 36 + this.#store = store; 37 + this.#options = options; 38 + } 39 + 40 + async get(key: K, options?: GetCachedOptions): Promise<V> { 41 + options?.signal?.throwIfAborted(); 42 + 43 + const parentOptions = this.#options; 44 + const pending = this.#pending; 45 + 46 + const isStale = parentOptions?.isStale; 47 + 48 + const allowStored: (value: V) => Awaitable<boolean> = options?.noCache 49 + ? returnFalse // Never allow stored values to be returned 50 + : options?.allowStale || isStale == null 51 + ? returnTrue // Always allow stored values to be returned 52 + : async (value: V) => !(await isStale(key, value)); 53 + 54 + // As long as concurrent requests are made for the same key, only one 55 + // request will be made to the cache & getter function at a time. This works 56 + // because there is no async operation between the while() loop and the 57 + // pending.set() call. Because of the "single threaded" nature of 58 + // JavaScript, the pending item will be set before the next iteration of the 59 + // while loop. 60 + let previousExecutionFlow: undefined | PendingItem<V>; 61 + while ((previousExecutionFlow = pending.get(key))) { 62 + try { 63 + const { isFresh, value } = await previousExecutionFlow; 64 + 65 + if (isFresh) return value; 66 + if (await allowStored(value)) return value; 67 + } catch { 68 + // Ignore errors from previous execution flows (they will have been 69 + // propagated by that flow). 70 + } 71 + 72 + options?.signal?.throwIfAborted(); 73 + } 74 + 75 + const lockKey = parentOptions?.lockKey?.(key); 76 + const run = async (): PendingItem<V> => { 77 + const storedValue = await this.getStored(key); 78 + 79 + if (storedValue !== undefined && (await allowStored(storedValue))) { 80 + // Use the stored value as return value for the current execution 81 + // flow. Notify other concurrent execution flows (that should be 82 + // "stuck" in the loop before until this promise resolves) that we got 83 + // a value, but that it came from the store (isFresh = false). 84 + return { isFresh: false, value: storedValue }; 85 + } 86 + 87 + return Promise.resolve() 88 + .then((): Awaitable<V> => (0, this.#getter)(key, options, storedValue)) 89 + .then( 90 + async (value): PendingItem<V> => { 91 + await this.setStored(key, value); 92 + return { isFresh: true, value }; 93 + }, 94 + async (err): Promise<never> => { 95 + if (storedValue !== undefined) { 96 + try { 97 + const deleteOnError = parentOptions?.deleteOnError; 98 + if (await deleteOnError?.(err, key, storedValue)) { 99 + await this.deleteStored(key, err); 100 + } 101 + } catch (error) { 102 + throw new AggregateError([err, error], 'Error while deleting stored value'); 103 + } 104 + } 105 + 106 + throw err; 107 + }, 108 + ); 109 + }; 110 + 111 + let promise: PendingItem<V>; 112 + 113 + if (lockKey !== undefined) { 114 + promise = navigator.locks.request(lockKey, run); 115 + } else { 116 + promise = run(); 117 + } 118 + 119 + promise = promise.finally(() => pending.delete(key)); 120 + 121 + if (pending.has(key)) { 122 + // This should never happen. Indeed, there must not be any 'await' 123 + // statement between this and the loop iteration check meaning that 124 + // this.pending.get returned undefined. It is there to catch bugs that 125 + // would occur in future changes to the code. 126 + throw new Error('Concurrent request for the same key'); 127 + } 128 + 129 + pending.set(key, promise); 130 + 131 + const { value } = await promise; 132 + return value; 133 + } 134 + 135 + async getStored(key: K): Promise<V | undefined> { 136 + try { 137 + return await this.#store.get(key); 138 + } catch (err) { 139 + return undefined; 140 + } 141 + } 142 + 143 + async setStored(key: K, value: V): Promise<void> { 144 + try { 145 + await this.#store.set(key, value); 146 + } catch (err) { 147 + const onStoreError = this.#options?.onStoreError; 148 + await onStoreError?.(err, key, value); 149 + } 150 + } 151 + 152 + async deleteStored(key: K, _cause?: unknown): Promise<void> { 153 + await this.#store.delete(key); 154 + } 155 + }
+82
src/lib/bsky-oauth/types/client.ts
··· 1 + export interface ClientMetadata { 2 + redirect_uris: string[]; 3 + response_types: ( 4 + | 'code' 5 + | 'token' 6 + | 'none' 7 + | 'code id_token token' 8 + | 'code id_token' 9 + | 'code token' 10 + | 'id_token token' 11 + | 'id_token' 12 + )[]; 13 + grant_types: ( 14 + | 'authorization_code' 15 + | 'implicit' 16 + | 'refresh_token' 17 + | 'password' 18 + | 'client_credentials' 19 + | 'urn:ietf:params:oauth:grant-type:jwt-bearer' 20 + | 'urn:ietf:params:oauth:grant-type:saml2-bearer' 21 + )[]; 22 + scope?: string; 23 + token_endpoint_auth_method?: 24 + | 'none' 25 + | 'client_secret_basic' 26 + | 'client_secret_jwt' 27 + | 'client_secret_post' 28 + | 'private_key_jwt' 29 + | 'self_signed_tls_client_auth' 30 + | 'tls_client_auth'; 31 + token_endpoint_auth_signing_alg?: string; 32 + introspection_endpoint_auth_method?: 33 + | 'none' 34 + | 'client_secret_basic' 35 + | 'client_secret_jwt' 36 + | 'client_secret_post' 37 + | 'private_key_jwt' 38 + | 'self_signed_tls_client_auth' 39 + | 'tls_client_auth'; 40 + introspection_endpoint_auth_signing_alg?: string; 41 + revocation_endpoint_auth_method?: 42 + | 'none' 43 + | 'client_secret_basic' 44 + | 'client_secret_jwt' 45 + | 'client_secret_post' 46 + | 'private_key_jwt' 47 + | 'self_signed_tls_client_auth' 48 + | 'tls_client_auth'; 49 + revocation_endpoint_auth_signing_alg?: string; 50 + pushed_authorization_request_endpoint_auth_method?: 51 + | 'none' 52 + | 'client_secret_basic' 53 + | 'client_secret_jwt' 54 + | 'client_secret_post' 55 + | 'private_key_jwt' 56 + | 'self_signed_tls_client_auth' 57 + | 'tls_client_auth'; 58 + pushed_authorization_request_endpoint_auth_signing_alg?: string; 59 + userinfo_signed_response_alg?: string; 60 + userinfo_encrypted_response_alg?: string; 61 + jwks_uri?: string; 62 + jwks?: unknown; 63 + application_type?: 'web' | 'native'; 64 + subject_type?: 'public' | 'pairwise'; 65 + request_object_signing_alg?: string; 66 + id_token_signed_response_alg?: string; 67 + authorization_signed_response_alg?: string; 68 + authorization_encrypted_response_enc?: 'A128CBC-HS256'; 69 + authorization_encrypted_response_alg?: string; 70 + client_id?: string; 71 + client_name?: string; 72 + client_uri?: string; 73 + policy_uri?: string; 74 + tos_uri?: string; 75 + logo_uri?: string; 76 + default_max_age?: number; 77 + require_auth_time?: boolean; 78 + contacts?: string[]; 79 + tls_client_certificate_bound_access_tokens?: boolean; 80 + dpop_bound_access_tokens?: boolean; 81 + authorization_details_types?: string[]; 82 + }
+4
src/lib/bsky-oauth/types/dpop.ts
··· 1 + export interface DPoPKey { 2 + key: CryptoKey; 3 + jwt: { typ: 'dpop+jwt'; alg: string; jwk: JsonWebKey }; 4 + }
+6
src/lib/bsky-oauth/types/identity.ts
··· 1 + import type { At } from '@atcute/client/lexicons'; 2 + 3 + export interface ResolvedIdentity { 4 + id: At.DID; 5 + pds: URL; 6 + }
+4
src/lib/bsky-oauth/types/par.ts
··· 1 + export interface OAuthParResponse { 2 + request_uri: string; 3 + expires_in: number; 4 + }
+56
src/lib/bsky-oauth/types/server.ts
··· 1 + export interface ProtectedResourceMetadata { 2 + resource: string; 3 + jwks_uri?: string; 4 + authorization_servers?: string[]; 5 + scopes_supported?: string[]; 6 + bearer_methods_supported?: ('header' | 'body' | 'query')[]; 7 + resource_signing_alg_values_supported?: string[]; 8 + resource_documentation?: string; 9 + resource_policy_uri?: string; 10 + resource_tos_uri?: string; 11 + } 12 + 13 + export interface AuthorizationServerMetadata { 14 + issuer: string; 15 + authorization_endpoint: string; 16 + token_endpoint: string; 17 + jwks_uri?: string; 18 + scopes_supported?: string[]; 19 + claims_supported?: string[]; 20 + claims_locales_supported?: string[]; 21 + claims_parameter_supported?: boolean; 22 + request_parameter_supported?: boolean; 23 + request_uri_parameter_supported?: boolean; 24 + require_request_uri_registration?: boolean; 25 + subject_types_supported?: string[]; 26 + response_types_supported?: string[]; 27 + response_modes_supported?: string[]; 28 + grant_types_supported?: string[]; 29 + code_challenge_methods_supported?: string[]; 30 + ui_locales_supported?: string[]; 31 + id_token_signing_alg_values_supported?: string[]; 32 + display_values_supported?: string[]; 33 + request_object_signing_alg_values_supported?: string[]; 34 + authorization_response_iss_parameter_supported?: boolean; 35 + authorization_details_types_supported?: string[]; 36 + request_object_encryption_alg_values_supported?: string[]; 37 + request_object_encryption_enc_values_supported?: string[]; 38 + token_endpoint_auth_methods_supported?: string[]; 39 + token_endpoint_auth_signing_alg_values_supported?: string[]; 40 + revocation_endpoint?: string; 41 + revocation_endpoint_auth_methods_supported?: string[]; 42 + revocation_endpoint_auth_signing_alg_values_supported?: string[]; 43 + introspection_endpoint?: string; 44 + introspection_endpoint_auth_methods_supported?: string[]; 45 + introspection_endpoint_auth_signing_alg_values_supported?: string[]; 46 + pushed_authorization_request_endpoint?: string; 47 + pushed_authorization_request_endpoint_auth_methods_supported?: string[]; 48 + pushed_authorization_request_endpoint_auth_signing_alg_values_supported?: string[]; 49 + require_pushed_authorization_requests?: boolean; 50 + userinfo_endpoint?: string; 51 + end_session_endpoint?: string; 52 + registration_endpoint?: string; 53 + dpop_signing_alg_values_supported?: string[]; 54 + protected_resources?: string[]; 55 + client_id_metadata_document_supported?: boolean; 56 + }
+10
src/lib/bsky-oauth/types/store.ts
··· 1 + export interface SimpleStore<K extends string | number, V extends {} | null> { 2 + /** 3 + * @return undefined if the key is not in the store (which is why Value cannot contain "undefined"). 4 + */ 5 + get: (key: K) => Promise<undefined | V>; 6 + set: (key: K, value: V) => Promise<void>; 7 + delete: (key: K) => Promise<void>; 8 + } 9 + 10 + export type Awaitable<T> = T | Promise<T>;
+37
src/lib/bsky-oauth/types/token.ts
··· 1 + import type { At } from '@atcute/client/lexicons'; 2 + 3 + export interface OAuthTokenResponse { 4 + access_token: string; 5 + // Can be DPoP or Bearer, normalize casing. 6 + token_type: string; 7 + issuer?: string; 8 + sub?: string; 9 + scope?: string; 10 + id_token?: `${string}.${string}.${string}`; 11 + refresh_token?: string; 12 + expires_in?: number; 13 + authorization_details?: 14 + | { 15 + type: string; 16 + locations?: string[]; 17 + actions?: string[]; 18 + datatypes?: string[]; 19 + identifier?: string; 20 + privileges?: string[]; 21 + }[] 22 + | undefined; 23 + } 24 + 25 + export interface TokenSet { 26 + sub: At.DID; 27 + iss: string; 28 + aud: string; 29 + scope?: string; 30 + 31 + id_token?: `${string}.${string}.${string}`; 32 + refresh_token?: string; 33 + access_token: string; 34 + token_type: string; 35 + /** ISO Date */ 36 + expires_at?: number; 37 + }
+98
src/lib/bsky-oauth/utils.ts
··· 1 + type FalsyValue = false | 0 | null | undefined; 2 + 3 + export const decoder = new TextDecoder(); 4 + export const encoder = new TextEncoder(); 5 + 6 + export const extractContentType = (headers: Headers): string | undefined => { 7 + return headers.get('content-type')?.split(';')[0]; 8 + }; 9 + 10 + export const followAbortSignals = (...signals: (AbortSignal | FalsyValue)[]): AbortSignal | undefined => { 11 + const filtered = signals.filter((v) => !!v); 12 + 13 + if (filtered.length === 0) { 14 + return; 15 + } 16 + if (filtered.length === 1) { 17 + return filtered[0]; 18 + } 19 + 20 + const controller = new AbortController(); 21 + const own = controller.signal; 22 + 23 + for (let idx = 0, len = filtered.length; idx < len; idx++) { 24 + const signal = filtered[idx]; 25 + 26 + if (signal.aborted) { 27 + controller.abort(signal.reason); 28 + break; 29 + } 30 + 31 + signal.addEventListener('abort', () => controller.abort(signal.reason), { signal: own }); 32 + } 33 + 34 + return own; 35 + }; 36 + 37 + export const toBase64Url = (input: Uint8Array): string => { 38 + const CHUNK_SIZE = 0x8000; 39 + const arr = []; 40 + 41 + for (let i = 0; i < input.byteLength; i += CHUNK_SIZE) { 42 + // @ts-expect-error 43 + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); 44 + } 45 + 46 + return btoa(arr.join('')).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 47 + }; 48 + 49 + export const fromBase64Url = (input: string): Uint8Array => { 50 + try { 51 + const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')); 52 + const bytes = new Uint8Array(binary.length); 53 + 54 + for (let i = 0; i < binary.length; i++) { 55 + bytes[i] = binary.charCodeAt(i); 56 + } 57 + 58 + return bytes; 59 + } catch (err) { 60 + throw new TypeError(`invalid base64url`, { cause: err }); 61 + } 62 + }; 63 + 64 + export const parseB64uJson = (input: string) => { 65 + const bytes = fromBase64Url(input); 66 + const text = decoder.decode(bytes); 67 + 68 + return JSON.parse(text); 69 + }; 70 + 71 + export const toSha256 = async (input: string): Promise<string> => { 72 + const bytes = encoder.encode(input); 73 + const digest = await crypto.subtle.digest('SHA-256', bytes); 74 + 75 + return toBase64Url(new Uint8Array(digest)); 76 + }; 77 + 78 + export const randomBytes = (length: number): string => { 79 + return toBase64Url(crypto.getRandomValues(new Uint8Array(length))); 80 + }; 81 + 82 + export const generateState = (): string => { 83 + return randomBytes(16); 84 + }; 85 + 86 + export const generateNonce = (): string => { 87 + return randomBytes(16); 88 + }; 89 + 90 + export const generatePKCE = async (): Promise<{ verifier: string; challenge: string; method: string }> => { 91 + const verifier = randomBytes(32); 92 + 93 + return { 94 + verifier: verifier, 95 + challenge: await toSha256(verifier), 96 + method: 'S256', 97 + }; 98 + };
-7
src/lib/preferences/sessions.ts
··· 1 1 import type { At } from '@atcute/client/lexicons'; 2 - import type { AtpSessionData } from '@atcute/client/middlewares/auth'; 3 2 4 3 export interface SessionPreferenceSchema { 5 4 $version: 1; ··· 10 9 export interface AccountData { 11 10 /** Account DID */ 12 11 readonly did: At.DID; 13 - /** Account's PDS or entryway (bsky.social instances) */ 14 - service: string; 15 - /** Account's session data, from `BskyAuth` */ 16 - session: AtpSessionData; 17 - /** Whether an account has a defined scope, from app passwords. */ 18 - scope: 'limited' | 'privileged' | undefined; 19 12 }
+7 -6
src/lib/states/agent.tsx
··· 1 1 import { createContext, createMemo, useContext, type JSX, type ParentProps } from 'solid-js'; 2 2 3 - import { XRPC } from '@atcute/client'; 4 - import type { AtpAuth } from '@atcute/client/middlewares/auth'; 3 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 4 + import type { At } from '@atcute/client/lexicons'; 5 5 import { QueryClient, QueryClientProvider } from '@mary/solid-query'; 6 6 7 7 import { assert } from '../invariant'; ··· 11 11 import { useSession } from './session'; 12 12 13 13 export interface AgentContext { 14 + did: At.DID | null; 14 15 rpc: XRPC; 15 - auth: AtpAuth | null; 16 16 persister: ReturnType<typeof createQueryPersister>; 17 17 } 18 18 ··· 26 26 27 27 if (currentAccount) { 28 28 return { 29 + did: currentAccount.did, 30 + 29 31 rpc: currentAccount.rpc, 30 - auth: currentAccount.auth, 31 32 persister: createQueryPersister({ name: `queryCache-${currentAccount.did}` }), 32 33 }; 33 34 } 34 35 35 36 return { 36 - rpc: new XRPC({ service: 'https://public.api.bsky.app' }), 37 - auth: null, 37 + did: null, 38 + rpc: new XRPC({ handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) }), 38 39 persister: createQueryPersister({ name: `queryCache-public` }), 39 40 }; 40 41 });
+59 -89
src/lib/states/session.tsx
··· 2 2 batch, 3 3 createContext, 4 4 createEffect, 5 + createMemo, 5 6 createRoot, 6 7 createSignal, 7 8 untrack, 8 9 useContext, 9 10 type ParentProps, 10 11 } from 'solid-js'; 11 - import { unwrap } from 'solid-js/store'; 12 12 13 13 import { XRPC } from '@atcute/client'; 14 14 import type { At } from '@atcute/client/lexicons'; 15 - import { AtpAuth, type AtpAccessJwt, type AtpAuthOptions } from '@atcute/client/middlewares/auth'; 16 - import { AtpMod } from '@atcute/client/middlewares/mod'; 17 - import { decodeJwt } from '@atcute/client/utils/jwt'; 18 15 19 16 import { BLUESKY_MODERATION_DID } from '~/api/defaults'; 20 17 21 - import { globalEvents } from '~/globals/events'; 22 18 import { sessions } from '~/globals/preferences'; 23 19 20 + import { attachLabelerHeaders, type Labeler } from '../atproto/labeler'; 21 + import { OAuthUserAgent } from '../bsky-oauth/agents/user-agent'; 24 22 import { makeAbortable } from '../hooks/abortable'; 25 23 import type { PerAccountPreferenceSchema } from '../preferences/account'; 26 24 import type { AccountData } from '../preferences/sessions'; 27 25 import { createReactiveLocalStorage, isExternalWriting } from '../signals/storage'; 28 26 27 + import { OAuthServerAgent } from '../bsky-oauth/agents/server-agent'; 28 + import { getSession } from '../bsky-oauth/agents/session'; 29 + import { database } from '../bsky-oauth/globals'; 30 + import { getMetadataFromAuthorizationServer } from '../bsky-oauth/resolver'; 29 31 import { assert } from '../invariant'; 30 32 import { mapDefined } from '../misc'; 31 - 32 - interface LoginOptions { 33 - service: string; 34 - identifier: string; 35 - password: string; 36 - authFactorToken?: string; 37 - } 38 33 39 34 export interface CurrentAccountState { 40 35 readonly did: At.DID; ··· 42 37 readonly preferences: PerAccountPreferenceSchema; 43 38 44 39 readonly rpc: XRPC; 45 - readonly auth: AtpAuth; 40 + readonly session: OAuthUserAgent; 46 41 readonly _cleanup: () => void; 47 42 } 48 43 ··· 50 45 readonly currentAccount: CurrentAccountState | undefined; 51 46 52 47 getAccounts(): AccountData[]; 53 - resumeSession(account: AccountData): Promise<void>; 54 - removeAccount(account: AccountData): void; 48 + resumeSession(did: At.DID): Promise<void>; 49 + removeAccount(did: At.DID): Promise<void>; 55 50 56 - login(opts: LoginOptions): Promise<void>; 57 - logout(): void; 51 + logout(): Promise<void>; 58 52 } 59 53 60 54 const Context = createContext<SessionContext>(); ··· 70 64 }); 71 65 }; 72 66 73 - const createAccountState = (account: AccountData, rpc: XRPC, auth: AtpAuth): CurrentAccountState => { 67 + const createAccountState = ( 68 + account: AccountData, 69 + session: OAuthUserAgent, 70 + rpc: XRPC, 71 + ): CurrentAccountState => { 74 72 return createRoot((cleanup): CurrentAccountState => { 75 73 const preferences = createAccountPreferences(account.did); 76 - const mod = new AtpMod(rpc); 77 74 78 75 const [abortable] = makeAbortable(); 79 76 80 - createEffect(() => { 81 - const entries = Object.entries(preferences.moderation.labelers); 82 - mod.labelers = entries.map(([did, info]) => ({ did: did as At.DID, redact: info.redact })); 77 + const labelers = createMemo((): Labeler[] => { 78 + return Object.entries(preferences.moderation.labelers).map(([did, info]): Labeler => { 79 + return { did: did as At.DID, redact: info.redact }; 80 + }); 83 81 }); 82 + 83 + // A bit of a hack, but works right now. 84 + rpc.handle = attachLabelerHeaders(rpc.handle, labelers); 84 85 85 86 createEffect(() => { 86 87 const signal = abortable(); ··· 125 126 data: account, 126 127 preferences: preferences, 127 128 128 - auth: auth, 129 129 rpc: rpc, 130 + session: session, 130 131 _cleanup: cleanup, 131 132 }; 132 133 }, null); 133 134 }; 134 135 135 - const getAuthOptions = (): AtpAuthOptions => { 136 - return { 137 - onExpired() { 138 - globalEvents.emit('sessionexpired'); 139 - }, 140 - onSessionUpdate(session) { 141 - const did = session.did; 142 - const existing = sessions.accounts.find((acc) => acc.did === did); 143 - 144 - if (existing) { 145 - batch(() => Object.assign(existing.session, session)); 146 - } 147 - }, 148 - }; 149 - }; 150 - 151 136 const context: SessionContext = { 152 137 get currentAccount() { 153 138 return state(); ··· 156 141 getAccounts(): AccountData[] { 157 142 return sessions.accounts; 158 143 }, 159 - async resumeSession(account: AccountData): Promise<void> { 144 + async resumeSession(did: At.DID): Promise<void> { 145 + const account = sessions.accounts.find((acc) => acc.did === did); 146 + if (!account) { 147 + return; 148 + } 149 + 160 150 const signal = getSignal(); 161 - const session = unwrap(account.session); 162 151 163 - const rpc = new XRPC({ service: account.service }); 164 - const auth = new AtpAuth(rpc, getAuthOptions()); 152 + const { tokenSet, dpopKey } = await getSession(did); 165 153 166 - await auth.resume(session); 154 + const session = new OAuthUserAgent(tokenSet, dpopKey); 155 + const rpc = new XRPC({ handler: session }); 156 + 167 157 signal.throwIfAborted(); 168 158 169 159 batch(() => { 170 - sessions.active = account.did; 171 - sessions.accounts = [account, ...sessions.accounts.filter((acc) => acc.did !== session.did)]; 172 - replaceState(createAccountState(account, rpc, auth)); 160 + sessions.active = did; 161 + sessions.accounts = [account, ...sessions.accounts.filter((acc) => acc.did !== did)]; 162 + 163 + replaceState(createAccountState(account, session, rpc)); 173 164 }); 174 165 }, 175 - removeAccount(account: AccountData): void { 166 + 167 + async removeAccount(did: At.DID): Promise<void> { 176 168 const $state = untrack(state); 177 - const isLoggedIn = $state !== undefined && $state.did === account.did; 169 + const isLoggedIn = $state !== undefined && $state.did === did; 178 170 179 171 batch(() => { 180 172 if (isLoggedIn) { 181 173 replaceState(undefined); 182 174 } 175 + 176 + sessions.accounts = sessions.accounts.filter((acc) => acc.did !== did); 183 177 }); 184 - }, 185 178 186 - async login(opts: LoginOptions): Promise<void> { 187 - const signal = getSignal(); 179 + try { 180 + if (isLoggedIn) { 181 + const session = $state.session; 188 182 189 - const rpc = new XRPC({ service: opts.service }); 190 - const auth = new AtpAuth(rpc, getAuthOptions()); 191 - 192 - await auth.login({ identifier: opts.identifier, password: opts.password, code: opts.authFactorToken }); 193 - signal.throwIfAborted(); 183 + await session.signOut(); 184 + } else { 185 + const { dpopKey, tokenSet } = await getSession(did, false); 194 186 195 - const session = auth.session!; 196 - const sessionJwt = decodeJwt(session.accessJwt) as AtpAccessJwt; 187 + const as_meta = await getMetadataFromAuthorizationServer(tokenSet.iss); 188 + const server = new OAuthServerAgent(as_meta, dpopKey); 197 189 198 - const scope = sessionJwt.scope; 199 - let accountScope: AccountData['scope']; 200 - if (scope === 'com.atproto.appPass') { 201 - accountScope = 'limited'; 202 - } else if (scope === 'com.atproto.appPassPrivileged') { 203 - accountScope = 'privileged'; 190 + await server.revoke(tokenSet.access_token); 191 + } 192 + } finally { 193 + await database.sessions.delete(did); 204 194 } 205 - 206 - const account: AccountData = { 207 - did: session.did, 208 - service: opts.service, 209 - session: session, 210 - scope: accountScope, 211 - }; 212 - 213 - batch(() => { 214 - sessions.active = account.did; 215 - sessions.accounts = [account, ...sessions.accounts.filter((acc) => acc.did !== session.did)]; 216 - replaceState(createAccountState(account, rpc, auth)); 217 - }); 218 195 }, 219 - logout(): void { 196 + async logout(): Promise<void> { 220 197 const $state = untrack(state); 221 198 if ($state !== undefined) { 222 - this.removeAccount($state.data); 199 + return this.removeAccount($state.did); 223 200 } 224 201 }, 225 202 }; 226 203 227 204 createEffect(() => { 228 205 const active = sessions.active; 229 - const activeAccount = active && sessions.accounts.find((acc) => acc.did === active); 230 206 231 207 // Only run this on external changes 232 208 if (isExternalWriting) { ··· 241 217 replaceState(undefined); 242 218 } 243 219 244 - // Account data exists, try to login as that account 245 - if (activeAccount) { 246 - context.resumeSession(activeAccount); 247 - } 248 - } else if (activeAccount) { 249 - // It's likely that this external write occured due to session changes 250 - // so update it to whatever it is now 251 - untrackedState.auth.session = unwrap(activeAccount.session); 220 + // Try to resume from this new account if we have it. 221 + context.resumeSession(active); 252 222 } 253 223 } else if (untrackedState) { 254 224 // No active account yet we have a session, log out
+6 -9
src/main.tsx
··· 5 5 import { createSignal, onMount, type JSX } from 'solid-js'; 6 6 import { render } from 'solid-js/web'; 7 7 8 + import type { At } from '@atcute/client/lexicons'; 9 + 8 10 import * as navigation from './globals/navigation'; 9 11 import * as preferences from './globals/preferences'; 10 12 11 13 import { on } from './lib/misc'; 12 14 import { configureRouter } from './lib/navigation/router'; 13 - 14 - import type { AccountData } from './lib/preferences/sessions'; 15 15 16 16 import { AgentProvider } from './lib/states/agent'; 17 17 import { BookmarksProvider } from './lib/states/bookmarks'; ··· 35 35 const session = useSession(); 36 36 37 37 onMount(() => { 38 - const resumeAccount = async (account: AccountData | undefined) => { 38 + const resumeAccount = async (did: At.DID | undefined) => { 39 39 try { 40 - if (account) { 41 - await session.resumeSession(account); 40 + if (did) { 41 + await session.resumeSession(did); 42 42 } 43 43 } finally { 44 44 setReady(true); ··· 46 46 }; 47 47 48 48 { 49 - const { active, accounts } = preferences.sessions; 50 - const account = active && accounts.find((acc) => acc.did === active); 51 - 52 - resumeAccount(account); 49 + resumeAccount(preferences.sessions.active); 53 50 } 54 51 }); 55 52
+8
src/routes.ts
··· 22 22 23 23 const routes: RouteDefinition[] = [ 24 24 { 25 + path: '/oauth/callback', 26 + component: lazy(() => import('./views/oauth-callback')), 27 + meta: { 28 + public: true, 29 + }, 30 + }, 31 + 32 + { 25 33 path: '/', 26 34 component: lazy(() => import('./views/main/home')), 27 35 single: true,
+30 -10
src/shell.tsx
··· 1 - import { Suspense, lazy, type Accessor, type Component, type ComponentProps } from 'solid-js'; 1 + import { 2 + ErrorBoundary, 3 + Suspense, 4 + createMemo, 5 + lazy, 6 + type Accessor, 7 + type Component, 8 + type ComponentProps, 9 + } from 'solid-js'; 2 10 3 11 import type { AppBskyNotificationGetUnreadCount } from '@atcute/client/lexicons'; 4 12 import type { DefinedCreateQueryResult } from '@mary/solid-query'; ··· 20 28 import MagnifyingGlassOutlinedIcon from './components/icons-central/magnifying-glass-outline'; 21 29 import MailOutlinedIcon from './components/icons-central/mail-outline'; 22 30 import MailSolidIcon from './components/icons-central/mail-solid'; 31 + import ErrorPage from './views/_error'; 23 32 24 33 const SignedOutView = lazy(() => import('./views/_signed-out')); 25 34 ··· 28 37 29 38 // Will always match because we've set a 404 handler. 30 39 const route = useMatchedRoute() as Accessor<MatchedRouteState>; 31 - const unread = createNotificationCountQuery(); 40 + 41 + const showNavBar = createMemo((): boolean => { 42 + return !!(currentAccount && route().def.meta?.main); 43 + }); 44 + 45 + const unread = createNotificationCountQuery({ 46 + get disabled() { 47 + return !showNavBar(); 48 + }, 49 + }); 32 50 33 51 return ( 34 52 <div ··· 41 59 <RouterView 42 60 render={({ def }) => { 43 61 return ( 44 - <Suspense 45 - children={(() => { 46 - if (!currentAccount && !def.meta?.public) { 47 - return <SignedOutView />; 48 - } 62 + <ErrorBoundary fallback={(error, reset) => <ErrorPage error={error} reset={reset} />}> 63 + <Suspense 64 + children={(() => { 65 + if (!currentAccount && !def.meta?.public) { 66 + return <SignedOutView />; 67 + } 49 68 50 - return <def.component />; 51 - })()} 52 - /> 69 + return <def.component />; 70 + })()} 71 + /> 72 + </ErrorBoundary> 53 73 ); 54 74 }} 55 75 />
+10
src/views/_error/index.tsx
··· 1 + export interface ErrorPageProps { 2 + error: unknown; 3 + reset: () => void; 4 + } 5 + 6 + const ErrorPage = ({ error, reset: retry }: ErrorPageProps) => { 7 + return <div>something went wrong</div>; 8 + }; 9 + 10 + export default ErrorPage;
+7 -5
src/views/main/notifications.tsx
··· 18 18 19 19 // We want to differentiate a refetch done by the user and one that's done 20 20 // by us from the route enter callback. 21 - const [isRefetching, setIsRefetching] = createSignal(false); 21 + const [isManualRefetch, setIsManualRefetch] = createSignal(false); 22 22 23 23 const isStale = () => { 24 24 if (unread.dataUpdatedAt > firstFetchedAt()) { ··· 30 30 31 31 const refetch = async () => { 32 32 try { 33 - setIsRefetching(true); 33 + setIsManualRefetch(true); 34 34 await reset(); 35 35 } finally { 36 - setIsRefetching(false); 36 + setIsManualRefetch(false); 37 37 } 38 38 }; 39 39 ··· 72 72 // Only show refreshing if: 73 73 // - User is explicitly refreshing 74 74 // - We're doing an automatic refresh with an unread count 75 - isRefreshing={isRefetching() || (feed.isRefetching && unread.data.count > 0)} 76 - onEndReached={() => feed.fetchNextPage()} 75 + isRefreshing={isManualRefetch() || (feed.isRefetching && unread.data.count > 0)} 76 + // Check for `isRefetching` here because our automatic refresh could be 77 + // cancelled due to this handler being called after resetting the data 78 + onEndReached={() => !feed.isRefetching && feed.fetchNextPage()} 77 79 onRefresh={refetch} 78 80 extraBottomGutter 79 81 />
+121
src/views/oauth-callback.tsx
··· 1 + import { createResource, Match, Switch } from 'solid-js'; 2 + 3 + import { OAuthServerAgent } from '~/lib/bsky-oauth/agents/server-agent'; 4 + import { sessions } from '~/lib/bsky-oauth/agents/session'; 5 + import { database } from '~/lib/bsky-oauth/globals'; 6 + import { getMetadataFromAuthorizationServer } from '~/lib/bsky-oauth/resolver'; 7 + 8 + import * as preferences from '~/globals/preferences'; 9 + 10 + import Button from '~/components/button'; 11 + import CircularProgress from '~/components/circular-progress'; 12 + import { OAuthUserAgent } from '~/lib/bsky-oauth/agents/user-agent'; 13 + 14 + class AuthorizationError extends Error { 15 + name = 'AuthorizationError'; 16 + } 17 + 18 + const OAuthCallbackPage = () => { 19 + const [resource] = createResource(async () => { 20 + const searchParams = new URLSearchParams(location.hash.slice(1)); 21 + 22 + // @todo: Store the path that the user was previously in 23 + 24 + // We've captured the search params, we don't want this to be replayed. 25 + // Do this on global history instance so it doesn't affect this page rendering. 26 + history.replaceState(null, '', '/'); 27 + 28 + const raw_issuer = searchParams.get('iss'); 29 + const state = searchParams.get('state'); 30 + const code = searchParams.get('code'); 31 + const error = searchParams.get('error'); 32 + 33 + if (!state || !(code || error)) { 34 + throw new Error(`missing parameters`); 35 + } 36 + 37 + const stored = await database.states.get(state); 38 + if (stored) { 39 + // Delete now that we've caught it 40 + await database.states.delete(state); 41 + } else { 42 + throw new Error(`unknown state`); 43 + } 44 + 45 + if (error) { 46 + throw new AuthorizationError(searchParams.get('error_description') || error); 47 + } 48 + if (!code) { 49 + throw new Error(`missing code parameter`); 50 + } 51 + 52 + // Retrieve server metadata 53 + const as_meta = await getMetadataFromAuthorizationServer(stored.issuer); 54 + const issuer = as_meta.issuer; 55 + 56 + if (raw_issuer !== null) { 57 + if (issuer !== raw_issuer) { 58 + throw new Error(`issuer mismatch`); 59 + } 60 + } else if (as_meta.authorization_response_iss_parameter_supported) { 61 + throw new Error(`expected server to provide iss parameter`); 62 + } 63 + 64 + // Retrieve authentication tokens 65 + const dpopKey = stored.dpopKey; 66 + 67 + const server = new OAuthServerAgent(as_meta, dpopKey); 68 + 69 + const tokenSet = await server.exchangeCode(code, stored.verifier); 70 + const sub = tokenSet.sub; 71 + 72 + // We're finished! 73 + await sessions.setStored(sub, { dpopKey, tokenSet }); 74 + 75 + // We make 4 requests right at the start of the app's launch, those requests 76 + // will fail immediately on bsky.social as they'd be missing a DPoP nonce, 77 + // so let's fire a random request right now. 78 + try { 79 + const session = new OAuthUserAgent(sub, dpopKey); 80 + await session.handle(`/xrpc/app.bsky.actor.getProfile?actor=${sub}`); 81 + } catch { 82 + // Don't worry about it failing. 83 + } 84 + 85 + { 86 + // Update UI preferences 87 + const ui = preferences.sessions; 88 + 89 + ui.active = sub; 90 + ui.accounts = [{ did: sub }, ...ui.accounts.filter((acc) => acc.did !== sub)]; 91 + 92 + // Reload, we've routed the user back to `/` earlier. 93 + location.reload(); 94 + } 95 + }); 96 + 97 + return ( 98 + <Switch> 99 + <Match when={resource.error}> 100 + <div class="flex grow flex-col items-center justify-center gap-6"> 101 + <div class="text-sm"> 102 + <p class="text-p-red-400">Authentication failed</p> 103 + <p class="text-contrast-muted">{'' + resource.error}</p> 104 + </div> 105 + <Button onClick={() => location.reload()} variant="primary" size="md"> 106 + Go home 107 + </Button> 108 + </div> 109 + </Match> 110 + 111 + <Match when> 112 + <div class="flex grow flex-col items-center justify-center gap-6"> 113 + <p class="text-sm text-contrast-muted">Processing your sign in information</p> 114 + <CircularProgress /> 115 + </div> 116 + </Match> 117 + </Switch> 118 + ); 119 + }; 120 + 121 + export default OAuthCallbackPage;
+12 -47
src/views/settings-account.tsx
··· 1 - import { safeUrlParse } from '~/api/utils/strings'; 2 - 3 - import { useSession } from '~/lib/states/session'; 4 - 5 1 import * as Boxed from '~/components/boxed'; 6 2 import * as Page from '~/components/page'; 7 3 8 4 const AccountSettingsPage = () => { 9 - const { currentAccount } = useSession(); 10 - 11 - const session = currentAccount!.data.session; 12 - const isLimited = currentAccount!.data.scope !== undefined; 13 - 14 5 return ( 15 6 <> 16 7 <Page.Header> ··· 26 17 <Boxed.GroupHeader>Account information</Boxed.GroupHeader> 27 18 28 19 <Boxed.List> 29 - <Boxed.ButtonItem label="Handle" blurb={'@' + session.handle} /> 30 - <Boxed.StaticItem 31 - label="Data server" 32 - description={formatDataServer(currentAccount!.data.service)} 33 - /> 20 + <Boxed.ButtonItem label="Handle" blurb={'@' + 'lol'} /> 21 + <Boxed.StaticItem label="Data server" description={'lol'} /> 34 22 </Boxed.List> 35 23 </Boxed.Group> 36 24 37 - {!isLimited && ( 38 - <Boxed.Group> 39 - <Boxed.GroupHeader>Account security</Boxed.GroupHeader> 25 + <Boxed.Group> 26 + <Boxed.GroupHeader>Account security</Boxed.GroupHeader> 40 27 41 - <Boxed.List> 42 - <Boxed.LinkItem to="/settings/app-passwords" label="Manage app passwords" /> 43 - <Boxed.ButtonItem label="Change password" /> 44 - <Boxed.StaticItem 45 - label="Email two-factor authentication" 46 - description={session.emailAuthFactor ? `Enabled` : `Disabled`} 47 - /> 48 - </Boxed.List> 49 - </Boxed.Group> 50 - )} 28 + <Boxed.List> 29 + <Boxed.LinkItem to="/settings/app-passwords" label="Manage app passwords" /> 30 + <Boxed.ButtonItem label="Change password" /> 31 + <Boxed.StaticItem label="Email two-factor authentication" description={'lol'} /> 32 + </Boxed.List> 33 + </Boxed.Group> 51 34 52 35 <Boxed.Group> 53 36 <Boxed.GroupHeader>Account management</Boxed.GroupHeader> ··· 55 38 <Boxed.List> 56 39 <Boxed.LinkItem to="/settings/export" label="Export my data" /> 57 40 58 - {!isLimited && ( 59 - <> 60 - <Boxed.ButtonItem label="Deactivate account" variant="danger" /> 61 - <Boxed.ButtonItem label="Delete account" variant="danger" /> 62 - </> 63 - )} 41 + <Boxed.ButtonItem label="Deactivate account" variant="danger" /> 42 + <Boxed.ButtonItem label="Delete account" variant="danger" /> 64 43 </Boxed.List> 65 44 </Boxed.Group> 66 45 </Boxed.Container> ··· 69 48 }; 70 49 71 50 export default AccountSettingsPage; 72 - 73 - const formatDataServer = (uri: string): string => { 74 - const url = safeUrlParse(uri); 75 - if (url === null) { 76 - return `N/A`; 77 - } 78 - 79 - const host = url.host; 80 - if (host.endsWith('.host.bsky.network')) { 81 - return 'bsky.social'; 82 - } 83 - 84 - return host; 85 - };
-11
src/views/settings-app-passwords.tsx
··· 1 1 import { For, Match, Show, Switch } from 'solid-js'; 2 2 3 - import { XRPCError } from '@atcute/client'; 4 3 import type { ComAtprotoServerListAppPasswords } from '@atcute/client/lexicons'; 5 4 import { createMutation, createQuery } from '@mary/solid-query'; 6 5 ··· 9 8 import { formatAbsDateTime } from '~/lib/intl/time'; 10 9 import { reconcile } from '~/lib/misc'; 11 10 import { useAgent } from '~/lib/states/agent'; 12 - import { useSession } from '~/lib/states/session'; 13 11 14 12 import * as Boxed from '~/components/boxed'; 15 13 import CircularProgressView from '~/components/circular-progress-view'; ··· 24 22 import AddAppPasswordPrompt from '~/components/settings/app-passwords/add-app-password-prompt'; 25 23 26 24 const AppPasswordsSettingsPage = () => { 27 - const { currentAccount } = useSession(); 28 25 const { rpc } = useAgent(); 29 26 30 - const isLimited = !currentAccount || currentAccount.data.scope !== undefined; 31 - 32 27 const passwords = createQuery(() => { 33 28 return { 34 29 queryKey: ['app-passwords'], 35 30 async queryFn() { 36 - // We know this is going to throw so throw early 37 - if (isLimited) { 38 - throw new XRPCError(400, { kind: 'InvalidToken', message: 'Bad token scope' }); 39 - } 40 - 41 31 const { data } = await rpc.get('com.atproto.server.listAppPasswords', {}); 42 32 43 33 return data.passwords; ··· 62 52 <IconButton 63 53 icon={AddOutlinedIcon} 64 54 title="Create new app password" 65 - disabled={isLimited} 66 55 onClick={() => { 67 56 openModal(() => <AddAppPasswordPrompt />); 68 57 }}
+11
src/vite-env.d.ts
··· 1 1 /// <reference types="vite/client" /> 2 2 /// <reference types="@atcute/bluesky/lexicons" /> 3 3 /// <reference types="@atcute/bluemoji/lexicons" /> 4 + 5 + interface ImportMetaEnv { 6 + readonly VITE_DEV_SERVER_PORT: string; 7 + readonly VITE_OAUTH_CLIENT_ID: string; 8 + readonly VITE_OAUTH_REDIRECT_URL: string; 9 + readonly VITE_OAUTH_SCOPE: string; 10 + } 11 + 12 + interface ImportMeta { 13 + readonly env: ImportMetaEnv; 14 + }
+39
vite.config.js
··· 3 3 import { defineConfig } from 'vite'; 4 4 import solid from 'vite-plugin-solid'; 5 5 6 + import metadata from './public/oauth/client-metadata.json' with { type: 'json' }; 7 + 8 + const SERVER_HOST = '127.0.0.1'; 9 + const SERVER_PORT = 52222; 10 + 6 11 export default defineConfig({ 7 12 build: { 8 13 target: 'esnext', ··· 21 26 '~': path.join(__dirname, './src'), 22 27 }, 23 28 }, 29 + server: { 30 + host: SERVER_HOST, 31 + port: SERVER_PORT, 32 + }, 33 + optimizeDeps: { 34 + esbuildOptions: { 35 + target: 'esnext', 36 + }, 37 + }, 24 38 plugins: [ 25 39 solid({ 26 40 babel: { ··· 42 56 return { code: transformed, map: null }; 43 57 }, 44 58 }, 59 + 60 + oauthMetadataPlugin(), 45 61 ], 46 62 }); 63 + 64 + /** 65 + * @returns {import('vite').Plugin} 66 + */ 67 + function oauthMetadataPlugin() { 68 + return { 69 + config(_conf, { command }) { 70 + if (command === 'build') { 71 + process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id; 72 + process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0]; 73 + } else { 74 + const redirectUri = `http://${SERVER_HOST}:${SERVER_PORT}/oauth/callback`; 75 + const clientId = `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}`; 76 + 77 + process.env.VITE_DEV_SERVER_PORT = '' + SERVER_PORT; 78 + process.env.VITE_OAUTH_CLIENT_ID = clientId; 79 + process.env.VITE_OAUTH_REDIRECT_URL = redirectUri; 80 + } 81 + 82 + process.env.VITE_OAUTH_SCOPE = metadata.scope; 83 + }, 84 + }; 85 + }