feat: create atproto oauth login

This commit is contained in:
Julien Calixte
2026-03-10 12:27:35 +01:00
parent 908641e54b
commit 8843d67a80
16 changed files with 485 additions and 7 deletions

View File

@@ -15,6 +15,7 @@
"generate-pwa-assets": "pwa-assets-generator" "generate-pwa-assets": "pwa-assets-generator"
}, },
"dependencies": { "dependencies": {
"@atproto/oauth-client-browser": "^0.3.41",
"@better-fetch/fetch": "^1.1.21", "@better-fetch/fetch": "^1.1.21",
"@better-fetch/logger": "^1.1.21", "@better-fetch/logger": "^1.1.21",
"@intlify/unplugin-vue-i18n": "^6.0.8", "@intlify/unplugin-vue-i18n": "^6.0.8",

234
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@atproto/oauth-client-browser':
specifier: ^0.3.41
version: 0.3.41
'@better-fetch/fetch': '@better-fetch/fetch':
specifier: ^1.1.21 specifier: ^1.1.21
version: 1.1.21 version: 1.1.21
@@ -254,6 +257,66 @@ packages:
'@ark/util@0.56.0': '@ark/util@0.56.0':
resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==}
'@atproto-labs/did-resolver@0.2.6':
resolution: {integrity: sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg==}
'@atproto-labs/fetch@0.2.3':
resolution: {integrity: sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==}
'@atproto-labs/handle-resolver@0.3.6':
resolution: {integrity: sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA==}
'@atproto-labs/identity-resolver@0.3.6':
resolution: {integrity: sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg==}
'@atproto-labs/pipe@0.1.1':
resolution: {integrity: sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==}
'@atproto-labs/simple-store-memory@0.1.4':
resolution: {integrity: sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==}
'@atproto-labs/simple-store@0.3.0':
resolution: {integrity: sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==}
'@atproto/common-web@0.4.18':
resolution: {integrity: sha512-ilImzP+9N/mtse440kN60pGrEzG7wi4xsV13nGeLrS+Zocybc/ISOpKlbZM13o+twPJ+Q7veGLw9CtGg0GAFoQ==}
'@atproto/did@0.3.0':
resolution: {integrity: sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA==}
'@atproto/jwk-jose@0.1.11':
resolution: {integrity: sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==}
'@atproto/jwk-webcrypto@0.2.0':
resolution: {integrity: sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==}
'@atproto/jwk@0.6.0':
resolution: {integrity: sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==}
'@atproto/lex-data@0.0.13':
resolution: {integrity: sha512-7Z7RwZ1Y/JzBF/Tcn/I4UJ/vIGfh5zn1zjv0KX+flke2JtgFkSE8uh2hOtqgBQMNqE3zdJFM+dcSWln86hR3MQ==}
'@atproto/lex-json@0.0.13':
resolution: {integrity: sha512-hwLhkKaIHulGJpt0EfXAEWdrxqM2L1tV/tvilzhMp3QxPqYgXchFnrfVmLsyFDx6P6qkH1GsX/XC2V36U0UlPQ==}
'@atproto/lexicon@0.6.2':
resolution: {integrity: sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==}
'@atproto/oauth-client-browser@0.3.41':
resolution: {integrity: sha512-4QTm8zPgm08vl53flrVmL+MS5IOhvWWctNZmEnPbvQ2t1ISw9Q5m815m2Sszi5ULMFjOqvT7lhKB7zQUn5gq5g==}
'@atproto/oauth-client@0.6.0':
resolution: {integrity: sha512-F7ZTKzFptXgyihMkd7QTdRSkrh4XqrS+qTw+V81k5Q6Bh3MB1L3ypvfSJ6v7SSUJa6XxoZYJTCahHC1e+ndE6Q==}
'@atproto/oauth-types@0.6.3':
resolution: {integrity: sha512-jdKuoPknJuh/WjI+mYk7agSbx9mNVMbS6Dr3k1z2YMY2oRiCQjxYBuo4MLKATbxj05nMQaZRWlHRUazoAu5Cng==}
'@atproto/syntax@0.5.0':
resolution: {integrity: sha512-UA2DSpGdOQzUQ4gi5SH+NEJz/YR3a3Fg3y2oh+xETDSiTRmA4VhHRCojhXAVsBxUT6EnItw190C/KN+DWW90kw==}
'@atproto/xrpc@0.7.7':
resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==}
'@babel/code-frame@7.27.1': '@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -2653,6 +2716,9 @@ packages:
resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==}
deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
core-js@3.48.0:
resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==}
core-util-is@1.0.2: core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@@ -4000,6 +4066,9 @@ packages:
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
iso-datestring-validator@2.2.2:
resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==}
isobject@2.1.0: isobject@2.1.0:
resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -4119,6 +4188,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
jose@5.10.0:
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
js-tokens@3.0.2: js-tokens@3.0.2:
resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==}
@@ -4380,6 +4452,9 @@ packages:
loupe@3.2.0: loupe@3.2.0:
resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@4.1.5: lru-cache@4.1.5:
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
@@ -4530,6 +4605,9 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
multiformats@9.9.0:
resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}
nan@2.25.0: nan@2.25.0:
resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==} resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==}
@@ -5612,6 +5690,9 @@ packages:
tslib@2.6.1: tslib@2.6.1:
resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.20.3: tsx@4.20.3:
resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -5678,6 +5759,9 @@ packages:
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
hasBin: true hasBin: true
uint8arrays@3.0.0:
resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==}
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -5710,6 +5794,9 @@ packages:
resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
unicode-segmenter@0.14.5:
resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==}
union-value@1.0.1: union-value@1.0.1:
resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -6104,6 +6191,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
snapshots: snapshots:
'@aashutoshrathi/word-wrap@1.2.6': {} '@aashutoshrathi/word-wrap@1.2.6': {}
@@ -6130,6 +6220,130 @@ snapshots:
'@ark/util@0.56.0': {} '@ark/util@0.56.0': {}
'@atproto-labs/did-resolver@0.2.6':
dependencies:
'@atproto-labs/fetch': 0.2.3
'@atproto-labs/pipe': 0.1.1
'@atproto-labs/simple-store': 0.3.0
'@atproto-labs/simple-store-memory': 0.1.4
'@atproto/did': 0.3.0
zod: 3.25.76
'@atproto-labs/fetch@0.2.3':
dependencies:
'@atproto-labs/pipe': 0.1.1
'@atproto-labs/handle-resolver@0.3.6':
dependencies:
'@atproto-labs/simple-store': 0.3.0
'@atproto-labs/simple-store-memory': 0.1.4
'@atproto/did': 0.3.0
zod: 3.25.76
'@atproto-labs/identity-resolver@0.3.6':
dependencies:
'@atproto-labs/did-resolver': 0.2.6
'@atproto-labs/handle-resolver': 0.3.6
'@atproto-labs/pipe@0.1.1': {}
'@atproto-labs/simple-store-memory@0.1.4':
dependencies:
'@atproto-labs/simple-store': 0.3.0
lru-cache: 10.4.3
'@atproto-labs/simple-store@0.3.0': {}
'@atproto/common-web@0.4.18':
dependencies:
'@atproto/lex-data': 0.0.13
'@atproto/lex-json': 0.0.13
'@atproto/syntax': 0.5.0
zod: 3.25.76
'@atproto/did@0.3.0':
dependencies:
zod: 3.25.76
'@atproto/jwk-jose@0.1.11':
dependencies:
'@atproto/jwk': 0.6.0
jose: 5.10.0
'@atproto/jwk-webcrypto@0.2.0':
dependencies:
'@atproto/jwk': 0.6.0
'@atproto/jwk-jose': 0.1.11
zod: 3.25.76
'@atproto/jwk@0.6.0':
dependencies:
multiformats: 9.9.0
zod: 3.25.76
'@atproto/lex-data@0.0.13':
dependencies:
multiformats: 9.9.0
tslib: 2.8.1
uint8arrays: 3.0.0
unicode-segmenter: 0.14.5
'@atproto/lex-json@0.0.13':
dependencies:
'@atproto/lex-data': 0.0.13
tslib: 2.8.1
'@atproto/lexicon@0.6.2':
dependencies:
'@atproto/common-web': 0.4.18
'@atproto/syntax': 0.5.0
iso-datestring-validator: 2.2.2
multiformats: 9.9.0
zod: 3.25.76
'@atproto/oauth-client-browser@0.3.41':
dependencies:
'@atproto-labs/did-resolver': 0.2.6
'@atproto-labs/handle-resolver': 0.3.6
'@atproto-labs/simple-store': 0.3.0
'@atproto/did': 0.3.0
'@atproto/jwk': 0.6.0
'@atproto/jwk-webcrypto': 0.2.0
'@atproto/oauth-client': 0.6.0
'@atproto/oauth-types': 0.6.3
core-js: 3.48.0
'@atproto/oauth-client@0.6.0':
dependencies:
'@atproto-labs/did-resolver': 0.2.6
'@atproto-labs/fetch': 0.2.3
'@atproto-labs/handle-resolver': 0.3.6
'@atproto-labs/identity-resolver': 0.3.6
'@atproto-labs/simple-store': 0.3.0
'@atproto-labs/simple-store-memory': 0.1.4
'@atproto/did': 0.3.0
'@atproto/jwk': 0.6.0
'@atproto/oauth-types': 0.6.3
'@atproto/xrpc': 0.7.7
core-js: 3.48.0
multiformats: 9.9.0
zod: 3.25.76
'@atproto/oauth-types@0.6.3':
dependencies:
'@atproto/did': 0.3.0
'@atproto/jwk': 0.6.0
zod: 3.25.76
'@atproto/syntax@0.5.0':
dependencies:
tslib: 2.8.1
'@atproto/xrpc@0.7.7':
dependencies:
'@atproto/lexicon': 0.6.2
zod: 3.25.76
'@babel/code-frame@7.27.1': '@babel/code-frame@7.27.1':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.27.1 '@babel/helper-validator-identifier': 7.27.1
@@ -8809,6 +9023,8 @@ snapshots:
core-js@2.6.12: {} core-js@2.6.12: {}
core-js@3.48.0: {}
core-util-is@1.0.2: {} core-util-is@1.0.2: {}
cose-base@1.0.3: cose-base@1.0.3:
@@ -10243,6 +10459,8 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
iso-datestring-validator@2.2.2: {}
isobject@2.1.0: isobject@2.1.0:
dependencies: dependencies:
isarray: 1.0.0 isarray: 1.0.0
@@ -10518,6 +10736,8 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
jose@5.10.0: {}
js-tokens@3.0.2: {} js-tokens@3.0.2: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -10762,6 +10982,8 @@ snapshots:
loupe@3.2.0: {} loupe@3.2.0: {}
lru-cache@10.4.3: {}
lru-cache@4.1.5: lru-cache@4.1.5:
dependencies: dependencies:
pseudomap: 1.0.2 pseudomap: 1.0.2
@@ -10955,6 +11177,8 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
multiformats@9.9.0: {}
nan@2.25.0: nan@2.25.0:
optional: true optional: true
@@ -12126,6 +12350,8 @@ snapshots:
tslib@2.6.1: {} tslib@2.6.1: {}
tslib@2.8.1: {}
tsx@4.20.3: tsx@4.20.3:
dependencies: dependencies:
esbuild: 0.25.5 esbuild: 0.25.5
@@ -12204,6 +12430,10 @@ snapshots:
uglify-js@3.19.3: uglify-js@3.19.3:
optional: true optional: true
uint8arrays@3.0.0:
dependencies:
multiformats: 9.9.0
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@@ -12239,6 +12469,8 @@ snapshots:
unicode-property-aliases-ecmascript@2.2.0: {} unicode-property-aliases-ecmascript@2.2.0: {}
unicode-segmenter@0.14.5: {}
union-value@1.0.1: union-value@1.0.1:
dependencies: dependencies:
arr-union: 3.1.0 arr-union: 3.1.0
@@ -12727,3 +12959,5 @@ snapshots:
yargs-parser: 7.0.0 yargs-parser: 7.0.0
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod@3.25.76: {}

