diff --git a/modules/oauth.ts b/modules/oauth.ts index 51f08a7837..7bf26d0ebd 100644 --- a/modules/oauth.ts +++ b/modules/oauth.ts @@ -12,8 +12,11 @@ export default defineNuxtModule({ async setup() { const nuxt = useNuxt() - const { previewUrl, productionUrl } = await getEnv(nuxt.options.dev) - const clientUri = productionUrl || previewUrl || 'http://127.0.0.1:3000' + const { previewUrl, productionUrl, env } = await getEnv(nuxt.options.dev) + const clientUri = + productionUrl || + (env === 'canary' ? 'https://main.npmx.dev' : previewUrl) || + 'http://127.0.0.1:3000' // bake it into a virtual file addServerTemplate({ diff --git a/nuxt.config.ts b/nuxt.config.ts index 43b55b19cf..b6a8725e96 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -41,7 +41,7 @@ export default defineNuxtConfig({ github: { orgToken: '', }, - oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined, + oauthJwkOne: '', // Upstash Redis for distributed OAuth token refresh locking in production upstash: { redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '', diff --git a/package.json b/package.json index 4605f274c7..a922fc4476 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,15 @@ "chromatic": "chromatic" }, "dependencies": { - "@atcute/bluesky-richtext-segmenter": "3.0.1", - "@atcute/tid": "1.1.3", + "@atcute/bluesky-richtext-segmenter": "3.0.2", + "@atcute/identity-resolver": "2.0.1", + "@atcute/identity-resolver-node": "2.0.1", + "@atcute/lexicons": "2.0.3", + "@atcute/oauth-node-client": "2.0.1", + "@atcute/tid": "1.1.4", "@atproto/api": "0.20.25", "@atproto/lex": "0.1.7", "@atproto/lex-password-session": "0.1.4", - "@atproto/oauth-client-node": "0.3.15", "@deno/doc": "jsr:^0.189.1", "@floating-ui/vue": "2.0.0", "@iconify-json/lucide": "1.2.116", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 254977758f..8e51565c95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,11 +64,23 @@ importers: .: dependencies: '@atcute/bluesky-richtext-segmenter': - specifier: 3.0.1 - version: 3.0.1 + specifier: 3.0.2 + version: 3.0.2 + '@atcute/identity-resolver': + specifier: 2.0.1 + version: 2.0.1(@atcute/identity@2.0.2)(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/identity-resolver-node': + specifier: 2.0.1 + version: 2.0.1(@atcute/identity-resolver@2.0.1)(@atcute/identity@2.0.2)(@atcute/lexicons@2.0.3) + '@atcute/lexicons': + specifier: 2.0.3 + version: 2.0.3 + '@atcute/oauth-node-client': + specifier: 2.0.1 + version: 2.0.1(@atcute/identity-resolver@2.0.1)(@atcute/lexicons@2.0.3)(typescript@6.0.3) '@atcute/tid': - specifier: 1.1.3 - version: 1.1.3 + specifier: 1.1.4 + version: 1.1.4 '@atproto/api': specifier: 0.20.25 version: 0.20.25 @@ -78,9 +90,6 @@ importers: '@atproto/lex-password-session': specifier: 0.1.4 version: 0.1.4 - '@atproto/oauth-client-node': - specifier: 0.3.15 - version: 0.3.15 '@deno/doc': specifier: jsr:^0.189.1 version: '@jsr/deno__doc@0.189.1(patch_hash=24f326e123c822a07976329a5afe91a8713e82d53134b5586625b72431c87832)' @@ -549,60 +558,84 @@ packages: peerDependencies: '@types/json-schema': ^7.0.15 - '@atcute/bluesky-richtext-segmenter@3.0.1': - resolution: {integrity: sha512-5TrzZTpPkVchiNG+YAW8MmBVnN/gLPN/V3nJgpQcnLrQOebqTV+lR+yZ0d4T70NsBD32vJHyxoX5PY+6hY2KlA==} + '@atcute/bluesky-richtext-segmenter@3.0.2': + resolution: {integrity: sha512-LXsUuElLQ/gQe2g04SzgTeEbSwhcnmswioGdneve8e3vE+FU353EjuEVIsnA/H5rnGeLswLdrua7Io1twezynA==} + + '@atcute/client@5.1.1': + resolution: {integrity: sha512-cn5/Zi/qo37WtQG6gzIC7JPs0RDzX9Z4eaceX45SpKgLZoc3fCFDJcE7C8xsbxBNfjry2T6PmUxWA8obebZsEQ==} + peerDependencies: + '@atcute/lexicons': ^2.0.0 + + '@atcute/identity-resolver-node@2.0.1': + resolution: {integrity: sha512-UtcmhY4Oxk9tk1elTHmilcIXA1EFtGQae7mp3l8Xqs6EZsZyH3cUDf/2jS1CoRT1GE/i3WfnLaxYmdMIVau1kg==} + peerDependencies: + '@atcute/identity': ^2.0.0 + '@atcute/identity-resolver': ^2.0.0 + '@atcute/lexicons': ^2.0.0 + + '@atcute/identity-resolver@2.0.1': + resolution: {integrity: sha512-0enA9w7XnbbqsZ5Rcl6jXLf7ZZuwFQ9dBmxFq3qOxPHLaCETsqsrQflXDPqiM27TnZwYq8sqCV5D1mFOksggDQ==} + peerDependencies: + '@atcute/identity': ^2.0.0 + '@atcute/lexicons': ^2.0.0 + + '@atcute/identity@2.0.2': + resolution: {integrity: sha512-amr/EQceqVtBVmjBK4uUF7nKKYuRttadigpvOcAn4dnO6SNSwSjQi8KDH9LnukEotPsSP4UDsQvNfHWEoUcslw==} + peerDependencies: + '@atcute/lexicons': ^2.0.0 + + '@atcute/lexicons@2.0.3': + resolution: {integrity: sha512-rs3WqZIQ7lhSYXEvRU/x9rBMY2/fzvrsTgTf2/bgt+MgIQqOKAMySK3TWqfrvesGHDLxFlunHE3uMXeNdEC90Q==} + + '@atcute/multibase@1.2.5': + resolution: {integrity: sha512-cReTONgYpQo/VHD3ZmzPNoyBKJgSk1J4h//cvvdVVJBMar+SjlQ/sUXeTjQfuyfmDKv+TLKhmutLcvbMcQ9Rvw==} + + '@atcute/oauth-crypto@1.0.1': + resolution: {integrity: sha512-ghC8ceFx0r6pi5X3dNoET1K6PNg8afeAOZLD4PKLhrpVYdRtRrgY4uqAe3ojtSIjoS6GXmVm063CoJ2xtg/MoQ==} + + '@atcute/oauth-keyset@0.1.2': + resolution: {integrity: sha512-2CUazWBWRjSEoYZDaLS4zEPgCOcr9LgnMhPr1pzr9I+DytRnoXmrdbRs4xBVg4iUdXtdzU48DsMaGi6bHXaFcg==} + + '@atcute/oauth-node-client@2.0.1': + resolution: {integrity: sha512-MgqN9Rnt5nuUH5zriETtbQrwcx/tWaELKaFVQtqggMtRI4k4IP5+ZbnG40tiJjbZfSclapA2kwOnT14kO0nAxg==} + peerDependencies: + '@atcute/identity-resolver': ^2.0.0 + '@atcute/lexicons': ^2.0.0 + + '@atcute/oauth-types@1.0.1': + resolution: {integrity: sha512-9uFP/nK0PgPDsaNbLArzrCHvTZO1k04cSEX5jZ75W2Jh7Jxbh/CSTenSrad3CZXuX6y6hDquEtl6H/IqLtTmHw==} - '@atcute/tid@1.1.3': - resolution: {integrity: sha512-Ox9KRz8i1QC2ILNKP4JY7q2s1Vnf0xIRiS2CL5RVhQ+k248462mOUdZJkPfPnlprALv6sATDIJMbWioGkXoQRA==} + '@atcute/tid@1.1.4': + resolution: {integrity: sha512-RVjGAxu+WgcQjs7AkicZKOqLOncnuXh7a0xUHTfXTYZjXSszB7jBLgrcLWjpU0mBTko9dT+GajsXAcBGYmzYKQ==} '@atcute/time-ms@1.3.3': resolution: {integrity: sha512-FeK2Xsv4ymP+PoTWtmMFuJmpxDBQsj+jNMnd/YpIm3etcIPj1YFb7DeTAG1c/Xvp8AM38MLdqUCc35/mANnBtw==} - '@atproto-labs/did-resolver@0.2.5': - resolution: {integrity: sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==} + '@atcute/uint8array@1.1.5': + resolution: {integrity: sha512-1SFCXOtjE3ismP92CqzbOfYSwJmPPPFbj798wLXSpLv2hSd5OIaUrTh6EqRVR1VZ3ZhR+vaBmd3kxAN0wkw+gQ==} + + '@atcute/util-fetch@2.0.2': + resolution: {integrity: sha512-I0oenHlJjwRpWPyAJojox5H0z9NthhdMFaEjtkxyjo85VPPxk1vMJk/1ZjMJsrs1SgHhB3spazEWzV5DW8w70A==} + + '@atcute/util-text@1.3.4': + resolution: {integrity: sha512-u2UAM7iSM09sQaSG9jtxWFSPgB8boVj50/BoyMvYnhVgGBu+nXIuAcdDUQCsZA44YZgTPPhN/b86JX+jH7SPzQ==} '@atproto-labs/did-resolver@0.3.4': resolution: {integrity: sha512-nBECoVG59NfbYthayxfR0s1dLFVdN+RKMaL0p0uaNX/U1SAETksN6Yydmr4oWOKSkyyU3GO9XZeo1yzwHnRcZw==} engines: {node: '>=22'} - '@atproto-labs/fetch-node@0.2.0': - resolution: {integrity: sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q==} - engines: {node: '>=18.7.0'} - - '@atproto-labs/fetch@0.2.3': - resolution: {integrity: sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==} - '@atproto-labs/fetch@0.3.3': resolution: {integrity: sha512-2gABLf0VEI86Sxo6YhGKQYK1tGhTZ4y+3TrCWputnupEZxuS/hw6+pFw9ceXZ3v9nnMNthzsAEcaAQ3V/tXsqg==} engines: {node: '>=22'} - '@atproto-labs/handle-resolver-node@0.1.24': - resolution: {integrity: sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg==} - engines: {node: '>=18.7.0'} - - '@atproto-labs/handle-resolver@0.3.5': - resolution: {integrity: sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA==} - - '@atproto-labs/identity-resolver@0.3.5': - resolution: {integrity: sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ==} - - '@atproto-labs/pipe@0.1.1': - resolution: {integrity: sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==} - '@atproto-labs/pipe@0.2.3': resolution: {integrity: sha512-hsjkaKGdhEGKhXuOOfIeYyZSQZfw007AXQJak/QTd9pLY1wHUJG85a9+u13xoMeav95gHkBRzswti7+kqsxgdw==} engines: {node: '>=22'} - '@atproto-labs/simple-store-memory@0.1.4': - resolution: {integrity: sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==} - '@atproto-labs/simple-store-memory@0.2.3': resolution: {integrity: sha512-RtL48op8Db/QFNbW+9Y+Os6DOMqysOkoG5QelTZ0qcZt/j0pUBY8v2zSr5/faVJ5Y30h1cONxMydV48tVWf8pg==} engines: {node: '>=22'} - '@atproto-labs/simple-store@0.3.0': - resolution: {integrity: sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==} - '@atproto-labs/simple-store@0.4.3': resolution: {integrity: sha512-ML1HAEtQIixkcU4FCGbZtAkoHTfmXDi63tNVuSSPG/cVUKyrUURg6Cr1bcfvbbkMTwRj9abEwa3JZR7UUYi4Ew==} engines: {node: '>=22'} @@ -611,9 +644,6 @@ packages: resolution: {integrity: sha512-PTwt6X0U45C9vikr8NRi0Qzcep9VqJet52HtfcxgG9R7ofRHxqLVPzfVuN/f2xOaYM6H5IG/Nk1E9kXZcI0mSQ==} engines: {node: '>=22'} - '@atproto/common-web@0.4.21': - resolution: {integrity: sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==} - '@atproto/common-web@0.5.3': resolution: {integrity: sha512-FkMhOcNv1y7r5984+zXB+uMN4zJ4QFLEbZrYyQo6bNCf/Kfav/pEHXXENlaZEOweq2NYXf+tr2giMssBuM9u5A==} engines: {node: '>=22'} @@ -626,22 +656,10 @@ packages: resolution: {integrity: sha512-Ea5VwU+ofVXrm4BWpTa7gI9kz7A6NwPpCY6r0f5k8UJpNq0GDHSb1F5MdsFDVNnSK5bEI7DIMNZwspkNsAKOgA==} engines: {node: '>=22'} - '@atproto/did@0.2.4': - resolution: {integrity: sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==} - '@atproto/did@0.5.3': resolution: {integrity: sha512-nKcdu5qB9iNJFaBeQOQUJ+6mA1npFcZ3DmD0xB+etkMRd/3UvOQql/CvcMZaYzl+eGoVs1JDdEsvoZz+idfhaw==} engines: {node: '>=22'} - '@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-builder@0.1.5': resolution: {integrity: sha512-wh4nOGD0NuXBbRfrb4XBQZruuboRZs+rfhv2yWCR2zeEAWEqVU2+hVVNdQPDfa+rkghXENF31JIKftbz9NF6Ww==} engines: {node: '>=22'} @@ -654,9 +672,6 @@ packages: resolution: {integrity: sha512-b/LXpLt3HZZEfItruCcPpHUbzoo+LjoOp0GetlTuHX0UnJYqzk4zLg5qSMTDo4JoHU4hFO1+26R6cg986ZvUAg==} engines: {node: '>=22'} - '@atproto/lex-data@0.0.15': - resolution: {integrity: sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==} - '@atproto/lex-data@0.1.4': resolution: {integrity: sha512-f9U95sk0zUtxHktvK59peU+Shd0cIUYV68p//GS7sfdkAxUIeaspvX6CY+Quv9Oa4aozmsXI52SjaNn1wuU8RQ==} engines: {node: '>=22'} @@ -669,9 +684,6 @@ packages: resolution: {integrity: sha512-H6AhjPPYbH+6sJ9xNY6zrA1t2IVdze8idk6SgJqQnGESL+v3bblVsfYsx394DveEnQ7pO2sikKwkxZ9emBnBbQ==} engines: {node: '>=22'} - '@atproto/lex-json@0.0.16': - resolution: {integrity: sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg==} - '@atproto/lex-json@0.1.3': resolution: {integrity: sha512-Ch2w9bCLFOwWINFFxpZo6DBW7+ZxkehciU3P8N94gSyENLe3/5WWXHdHvkiH7EXZrpI7fKY8nVWn4GIL0ttqxw==} engines: {node: '>=22'} @@ -693,37 +705,18 @@ packages: engines: {node: '>=22'} hasBin: true - '@atproto/lexicon@0.6.2': - resolution: {integrity: sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==} - '@atproto/lexicon@0.7.4': resolution: {integrity: sha512-ulk4RGwMBp4vbkTOZcIwZJeTEPlj3PImOnx9TqxSxMDnBdySxarpd0Aprg7+iAvGyKTsMcrYHpeoLukZaquk0A==} engines: {node: '>=22'} - '@atproto/oauth-client-node@0.3.15': - resolution: {integrity: sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA==} - engines: {node: '>=18.7.0'} - - '@atproto/oauth-client@0.5.13': - resolution: {integrity: sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA==} - - '@atproto/oauth-types@0.6.1': - resolution: {integrity: sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA==} - '@atproto/repo@0.10.3': resolution: {integrity: sha512-Zk4xWtE/Mrf1+dAUwJLnWbeYwnvLMXF102mewzhPwsVkU63LsYO6nP9e5d4nqW/ClFDg9WKXo8zAv4792OnZNw==} engines: {node: '>=22'} - '@atproto/syntax@0.5.4': - resolution: {integrity: sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==} - '@atproto/syntax@0.6.4': resolution: {integrity: sha512-ELgpShRGMF65cvLXcoMCZFL0HBzy67yz/Nlhox3yOtTh120B09KjjvZYXhMysVog3nUJqv2EW5rf1VRYCeM/hw==} engines: {node: '>=22'} - '@atproto/xrpc@0.7.7': - resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} - '@atproto/xrpc@0.8.3': resolution: {integrity: sha512-0gUGN71+bSqV3PI5U4cQ4fdnw4nI0eD6czHDQ6jB7hMYBwNcJpwAyfA9W+C1G1wayIvP0SIap/M0H/pSKDWFIQ==} engines: {node: '>=22'} @@ -6798,6 +6791,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + esm-resolve@1.0.11: resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==} @@ -7755,9 +7751,6 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} @@ -8388,9 +8381,6 @@ packages: multiformats@13.4.2: resolution: {integrity: sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==} - multiformats@9.9.0: - resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} - mute-stream@3.0.0: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -8400,6 +8390,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.16: + resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} + engines: {node: ^18 || >=20} + hasBin: true + nanotar@0.3.0: resolution: {integrity: sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg==} @@ -10110,9 +10105,6 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - uint8arrays@3.0.0: - resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} - uint8arrays@5.1.1: resolution: {integrity: sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==} @@ -10145,10 +10137,6 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@6.27.0: - resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} - engines: {node: '>=18.17'} - unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -10178,6 +10166,9 @@ packages: unicode-segmenter@0.14.5: resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} + unicode-segmenter@0.17.1: + resolution: {integrity: sha512-imwm2Aty0hfnY/3LqBWiyhuxxGso7qwhLXDp0BjOpmtYdjYgWJvT9JXthbu3MmEITwgjbYWaRJ0b2yRfgYnD5Q==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -10658,6 +10649,9 @@ packages: vue-component-type-helpers@3.3.7: resolution: {integrity: sha512-Skkhw9agYSgsWqv7bxSOGJZa9SaiJbZVGdXuFWnrzKaQYHnw9qbjD630rw6RyMqDbp54nfLCLw5SZA55if7JLg==} + vue-component-type-helpers@3.3.8: + resolution: {integrity: sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==} + vue-data-ui@3.22.13: resolution: {integrity: sha512-NQeLKNUZQWw9DGQUEQPQo2kIcKV+Uap685DTUzydK6b+N4E6CByy/VepaNIScRbyUV1wFWe5aG3pnyyg32aN7g==} peerDependencies: @@ -11126,81 +11120,125 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.3.0 - '@atcute/bluesky-richtext-segmenter@3.0.1': {} + '@atcute/bluesky-richtext-segmenter@3.0.2': {} - '@atcute/tid@1.1.3': + '@atcute/client@5.1.1(@atcute/lexicons@2.0.3)(typescript@6.0.3)': dependencies: - '@atcute/time-ms': 1.3.3 + '@atcute/identity': 2.0.2(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/lexicons': 2.0.3 + transitivePeerDependencies: + - typescript - '@atcute/time-ms@1.3.3': {} + '@atcute/identity-resolver-node@2.0.1(@atcute/identity-resolver@2.0.1)(@atcute/identity@2.0.2)(@atcute/lexicons@2.0.3)': + dependencies: + '@atcute/identity': 2.0.2(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/identity-resolver': 2.0.1(@atcute/identity@2.0.2)(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/lexicons': 2.0.3 - '@atproto-labs/did-resolver@0.2.5': + '@atcute/identity-resolver@2.0.1(@atcute/identity@2.0.2)(@atcute/lexicons@2.0.3)(typescript@6.0.3)': 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.2.4 - zod: 3.25.76 + '@atcute/identity': 2.0.2(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/lexicons': 2.0.3 + '@atcute/util-fetch': 2.0.2(typescript@6.0.3) + valibot: 1.4.2(typescript@6.0.3) + transitivePeerDependencies: + - typescript - '@atproto-labs/did-resolver@0.3.4': + '@atcute/identity@2.0.2(@atcute/lexicons@2.0.3)(typescript@6.0.3)': dependencies: - '@atproto-labs/fetch': 0.3.3 - '@atproto-labs/pipe': 0.2.3 - '@atproto-labs/simple-store': 0.4.3 - '@atproto-labs/simple-store-memory': 0.2.3 - '@atproto/did': 0.5.3 - zod: 3.25.76 + '@atcute/lexicons': 2.0.3 + valibot: 1.4.2(typescript@6.0.3) + transitivePeerDependencies: + - typescript - '@atproto-labs/fetch-node@0.2.0': + '@atcute/lexicons@2.0.3': dependencies: - '@atproto-labs/fetch': 0.2.3 - '@atproto-labs/pipe': 0.1.1 - ipaddr.js: 2.4.0 - undici: 6.27.0 + '@atcute/uint8array': 1.1.5 + '@atcute/util-text': 1.3.4 + '@standard-schema/spec': 1.1.0 + esm-env: 1.2.2 - '@atproto-labs/fetch@0.2.3': + '@atcute/multibase@1.2.5': dependencies: - '@atproto-labs/pipe': 0.1.1 + '@atcute/uint8array': 1.1.5 - '@atproto-labs/fetch@0.3.3': + '@atcute/oauth-crypto@1.0.1(typescript@6.0.3)': dependencies: - '@atproto-labs/pipe': 0.2.3 + '@atcute/multibase': 1.2.5 + '@atcute/uint8array': 1.1.5 + nanoid: 5.1.16 + valibot: 1.4.2(typescript@6.0.3) + transitivePeerDependencies: + - typescript + + '@atcute/oauth-keyset@0.1.2(typescript@6.0.3)': + dependencies: + '@atcute/oauth-crypto': 1.0.1(typescript@6.0.3) + transitivePeerDependencies: + - typescript - '@atproto-labs/handle-resolver-node@0.1.24': + '@atcute/oauth-node-client@2.0.1(@atcute/identity-resolver@2.0.1)(@atcute/lexicons@2.0.3)(typescript@6.0.3)': + dependencies: + '@atcute/client': 5.1.1(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/identity': 2.0.2(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/identity-resolver': 2.0.1(@atcute/identity@2.0.2)(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/lexicons': 2.0.3 + '@atcute/oauth-crypto': 1.0.1(typescript@6.0.3) + '@atcute/oauth-keyset': 0.1.2(typescript@6.0.3) + '@atcute/oauth-types': 1.0.1(typescript@6.0.3) + '@atcute/util-fetch': 2.0.2(typescript@6.0.3) + nanoid: 5.1.16 + valibot: 1.4.2(typescript@6.0.3) + transitivePeerDependencies: + - typescript + + '@atcute/oauth-types@1.0.1(typescript@6.0.3)': dependencies: - '@atproto-labs/fetch-node': 0.2.0 - '@atproto-labs/handle-resolver': 0.3.5 - '@atproto/did': 0.2.4 + '@atcute/identity': 2.0.2(@atcute/lexicons@2.0.3)(typescript@6.0.3) + '@atcute/lexicons': 2.0.3 + '@atcute/oauth-keyset': 0.1.2(typescript@6.0.3) + valibot: 1.4.2(typescript@6.0.3) + transitivePeerDependencies: + - typescript - '@atproto-labs/handle-resolver@0.3.5': + '@atcute/tid@1.1.4': dependencies: - '@atproto-labs/simple-store': 0.3.0 - '@atproto-labs/simple-store-memory': 0.1.4 - '@atproto/did': 0.2.4 - zod: 3.25.76 + '@atcute/time-ms': 1.3.3 + + '@atcute/time-ms@1.3.3': {} + + '@atcute/uint8array@1.1.5': {} - '@atproto-labs/identity-resolver@0.3.5': + '@atcute/util-fetch@2.0.2(typescript@6.0.3)': dependencies: - '@atproto-labs/did-resolver': 0.2.5 - '@atproto-labs/handle-resolver': 0.3.5 + valibot: 1.4.2(typescript@6.0.3) + transitivePeerDependencies: + - typescript - '@atproto-labs/pipe@0.1.1': {} + '@atcute/util-text@1.3.4': + dependencies: + unicode-segmenter: 0.17.1 - '@atproto-labs/pipe@0.2.3': {} + '@atproto-labs/did-resolver@0.3.4': + dependencies: + '@atproto-labs/fetch': 0.3.3 + '@atproto-labs/pipe': 0.2.3 + '@atproto-labs/simple-store': 0.4.3 + '@atproto-labs/simple-store-memory': 0.2.3 + '@atproto/did': 0.5.3 + zod: 3.25.76 - '@atproto-labs/simple-store-memory@0.1.4': + '@atproto-labs/fetch@0.3.3': dependencies: - '@atproto-labs/simple-store': 0.3.0 - lru-cache: 10.4.3 + '@atproto-labs/pipe': 0.2.3 + + '@atproto-labs/pipe@0.2.3': {} '@atproto-labs/simple-store-memory@0.2.3': dependencies: '@atproto-labs/simple-store': 0.4.3 lru-cache: 10.4.3 - '@atproto-labs/simple-store@0.3.0': {} - '@atproto-labs/simple-store@0.4.3': {} '@atproto/api@0.20.25': @@ -11214,13 +11252,6 @@ snapshots: tlds: 1.261.0 zod: 3.25.76 - '@atproto/common-web@0.4.21': - dependencies: - '@atproto/lex-data': 0.0.15 - '@atproto/lex-json': 0.0.16 - '@atproto/syntax': 0.5.4 - zod: 3.25.76 - '@atproto/common-web@0.5.3': dependencies: '@atproto/lex-data': 0.1.4 @@ -11242,30 +11273,10 @@ snapshots: '@noble/hashes': 1.8.0 uint8arrays: 5.1.1 - '@atproto/did@0.2.4': - dependencies: - zod: 3.25.76 - '@atproto/did@0.5.3': 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-builder@0.1.5': dependencies: '@atproto/lex-document': 0.1.3 @@ -11287,13 +11298,6 @@ snapshots: '@atproto/lex-schema': 0.1.6 tslib: 2.8.1 - '@atproto/lex-data@0.0.15': - dependencies: - multiformats: 9.9.0 - tslib: 2.8.1 - uint8arrays: 3.0.0 - unicode-segmenter: 0.14.5 - '@atproto/lex-data@0.1.4': dependencies: multiformats: 13.4.2 @@ -11318,11 +11322,6 @@ snapshots: '@atproto/syntax': 0.6.4 tslib: 2.8.1 - '@atproto/lex-json@0.0.16': - dependencies: - '@atproto/lex-data': 0.0.15 - tslib: 2.8.1 - '@atproto/lex-json@0.1.3': dependencies: '@atproto/lex-data': 0.1.4 @@ -11364,14 +11363,6 @@ snapshots: tslib: 2.8.1 yargs: 18.0.0 - '@atproto/lexicon@0.6.2': - dependencies: - '@atproto/common-web': 0.4.21 - '@atproto/syntax': 0.5.4 - iso-datestring-validator: 2.2.2 - multiformats: 9.9.0 - zod: 3.25.76 - '@atproto/lexicon@0.7.4': dependencies: '@atproto/common-web': 0.5.3 @@ -11379,40 +11370,6 @@ snapshots: multiformats: 13.4.2 zod: 3.25.76 - '@atproto/oauth-client-node@0.3.15': - dependencies: - '@atproto-labs/did-resolver': 0.2.5 - '@atproto-labs/handle-resolver-node': 0.1.24 - '@atproto-labs/simple-store': 0.3.0 - '@atproto/did': 0.2.4 - '@atproto/jwk': 0.6.0 - '@atproto/jwk-jose': 0.1.11 - '@atproto/jwk-webcrypto': 0.2.0 - '@atproto/oauth-client': 0.5.13 - '@atproto/oauth-types': 0.6.1 - - '@atproto/oauth-client@0.5.13': - dependencies: - '@atproto-labs/did-resolver': 0.2.5 - '@atproto-labs/fetch': 0.2.3 - '@atproto-labs/handle-resolver': 0.3.5 - '@atproto-labs/identity-resolver': 0.3.5 - '@atproto-labs/simple-store': 0.3.0 - '@atproto-labs/simple-store-memory': 0.1.4 - '@atproto/did': 0.2.4 - '@atproto/jwk': 0.6.0 - '@atproto/oauth-types': 0.6.1 - '@atproto/xrpc': 0.7.7 - core-js: 3.49.0 - multiformats: 9.9.0 - zod: 3.25.76 - - '@atproto/oauth-types@0.6.1': - dependencies: - '@atproto/did': 0.2.4 - '@atproto/jwk': 0.6.0 - zod: 3.25.76 - '@atproto/repo@0.10.3': dependencies: '@atproto/common': 0.6.5 @@ -11424,20 +11381,11 @@ snapshots: varint: 6.0.0 zod: 3.25.76 - '@atproto/syntax@0.5.4': - dependencies: - tslib: 2.8.1 - '@atproto/syntax@0.6.4': dependencies: iso-datestring-validator: 2.2.2 tslib: 2.8.1 - '@atproto/xrpc@0.7.7': - dependencies: - '@atproto/lexicon': 0.6.2 - zod: 3.25.76 - '@atproto/xrpc@0.8.3': dependencies: '@atproto/lexicon': 0.7.4 @@ -15121,7 +15069,7 @@ snapshots: storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7)(vite-plus@0.2.2) type-fest: 2.19.0 vue: 3.5.39(typescript@6.0.3) - vue-component-type-helpers: 3.3.7 + vue-component-type-helpers: 3.3.8 '@swc/helpers@0.5.23': dependencies: @@ -17843,6 +17791,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + esm-resolve@1.0.11: {} espree@11.2.0: @@ -18962,8 +18912,6 @@ snapshots: jiti@2.7.0: {} - jose@5.10.0: {} - jose@6.2.3: {} js-beautify@1.15.4: @@ -19754,12 +19702,12 @@ snapshots: multiformats@13.4.2: {} - multiformats@9.9.0: {} - mute-stream@3.0.0: {} nanoid@3.3.15: {} + nanoid@5.1.16: {} + nanotar@0.3.0: {} napi-build-utils@2.0.0: {} @@ -22242,10 +22190,6 @@ snapshots: ufo@1.6.4: {} - uint8arrays@3.0.0: - dependencies: - multiformats: 9.9.0 - uint8arrays@5.1.1: dependencies: multiformats: 13.4.2 @@ -22287,8 +22231,6 @@ snapshots: undici-types@7.18.2: {} - undici@6.27.0: {} - unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -22312,6 +22254,8 @@ snapshots: unicode-segmenter@0.14.5: {} + unicode-segmenter@0.17.1: {} + unicorn-magic@0.3.0: {} unicorn-magic@0.4.0: {} @@ -22872,6 +22816,8 @@ snapshots: vue-component-type-helpers@3.3.7: {} + vue-component-type-helpers@3.3.8: {} + vue-data-ui@3.22.13(vue@3.5.39): dependencies: vue: 3.5.39(typescript@6.0.3) diff --git a/scripts/gen-jwk.ts b/scripts/gen-jwk.ts index 39a2740c87..852d32d8af 100644 --- a/scripts/gen-jwk.ts +++ b/scripts/gen-jwk.ts @@ -1,9 +1,8 @@ -import { JoseKey } from '@atproto/oauth-client-node' +import { generateClientAssertionKey } from '@atcute/oauth-node-client' async function run() { const kid = Date.now().toString() - const key = await JoseKey.generate(['ES256'], kid) - const jwk = key.privateJwk + const jwk = await generateClientAssertionKey(kid, 'ES256') console.log(JSON.stringify(jwk)) } diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index 0b58fcc9a1..3af7f571e1 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -1,5 +1,5 @@ -import type { OAuthSession } from '@atproto/oauth-client-node' -import { OAuthCallbackError } from '@atproto/oauth-client-node' +import type { AuthorizeTarget, OAuthSession } from '@atcute/oauth-node-client' +import { OAuthCallbackError } from '@atcute/oauth-node-client' import { createError, getQuery, sendRedirect, setCookie, getCookie, deleteCookie } from 'h3' import type { H3Event } from 'h3' import { SLINGSHOT_HOST } from '#shared/utils/constants' @@ -10,14 +10,23 @@ import { Client, isAtUriString, isTypedBlobRef } from '@atproto/lex' import * as app from '#shared/types/lexicons/app' import * as blue from '#shared/types/lexicons/blue' import { isAtIdentifierString } from '@atproto/lex' -import { scope } from '#server/utils/atproto/oauth' +import { sessionAsAgent } from '#server/utils/atproto/oauth' import { UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants' // @ts-expect-error virtual file from oauth module import { clientUri } from '#oauth/config' +import type { ActorIdentifier } from '@atcute/lexicons' +import { isActorIdentifier } from '@atcute/lexicons/syntax' const OAUTH_REQUEST_COOKIE_PREFIX = 'atproto_oauth_req' const slingshotClient = new Client({ service: `https://${SLINGSHOT_HOST}` }) +function isValidTarget(target: unknown): target is `https://${string}` | ActorIdentifier { + return ( + typeof target === 'string' && + (isAtIdentifierString(target) || (URL.canParse(target) && target.startsWith('https://'))) + ) +} + export default defineEventHandler(async event => { const config = useRuntimeConfig(event) if (!config.sessionPassword) { @@ -32,10 +41,7 @@ export default defineEventHandler(async event => { if (query.handle) { // Initiate auth flow - if ( - typeof query.handle !== 'string' || - (!query.handle.startsWith('https://') && !isAtIdentifierString(query.handle)) - ) { + if (!isValidTarget(query.handle)) { throw createError({ statusCode: 400, message: 'Invalid handle parameter', @@ -55,9 +61,13 @@ export default defineEventHandler(async event => { // Invalid URL, fall back to root } + const target: AuthorizeTarget = isActorIdentifier(query.handle) + ? { type: 'account', identifier: query.handle } + : { type: 'pds', serviceUrl: query.handle } + try { - const redirectUrl = await event.context.oauthClient.authorize(query.handle, { - scope, + const { url } = await event.context.oauthClient.authorize({ + target, prompt: query.create ? 'create' : undefined, // TODO: I do not believe this is working as expected on // an unsupported locale on the PDS. Gives Invalid at body.ui_locales @@ -66,7 +76,7 @@ export default defineEventHandler(async event => { state: encodeOAuthState(event, { redirectPath }), }) - return sendRedirect(event, redirectUrl.toString()) + return sendRedirect(event, url.toString()) } catch (error) { const message = error instanceof Error ? error.message : 'Failed to initiate authentication.' @@ -182,8 +192,8 @@ function generateRandomHexString(byteLength: number = 16): string { * @returns The original OAuth state if the id is valid * @throws An error if the id is missing or invalid, indicating a potential issue with cookies or expired state */ -function decodeOAuthState(event: H3Event, state: string | null): OAuthStateData { - if (!state) { +function decodeOAuthState(event: H3Event, state: unknown): OAuthStateData { + if (!state || typeof state !== 'string') { // May happen during transition period (if a user initiated auth flow before // the release with the new state handling, then tries to complete it after // the release). @@ -233,7 +243,7 @@ async function getMiniProfile(authSession: OAuthSession) { if (response.success) { const miniDoc = response.body - let avatar: string | undefined = await getAvatar(authSession.did, miniDoc.pds) + const avatar: string | undefined = await getAvatar(authSession.did, miniDoc.pds) return { ...miniDoc, @@ -242,7 +252,7 @@ async function getMiniProfile(authSession: OAuthSession) { } else { //If slingshot fails we still want to set some key info we need. const pdsBase = (await authSession.getTokenInfo()).aud - let avatar: string | undefined = await getAvatar(authSession.did, pdsBase) + const avatar: string | undefined = await getAvatar(authSession.did, pdsBase) return { did: authSession.did, handle: 'Not available', @@ -285,7 +295,7 @@ async function getAvatar(did: DidString, pds: string) { } async function getNpmxProfile(handle: string, authSession: OAuthSession) { - const client = new Client(authSession) + const client = new Client(sessionAsAgent(authSession)) // get existing npmx profile OR create a new one const profileUri = `at://${client.did}/dev.npmx.actor.profile/self` diff --git a/server/api/social/like.delete.ts b/server/api/social/like.delete.ts index 103d16cca8..ec9b71a2bb 100644 --- a/server/api/social/like.delete.ts +++ b/server/api/social/like.delete.ts @@ -1,5 +1,6 @@ import * as v from 'valibot' import { Client } from '@atproto/lex' +import { sessionAsAgent } from '#server/utils/atproto/oauth' import * as dev from '#shared/types/lexicons/dev' import { PackageLikeBodySchema } from '#shared/schemas/social' import { throwOnMissingOAuthScope } from '#server/utils/atproto/oauth' @@ -11,8 +12,8 @@ export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) } - //Checks if the user has a scope to like packages - await throwOnMissingOAuthScope(oAuthSession, LIKES_SCOPE) + // Checks if the user has a scope to like packages + await throwOnMissingOAuthScope(oAuthSession, dev.npmx.feed.like.$nsid) const body = v.parse(PackageLikeBodySchema, await readBody(event)) @@ -24,7 +25,7 @@ export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { ) if (getTheUsersLikedRecord) { - const client = new Client(oAuthSession) + const client = new Client(sessionAsAgent(oAuthSession)) await client.delete(dev.npmx.feed.like, { rkey: getTheUsersLikedRecord.rkey, diff --git a/server/api/social/like.post.ts b/server/api/social/like.post.ts index 3bdcf54105..5cdafa0976 100644 --- a/server/api/social/like.post.ts +++ b/server/api/social/like.post.ts @@ -1,8 +1,8 @@ import * as v from 'valibot' import { Client, toDatetimeString } from '@atproto/lex' +import { sessionAsAgent } from '#server/utils/atproto/oauth' import * as dev from '#shared/types/lexicons/dev' import type { UriString } from '@atproto/lex' -import { LIKES_SCOPE } from '#shared/utils/constants' import { PackageLikeBodySchema } from '#shared/schemas/social' import { throwOnMissingOAuthScope } from '#server/utils/atproto/oauth' @@ -13,8 +13,8 @@ export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) } - //Checks if the user has a scope to like packages - await throwOnMissingOAuthScope(oAuthSession, LIKES_SCOPE) + // Checks if the user has a scope to like packages + await throwOnMissingOAuthScope(oAuthSession, dev.npmx.feed.like.$nsid) const body = v.parse(PackageLikeBodySchema, await readBody(event)) @@ -27,7 +27,7 @@ export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { } const subjectRef = PACKAGE_SUBJECT_REF(body.packageName) - const client = new Client(oAuthSession) + const client = new Client(sessionAsAgent(oAuthSession)) const like = dev.npmx.feed.like.$build({ createdAt: toDatetimeString(new Date()), diff --git a/server/api/social/profile/[identifier]/index.put.ts b/server/api/social/profile/[identifier]/index.put.ts index f71e234d8b..79df38aa2a 100644 --- a/server/api/social/profile/[identifier]/index.put.ts +++ b/server/api/social/profile/[identifier]/index.put.ts @@ -1,5 +1,6 @@ import { parse } from 'valibot' import { Client } from '@atproto/lex' +import { sessionAsAgent } from '#server/utils/atproto/oauth' import * as dev from '#shared/types/lexicons/dev' import type { NPMXProfile } from '#shared/types/social' import { ProfileEditBodySchema } from '#shared/schemas/social' @@ -11,7 +12,7 @@ export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) } - await throwOnMissingOAuthScope(oAuthSession, PROFILE_SCOPE) + await throwOnMissingOAuthScope(oAuthSession, dev.npmx.actor.profile.$nsid) const requestBody = await readBody(event) if (requestBody.website && !URL.canParse(requestBody.website)) { @@ -19,7 +20,7 @@ export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { } const body = parse(ProfileEditBodySchema, requestBody) - const client = new Client(oAuthSession) + const client = new Client(sessionAsAgent(oAuthSession)) const profile = dev.npmx.actor.profile.$build({ displayName: body.displayName, diff --git a/server/plugins/oauth-client.ts b/server/plugins/oauth-client.ts index 7f888271ba..6bc90792f4 100644 --- a/server/plugins/oauth-client.ts +++ b/server/plugins/oauth-client.ts @@ -1,7 +1,7 @@ -import type { NodeOAuthClient } from '@atproto/oauth-client-node' +import type { OAuthClient } from '@atcute/oauth-node-client' /** - * Creates a long living instance of the NodeOAuthClient. + * Creates a long living instance of the OAuthClient. */ export default defineNitroPlugin(async nitroApp => { const oauthClient = await getNodeOAuthClient() @@ -15,6 +15,6 @@ export default defineNitroPlugin(async nitroApp => { // Extend the H3EventContext type declare module 'h3' { interface H3EventContext { - oauthClient: NodeOAuthClient + oauthClient: OAuthClient } } diff --git a/server/routes/.well-known/jwks.json.get.ts b/server/routes/.well-known/jwks.json.get.ts index f7c2eda628..ef7921914a 100644 --- a/server/routes/.well-known/jwks.json.get.ts +++ b/server/routes/.well-known/jwks.json.get.ts @@ -1,11 +1,8 @@ -import { loadJWKs } from '#server/utils/atproto/oauth' - -export default defineEventHandler(async _ => { - const keys = await loadJWKs() - if (!keys) { - console.error('Failed to load JWKs. May not be set') - return [] +export default defineEventHandler(event => { + const jwks = event.context.oauthClient.jwks + if (!jwks) { + console.error('JWKS not configured (running as public client)') + return { keys: [] } } - - return keys.publicJwks + return jwks }) diff --git a/server/routes/oauth-client-metadata.json.get.ts b/server/routes/oauth-client-metadata.json.get.ts index 60706d617e..f57db77f02 100644 --- a/server/routes/oauth-client-metadata.json.get.ts +++ b/server/routes/oauth-client-metadata.json.get.ts @@ -1,5 +1,3 @@ -export default defineEventHandler(async _ => { - const keyset = await loadJWKs() - const pk = keyset?.findPrivateKey({ usage: 'sign' }) - return getOauthClientMetadata(pk?.alg) +export default defineEventHandler(event => { + return event.context.oauthClient.metadata }) diff --git a/server/utils/atproto/lock.ts b/server/utils/atproto/lock.ts index b91a1ac812..a6b0f51e39 100644 --- a/server/utils/atproto/lock.ts +++ b/server/utils/atproto/lock.ts @@ -1,16 +1,13 @@ import { setTimeout } from 'node:timers/promises' -import type { RuntimeLock } from '@atproto/oauth-client-node' -import { requestLocalLock } from '@atproto/oauth-client-node' +import type { LockFunction } from '@atcute/oauth-node-client' import { Redis } from '@upstash/redis' -type Awaitable = T | PromiseLike - /** * Creates a distributed lock using Upstash Redis. * Falls back gracefully if the lock cannot be acquired. */ -function createUpstashLock(redis: Redis): RuntimeLock { - return async (key: string, fn: () => Awaitable): Promise => { +function createUpstashLock(redis: Redis): LockFunction { + return async (key: string, fn: () => Promise): Promise => { const lockKey = `oauth:lock:${key}` const lockValue = crypto.randomUUID() const lockTTL = 30 // seconds @@ -50,9 +47,9 @@ function createUpstashLock(redis: Redis): RuntimeLock { /** * Returns the appropriate lock mechanism based on environment: * - Production with Upstash config: distributed Redis lock - * - Otherwise: in-memory lock (sufficient for single instance) + * - Otherwise: undefined (OAuthClient uses its own in-memory lock by default) */ -export function getOAuthLock(): RuntimeLock { +export function getOAuthLock(): LockFunction | undefined { const config = useRuntimeConfig() // Use distributed lock in production if Upstash is configured @@ -64,6 +61,6 @@ export function getOAuthLock(): RuntimeLock { return createUpstashLock(redis) } - // Fall back to in-memory lock for dev/preview or when Redis isn't configured - return requestLocalLock + // Fall back to undefined for dev/preview — OAuthClient defaults to in-memory lock + return undefined } diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index 9bdc52f054..0b0f5236cb 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -1,4 +1,4 @@ -import type { NodeSavedSession, NodeSavedSessionStore } from '@atproto/oauth-client-node' +import type { SessionStore, StoredSession } from '@atcute/oauth-node-client' import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage' // Refresh tokens from a confidential client should last for 180 days, each new refresh of access token resets @@ -6,7 +6,7 @@ import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage' // Note: This expiration only lasts this long in production. Local dev is 2 weeks const SESSION_EXPIRATION = CACHE_MAX_AGE_ONE_DAY * 179 -export class OAuthSessionStore implements NodeSavedSessionStore { +export class OAuthSessionStore implements SessionStore { private readonly cache: CacheAdapter constructor() { @@ -14,19 +14,23 @@ export class OAuthSessionStore implements NodeSavedSessionStore { } private createStorageKey(did: string) { - return `sessions:${did}` + return `sessions:v2:${did}` } - async get(key: string): Promise { - let session = await this.cache.get(this.createStorageKey(key)) + async get(key: string): Promise { + const session = await this.cache.get(this.createStorageKey(key)) return session ?? undefined } - async set(key: string, val: NodeSavedSession) { - await this.cache.set(this.createStorageKey(key), val, SESSION_EXPIRATION) + async set(key: string, val: StoredSession) { + await this.cache.set(this.createStorageKey(key), val, SESSION_EXPIRATION) } - async del(key: string) { + async delete(key: string) { await this.cache.delete(this.createStorageKey(key)) } + + async clear() { + // Cache adapter does not expose bulk-clear; individual sessions expire via TTL + } } diff --git a/server/utils/atproto/oauth-state-store.ts b/server/utils/atproto/oauth-state-store.ts index b04da8ff8c..ae4b85858f 100644 --- a/server/utils/atproto/oauth-state-store.ts +++ b/server/utils/atproto/oauth-state-store.ts @@ -1,10 +1,10 @@ -import type { NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node' +import type { StateStore, StoredState } from '@atcute/oauth-node-client' import { OAUTH_CACHE_STORAGE_BASE } from './storage' // It is recommended that oauth state is only saved for 30 minutes const STATE_EXPIRATION = CACHE_MAX_AGE_ONE_MINUTE * 30 -export class OAuthStateStore implements NodeSavedStateStore { +export class OAuthStateStore implements StateStore { private readonly cache: CacheAdapter constructor() { @@ -15,16 +15,20 @@ export class OAuthStateStore implements NodeSavedStateStore { return `state:${key}` } - async get(key: string): Promise { - const state = await this.cache.get(this.createStorageKey(key)) + async get(key: string): Promise { + const state = await this.cache.get(this.createStorageKey(key)) return state ?? undefined } - async set(key: string, val: NodeSavedState) { - await this.cache.set(this.createStorageKey(key), val, STATE_EXPIRATION) + async set(key: string, val: StoredState) { + await this.cache.set(this.createStorageKey(key), val, STATE_EXPIRATION) } - async del(key: string) { + async delete(key: string) { await this.cache.delete(this.createStorageKey(key)) } + + async clear() { + // Cache adapter does not expose bulk-clear; individual states expire via TTL + } } diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index ae9ba489d2..4559c00f3e 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -1,63 +1,42 @@ -import type { - OAuthClientMetadata, - OAuthRedirectUri, - OAuthSession, - WebUri, -} from '@atproto/oauth-client-node' -import { JoseKey, Keyset, oauthRedirectUriSchema, webUriSchema } from '@atproto/oauth-client-node' +import { + scope, + OAuthClient, + type OAuthSession, + type ClientAssertionPrivateJwk, +} from '@atcute/oauth-node-client' +import { + LocalActorResolver, + CompositeHandleResolver, + CompositeDidDocumentResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + DohJsonHandleResolver, +} from '@atcute/identity-resolver' +import { NodeDnsHandleResolver } from '@atcute/identity-resolver-node' +import type { Agent } from '@atproto/lex' import type { EventHandlerRequest, H3Event, SessionManager } from 'h3' -import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client-node' import { getOAuthLock } from '#server/utils/atproto/lock' import { useOAuthStorage } from '#server/utils/atproto/storage' -import { LIKES_SCOPE, PROFILE_SCOPE } from '#shared/utils/constants' import type { UserServerSession } from '#shared/types/userSession' // @ts-expect-error virtual file from oauth module import { clientUri } from '#oauth/config' +import * as dev from '#shared/types/lexicons/dev' -// TODO: If you add writing a new record you will need to add a scope for it -export const scope = `atproto ${LIKES_SCOPE} ${PROFILE_SCOPE}` +const SCOPES = [ + scope.repo({ + collection: [dev.npmx.feed.like.$nsid, dev.npmx.actor.profile.$nsid], + action: ['create', 'update', 'delete'], + }), +] /** - * Resolves a did to a handle via DoH or via the http website calls + * Creates an @atproto/lex-compatible Agent from an atcute OAuthSession. + * Required for compatibility while @atproto/lex is still in use. */ -export const handleResolver = new AtprotoDohHandleResolver({ - dohEndpoint: 'https://cloudflare-dns.com/dns-query', -}) - -/** - * Generates the OAuth client metadata. pkAlg is used to signify that the OAuth client is confidential - */ -export function getOauthClientMetadata(pkAlg: string | undefined = undefined): OAuthClientMetadata { - const redirect_uri: OAuthRedirectUri = oauthRedirectUriSchema.parse( - `${clientUri}/api/auth/atproto`, - ) - - const client_id = - import.meta.dev || import.meta.test - ? `http://localhost?redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}` - : `${clientUri}/oauth-client-metadata.json` - - const jwks_uri: WebUri | undefined = pkAlg - ? webUriSchema.parse(`${clientUri}/.well-known/jwks.json`) - : undefined - +export function sessionAsAgent(session: OAuthSession): Agent { return { - client_name: 'npmx.dev', - client_id, - client_uri: clientUri, - logo_uri: webUriSchema.parse(`${clientUri}/logo-icon.svg`), - scope, - redirect_uris: [redirect_uri], - grant_types: ['authorization_code', 'refresh_token'], - application_type: 'web', - dpop_bound_access_tokens: true, - response_types: ['code'], - subject_type: 'public', - authorization_signed_response_alg: 'RS256', - // confidential client values - token_endpoint_auth_method: pkAlg ? 'private_key_jwt' : 'none', - jwks_uri, - token_endpoint_auth_signing_alg: pkAlg, + did: session.did, + fetchHandler: (path, init) => session.handle(path, init), } } @@ -67,35 +46,78 @@ type EventHandlerWithOAuthSession = ( serverSession: SessionManager, ) => Promise -export async function getNodeOAuthClient(): Promise { - const { stateStore, sessionStore } = useOAuthStorage() - - // These are optional and not expected or can be used easily in local development, only in production +export async function getNodeOAuthClient(): Promise { + const { sessions, states } = useOAuthStorage() const keyset = await loadJWKs() - const pk = keyset?.findPrivateKey({ usage: 'sign' }) - const clientMetadata = getOauthClientMetadata(pk?.alg) - - return new NodeOAuthClient({ - stateStore, - sessionStore, - clientMetadata, - requestLock: getOAuthLock(), - handleResolver, - keyset, + const stores = { sessions, states } + const requestLock = getOAuthLock() + const redirectUri = new URL('/api/auth/atproto', clientUri).toString() + + console.log('__debug', clientUri, Array.isArray(keyset) ? keyset.length : !!keyset) + + const actorResolver = new LocalActorResolver({ + handleResolver: new CompositeHandleResolver({ + strategy: 'race', + methods: { + dns: new NodeDnsHandleResolver(), + http: new DohJsonHandleResolver({ dohUrl: 'https://cloudflare-dns.com/dns-query' }), + }, + }), + didDocumentResolver: new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver(), + }, + }), + }) + + if (keyset) { + return new OAuthClient({ + metadata: { + client_id: `${clientUri}/oauth-client-metadata.json`, + redirect_uris: [redirectUri], + client_name: 'npmx.dev', + client_uri: clientUri, + logo_uri: `${clientUri}/logo-icon.svg`, + jwks_uri: `${clientUri}/.well-known/jwks.json`, + scope: SCOPES, + }, + keyset, + stores, + requestLock, + actorResolver, + }) + } + + return new OAuthClient({ + metadata: { + redirect_uris: [redirectUri], + scope: SCOPES, + }, + stores, + requestLock, + actorResolver, }) } -export async function loadJWKs(): Promise { +export async function loadJWKs(): Promise { // If we ever need to add multiple JWKs to rotate keys we will need to add a new one // under a new variable and update here - const jwkOne = useRuntimeConfig().oauthJwkOne - if (!jwkOne) return undefined - - // For multiple keys if we need to rotate - // const keys = await Promise.all([JoseKey.fromImportable(jwkOne)]) + const raw = useRuntimeConfig().oauthJwkOne + if (!raw) return undefined + + const one = + typeof raw === 'string' + ? (JSON.parse(raw) as ClientAssertionPrivateJwk) + : // nuxt can auto parse the string as JSON depending + // on how the env var value is formatted + (raw as unknown as ClientAssertionPrivateJwk) + + if (!one) { + throw new Error('failed to parse jwk from env var') + } - const keys = await JoseKey.fromImportable(jwkOne) - return new Keyset([keys]) + return [one] } async function getOAuthSession(event: H3Event): Promise<{ @@ -114,8 +136,6 @@ async function getOAuthSession(event: H3Event): Promise<{ const oauthSession = await event.context.oauthClient.restore(currentSession.public.did) return { oauthSession, serverSession } } catch (error) { - // Log error safely without using util.inspect on potentially problematic objects - // The @atproto library creates error objects with getters that crash Node's util.inspect // eslint-disable-next-line no-console console.error( '[oauth] Failed to get session:', @@ -129,6 +149,9 @@ async function getOAuthSession(event: H3Event): Promise<{ * Throws if the logged in OAuth Session does not have the required scopes. * As we add new scopes we need to check if the client has the ability to use it. * If not need to let the client know to redirect the user to the PDS to upgrade their scopes. + * + * @todo should do a more thorough check by parsing scopes + * * @param oAuthSession - The current OAuth session from the event * @param requiredScopes - The required scope you are checking if you can use */ diff --git a/server/utils/atproto/storage.ts b/server/utils/atproto/storage.ts index a62c9ceb71..99549af9ce 100644 --- a/server/utils/atproto/storage.ts +++ b/server/utils/atproto/storage.ts @@ -5,7 +5,7 @@ export const OAUTH_CACHE_STORAGE_BASE = 'atproto:oauth' export const useOAuthStorage = () => { return { - stateStore: new OAuthStateStore(), - sessionStore: new OAuthSessionStore(), + sessions: new OAuthSessionStore(), + states: new OAuthStateStore(), } } diff --git a/shared/types/userSession.ts b/shared/types/userSession.ts index 53bb09d71e..5f2724fa85 100644 --- a/shared/types/userSession.ts +++ b/shared/types/userSession.ts @@ -1,9 +1,9 @@ -import type { NodeSavedSession } from '@atproto/oauth-client-node' +import type { Did } from '@atcute/lexicons' export interface UserServerSession { public?: | { - did: string + did: Did handle: string pds: string avatar?: string @@ -19,5 +19,5 @@ export interface UserServerSession { // DO NOT USE // Here for historic reasons to redirect users logged in with the previous oauth to login again - oauthSession?: NodeSavedSession | undefined + oauthSession?: unknown } diff --git a/shared/utils/constants.ts b/shared/utils/constants.ts index bf8030c9ef..602ec7a8da 100644 --- a/shared/utils/constants.ts +++ b/shared/utils/constants.ts @@ -1,5 +1,3 @@ -import * as dev from '../types/lexicons/dev' - // Duration export const CACHE_MAX_AGE_ONE_MINUTE = 60 export const CACHE_MAX_AGE_FIVE_MINUTES = 60 * 5 @@ -63,8 +61,6 @@ export const SLINGSHOT_HOST = 'slingshot.microcosm.blue' export const PACKAGE_SUBJECT_REF = (packageName: string) => `https://npmx.dev/package/${packageName}` // OAuth scopes as we add new ones we need to check these on certain actions. If not redirect the user to login again to upgrade the scopes -export const LIKES_SCOPE = `repo:${dev.npmx.feed.like.$nsid}` -export const PROFILE_SCOPE = `repo:${dev.npmx.actor.profile.$nsid}` export const NPMX_DEV_DID = 'did:plc:u5zp7npt5kpueado77kuihyz' export const TID_CLOCK_ID = 3 diff --git a/test/unit/server/utils/atproto/lock.spec.ts b/test/unit/server/utils/atproto/lock.spec.ts index 0cf6abb037..1c47bdf30e 100644 --- a/test/unit/server/utils/atproto/lock.spec.ts +++ b/test/unit/server/utils/atproto/lock.spec.ts @@ -13,11 +13,6 @@ vi.mock('@upstash/redis', () => ({ }, })) -const mockLocalLock = vi.fn() -vi.mock('@atproto/oauth-client-node', () => ({ - requestLocalLock: mockLocalLock, -})) - const mockConfig = { upstash: { redisRestUrl: '', @@ -44,28 +39,28 @@ describe('lock', () => { mockConfig.upstash.redisRestToken = '' }) - it('returns local lock when upstash is not configured', () => { + it('returns undefined when upstash is not configured', () => { const lock = getOAuthLock() - expect(lock).toBe(mockLocalLock) + expect(lock).toBeUndefined() }) - it('returns local lock when only redisRestUrl is set', () => { + it('returns undefined when only redisRestUrl is set', () => { mockConfig.upstash.redisRestUrl = 'https://totally-a-redis-server.com' const lock = getOAuthLock() - expect(lock).toBe(mockLocalLock) + expect(lock).toBeUndefined() }) - it('returns local lock when only redisRestToken is set', () => { + it('returns undefined when only redisRestToken is set', () => { mockConfig.upstash.redisRestToken = 'super-fancy-secret-token' const lock = getOAuthLock() - expect(lock).toBe(mockLocalLock) + expect(lock).toBeUndefined() }) it('returns upstash lock when both url and token are configured', () => { mockConfig.upstash.redisRestUrl = 'https://redis.redis.redis' mockConfig.upstash.redisRestToken = 'token-123' const lock = getOAuthLock() - expect(lock).not.toBe(mockLocalLock) + expect(lock).not.toBeUndefined() expect(typeof lock).toBe('function') }) @@ -75,7 +70,7 @@ describe('lock', () => { mockRedisDel.mockResolvedValueOnce(1) const lock = getUpstashLock() - const result = await lock('test-key', () => 'hello') + const result = await lock!('test-key', () => Promise.resolve('hello')) expect(result).toBe('hello') expect(mockRedisSet).toHaveBeenCalledOnce() @@ -94,7 +89,7 @@ describe('lock', () => { mockRedisDel.mockResolvedValueOnce(1) const lock = getUpstashLock() - const result = await lock('retry-key', () => 42) + const result = await lock!('retry-key', () => Promise.resolve(42)) expect(result).toBe(42) expect(mockRedisSet).toHaveBeenCalledTimes(2) @@ -105,7 +100,7 @@ describe('lock', () => { mockRedisSet.mockResolvedValueOnce(null).mockResolvedValueOnce(null) const lock = getUpstashLock() - const result = await lock('no-lock-key', () => 'fallback') + const result = await lock!('no-lock-key', () => Promise.resolve('fallback')) expect(result).toBe('fallback') expect(mockRedisSet).toHaveBeenCalledTimes(2) @@ -118,7 +113,7 @@ describe('lock', () => { mockRedisGet.mockResolvedValueOnce('some-other-uuid') const lock = getUpstashLock() - await lock('stolen-key', () => 'done') + await lock!('stolen-key', () => Promise.resolve('done')) expect(mockRedisGet).toHaveBeenCalledWith('oauth:lock:stolen-key') expect(mockRedisDel).not.toHaveBeenCalled() @@ -131,7 +126,7 @@ describe('lock', () => { const lock = getUpstashLock() await expect( - lock('error-key', () => { + lock!('error-key', () => { throw new Error('boom') }), ).rejects.toThrow('boom') @@ -145,7 +140,7 @@ describe('lock', () => { mockRedisDel.mockResolvedValueOnce(1) const lock = getUpstashLock() - const result = await lock('async-key', async () => { + const result = await lock!('async-key', async () => { await setTimeout(10) return 'async-result' })