View File

@@ -0,0 +1,15 @@
{
"client_id": "https://remanso.space/client-metadata.json",
"client_name": "Remanso",
"client_uri": "https://remanso.space",
"redirect_uris": [
"https://remanso.space/",
"http://localhost:5173/"
],
"scope": "atproto transition:generic",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"application_type": "web",
"dpop_bound_access_tokens": true,
"token_endpoint_auth_method": "none"
}

View File

@@ -1,13 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import NewVersion from '@/components/NewVersion.vue' import NewVersion from '@/components/NewVersion.vue'
import { useATProtoLogin } from '@/hooks/useATProtoLogin.hook'
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
const { isReady } = useGitHubLogin() const { isReady } = useGitHubLogin()
const { isATProtoReady } = useATProtoLogin()
</script> </script>
<template> <template>
<div id="main-app" class="prose"> <div id="main-app" class="prose">
<router-view v-if="isReady" /> <router-view v-if="isReady && isATProtoReady" />
<new-version /> <new-version />
</div> </div>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useATProtoLogin } from '@/hooks/useATProtoLogin.hook'
const { handle, isLoggedIn, signIn, signOut } = useATProtoLogin()
const inputHandle = ref('')
const onSignIn = () => {
if (inputHandle.value) {
signIn(inputHandle.value)
}
}
</script>
<template>
<div v-if="isLoggedIn" class="sign-in-atproto">
<span>{{ handle }}</span>
<button class="btn btn-sm" @click="signOut">Sign out</button>
</div>
<div v-else class="sign-in-atproto">
<input
v-model="inputHandle"
class="input input-sm"
type="text"
placeholder="yourhandle.bsky.social"
@keyup.enter="onSignIn"
/>
<button class="btn btn-sm" @click="onSignIn">Sign in with Bluesky</button>
</div>
</template>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import RepoList from "@/components/RepoList.vue" import RepoList from "@/components/RepoList.vue"
import SignInAtproto from "@/components/SignInAtproto.vue"
import SignInGithub from "@/components/SignInGithub.vue" import SignInGithub from "@/components/SignInGithub.vue"
import ThemeSwap from "@/components/ThemeSwap.vue" import ThemeSwap from "@/components/ThemeSwap.vue"
import { useForm } from "@/hooks/useForm.hook" import { useForm } from "@/hooks/useForm.hook"
@@ -23,6 +24,7 @@ const { userInput, repoInput, submit } = useForm()
<div class="get-started"> <div class="get-started">
<sign-in-github /> <sign-in-github />
<sign-in-atproto />
<router-link <router-link
:to="{ :to="{
name: 'FluxNoteView', name: 'FluxNoteView',

View File

@@ -6,5 +6,6 @@ export enum DataType {
BacklinkNote = 'BacklinkNote', BacklinkNote = 'BacklinkNote',
RepetitionCard = 'RepetitionCard', RepetitionCard = 'RepetitionCard',
History = 'History', History = 'History',
UserSettings = 'UserSettings' UserSettings = 'UserSettings',
AtprotoSession = 'AtprotoSession'
} }

View File

@@ -0,0 +1,7 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
export interface AtprotoSession extends Model<DataType.AtprotoSession> {
did: string
handle: string
}

View File

@@ -0,0 +1,59 @@
import { computed, ref } from 'vue'
import { getAuthor } from '@/modules/atproto/getAuthor'
import { restoreSession, sdkSignOut, signInWithHandle } from '@/modules/atproto/service/atprotoOAuth'
import { clearSession, loadSession, saveSession } from '@/modules/atproto/service/atprotoSession'
const did = ref<string | null>(null)
const handle = ref<string | null>(null)
let init = true
const initializeAuth = async () => {
const session = await restoreSession()
if (session) {
const author = await getAuthor(session.did)
const resolvedHandle = author?.handle ?? ''
did.value = session.did
handle.value = resolvedHandle
await saveSession(session.did, resolvedHandle)
} else {
const stored = await loadSession()
did.value = stored?.did ?? ''
handle.value = stored?.handle ?? ''
}
}
export const useATProtoLogin = () => {
if (init) {
init = false
initializeAuth()
}
const isLoggedIn = computed(() => !!did.value)
const isATProtoReady = computed(() => did.value !== null)
const signIn = async (inputHandle: string): Promise<void> => {
await signInWithHandle(inputHandle)
}
const signOut = async (): Promise<void> => {
if (did.value) {
await sdkSignOut(did.value)
}
await clearSession()
did.value = ''
handle.value = ''
}
return {
did,
handle,
isLoggedIn,
isATProtoReady,
signIn,
signOut,
}
}

View File

@@ -0,0 +1,21 @@
import { Ref, ref, watch } from 'vue'
import { getFollows } from '@/modules/atproto/service/getFollows'
export const useFollows = (did: Ref<string | null>) => {
const follows = ref<Set<string>>(new Set())
watch(
did,
async (value) => {
if (value) {
follows.value = await getFollows(value)
} else {
follows.value = new Set()
}
},
{ immediate: true },
)
return { follows }
}

View File

@@ -3,7 +3,12 @@ import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core" import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref } from "vue" import { computed, ref, Ref } from "vue"
export function usePublicNoteList(did?: Ref<string | undefined>) { interface UsePublicNoteListOptions {
did?: Ref<string | undefined>
followsFilter?: Ref<Set<string>>
}
export function usePublicNoteList(options?: UsePublicNoteListOptions) {
const isLoading = ref(false) const isLoading = ref(false)
const notes = ref<PublicNoteListItem[]>([]) const notes = ref<PublicNoteListItem[]>([])
const cursor = ref<string | null | undefined>(null) const cursor = ref<string | null | undefined>(null)
@@ -12,7 +17,7 @@ export function usePublicNoteList(did?: Ref<string | undefined>) {
const onLoadMore = async () => { const onLoadMore = async () => {
isLoading.value = true isLoading.value = true
const path = did?.value ? `/${did.value}/notes` : "/notes" const path = options?.did?.value ? `/${options.did.value}/notes` : "/notes"
const noteAPI = new URL(path, "https://api.litenote.li212.fr") const noteAPI = new URL(path, "https://api.litenote.li212.fr")
if (cursor.value) { if (cursor.value) {
@@ -39,8 +44,14 @@ export function usePublicNoteList(did?: Ref<string | undefined>) {
const getAuthor = (did: string) => const getAuthor = (did: string) =>
authors.value.has(did) ? authors.value.get(did)?.handle : "" authors.value.has(did) ? authors.value.get(did)?.handle : ""
const filteredNotes = computed(() => {
const filter = options?.followsFilter?.value
if (!filter || filter.size === 0) return notes.value
return notes.value.filter((n) => filter.has(n.did))
})
return { return {
notes, notes: filteredNotes,
isLoading, isLoading,
canLoadMore, canLoadMore,
onLoadMore, onLoadMore,

View File

@@ -0,0 +1,30 @@
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
const CLIENT_ID = import.meta.env.DEV
? 'http://localhost'
: 'https://remanso.space/client-metadata.json'
let clientPromise: Promise<BrowserOAuthClient> | null = null
export const getOAuthClient = (): Promise<BrowserOAuthClient> => {
if (!clientPromise) {
clientPromise = BrowserOAuthClient.load({ clientId: CLIENT_ID })
}
return clientPromise
}
export const signInWithHandle = async (handle: string): Promise<void> => {
const client = await getOAuthClient()
await client.signInRedirect(handle, { scope: 'atproto transition:generic' })
}
export const restoreSession = async () => {
const client = await getOAuthClient()
const result = await client.init()
return result?.session ?? null
}
export const sdkSignOut = async (sub: string): Promise<void> => {
const client = await getOAuthClient()
await client.revoke(sub)
}

View File

@@ -0,0 +1,23 @@
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { AtprotoSession } from '@/data/models/AtprotoSession'
const SESSION_ID = `${DataType.AtprotoSession}-current`
export const loadSession = (): Promise<AtprotoSession | null> => {
return data.get<DataType.AtprotoSession, AtprotoSession>(SESSION_ID)
}
export const saveSession = async (did: string, handle: string): Promise<void> => {
const session: AtprotoSession = {
_id: SESSION_ID,
$type: DataType.AtprotoSession,
did,
handle,
}
await data.update<DataType.AtprotoSession, AtprotoSession>(session)
}
export const clearSession = (): Promise<boolean> => {
return data.remove(SESSION_ID)
}

View File

@@ -0,0 +1,24 @@
export const getFollows = async (did: string): Promise<Set<string>> => {
const follows = new Set<string>()
let cursor: string | undefined
do {
const url = new URL('https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows')
url.searchParams.set('actor', did)
url.searchParams.set('limit', '100')
if (cursor) {
url.searchParams.set('cursor', cursor)
}
const response = await fetch(url)
const result: { follows: { did: string }[]; cursor?: string } = await response.json()
for (const follow of result.follows) {
follows.add(follow.did)
}
cursor = result.cursor
} while (cursor)
return follows
}

View File

@@ -9,7 +9,7 @@ import { computed } from "vue"
const props = defineProps<{ did: string }>() const props = defineProps<{ did: string }>()
const did = computed(() => props.did) const did = computed(() => props.did)
const { notes, isLoading, canLoadMore, onLoadMore } = usePublicNoteList(did) const { notes, isLoading, canLoadMore, onLoadMore } = usePublicNoteList({ did })
const author = computedAsync(async () => getAuthor(did.value)) const author = computedAsync(async () => getAuthor(did.value))
</script> </script>

View File

@@ -1,10 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import BackButton from "@/components/BackButton.vue" import BackButton from "@/components/BackButton.vue"
import PublicNoteList from "@/components/PublicNoteList.vue" import PublicNoteList from "@/components/PublicNoteList.vue"
import SignInAtproto from "@/components/SignInAtproto.vue"
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
import { useFollows } from "@/hooks/useFollows.hook"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook" import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
const { did, isLoggedIn } = useATProtoLogin()
const { follows } = useFollows(did)
const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } = const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
usePublicNoteList() usePublicNoteList({ followsFilter: follows })
</script> </script>
<template> <template>
@@ -12,6 +17,10 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
<div class="header"> <div class="header">
<back-button class="back-button" :fallback="{ name: 'Home' }" /> <back-button class="back-button" :fallback="{ name: 'Home' }" />
<h1>Remanso notes</h1> <h1>Remanso notes</h1>
<sign-in-atproto />
</div>
<div v-if="isLoggedIn && follows.size > 0" class="follows-badge">
Showing follows only
</div> </div>
<div v-if="isLoading"></div> <div v-if="isLoading"></div>
<div v-else> <div v-else>
@@ -74,4 +83,11 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
overflow-y: auto; overflow-y: auto;
} }
} }
.follows-badge {
font-size: 0.8rem;
opacity: 0.7;
text-align: center;
margin-bottom: 0.5rem;
}
</style> </style>